Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: tree-shaking in blade components #1045

Merged
merged 13 commits into from Mar 23, 2023
Merged

Conversation

saurabhdaware
Copy link
Member

@saurabhdaware saurabhdaware commented Mar 10, 2023

Description

Reduces bundle-size from 44kb to 13kb (for an App that is only using Button component)

Fixes #959

Before

image

After

image

Issue

😇 This creates no side-effects

const MyComponent = () => {};
MyComponent.displayName = 'MyComponent';
export { MyComponent }; 

😇 This creates no side-effects

const _MyComponent = () => {};
const MyComponent = /*#__PURE__*/ React.forwardRef(_MyComponent); // In this case, PURE comment is added by babel so we don't have to explicitly add it
// MyComponent.displayName = 'MyComponent';
export { MyComponent }; 

👿 This CREATES side-effects

Here, it considers the .displayName assignment on ref as a side-effect and bundles the entire file

const _MyComponent = () => {};
const MyComponent = /*#__PURE__*/ React.forwardRef(_MyComponent)
MyComponent.displayName = 'MyComponent';
export { MyComponent }; 

😇 Solution - This creates no side-effects

const _MyComponent = () => {};
const MyComponentWithRef = /*#__PURE__*/ React.forwardRef(_MyComponent)
const MyComponent = /*#__PURE__*/ Object.assign(MyComponentWithRef, {
  componentId: 'MyComponent',
});;
export { MyComponent }; 

Also, in ActionList's case, having multiple components using each other in same file and defining .componentId was creating side-effects as well. Doing the same Object.assign with PURE comment fixed that as well.

@changeset-bot
Copy link

changeset-bot bot commented Mar 10, 2023

🦋 Changeset detected

Latest commit: 4cda6f6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@razorpay/blade Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codesandbox-ci
Copy link

codesandbox-ci bot commented Mar 10, 2023

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 4cda6f6:

Sandbox Source
razorpay/blade: basic Configuration

@@ -130,9 +129,9 @@ const ActionListSection: WithComponentId<ActionListSectionProps> = ({
);
};

ActionListSection.componentId = componentIds.ActionListSection;
/*#__PURE__*/ Object.assign(ActionListSection, { componentId: componentIds.ActionListSection });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅 this looks scary

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol check it out now. It got scarier 😆

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2023

✅ PR title follows Conventional Commits specification.

@@ -443,7 +443,8 @@ const _BaseButton: React.ForwardRefRenderFunction<BladeElementRef, BaseButtonPro
);
};

const BaseButton = React.forwardRef(_BaseButton);
BaseButton.displayName = 'BaseButton';
const BaseButton = /*#__PURE__*/ Object.assign(React.forwardRef(_BaseButton), {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also add the explanation as comment to one particular component (probably button)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need a lint rule to enforce this, easy to miss out. How did you even figure this out lol 😸

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw is there a babel plugin that automatically does this transformation? Seems like a common issue libraries might be running into 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is this babel plugin - https://github.com/mbrowne/babel-plugin-pure-static-props. I tried this first but it didn't work. I think it works when props are on react component and not when they are on refs. Even for react components it didn't work at lots of places for some reason. Couldn't find any other plugin either.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need a lint rule to enforce this, easy to miss out.

Yes definitely! Exploring if it can be eslint rule or babel plugin.

How did you even figure this out lol 😸

Lol it was tricky

@@ -101,7 +101,8 @@ const _StyledBaseButton: React.ForwardRefRenderFunction<BladeElementRef, StyledB
);
};

const StyledBaseButton = React.forwardRef(_StyledBaseButton);
StyledBaseButton.displayName = 'StyledBaseButton';
const StyledBaseButton = /*#__PURE__*/ Object.assign(React.forwardRef(_StyledBaseButton), {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here, what is the main factor?

The PURE comment or the Object.assign? which one does the magic?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PURE comment.

You can also define component inside a function and add prop inside that and return the component with imediate function call and this would probably work.

const StyledBaseButton = /*#__PURE__*/ (() => {
  const MyComponent = () => {};
  MyComponent.displayName = 'MyComponent';
  return MyComponent;
})();

The PURE comment only works on function calls so Object.assign just seems cleaner way compared to any other option.

@anuraghazra
Copy link
Member

So here:

Why is the Input also got bundled when we are only importing Button?

image

@saurabhdaware
Copy link
Member Author

Why is the Input also got bundled when we are only importing Button?

Input is getting bundled because we have this autocompleteSuggestionType array as separate variable. This might need some change in logic (maybe move this array inside component render or try something else) so I didn't get much into this right now. I'll create separate issue for input treeshaking.

image

@burhanuday
Copy link
Contributor

Not sure why we are using the Pure pragma and not bundler level config? @saurabhdaware
Since most files will be side effects free, couldnt we use the sideEffects property in package json? I am sure webpack has this feature though not sure about Rollup. There is an issue referencing it here - rollup/rollup#2593 (comment)

@saurabhdaware
Copy link
Member Author

sideEffects property goes on consumer's package.json usually. You can see in the issue you mentioned, rollup doesn't have sideEffects property support yet. I also tried adding sideEffects option to consumer with current latest blade but that didn't strip off Blade's bundle either 🤔

Also I am not sure if I would prefer that because that can cause unexpected results in production unless you have a good eslint setup that stops you from adding side-effects (which I couldn't find yet).

In my opinion, Ideal way to maintain treeshaking might be having a bundle-size check in PRs, having eslint rules to stop us from adding side-effects and a babel plugin to take care of common patterns 🤔

@anuraghazra
Copy link
Member

Input is getting bundled because we have this autocompleteSuggestionType array as separate variable

I feel like something is wrong with our rollup setup or maybe with how the consumer end's config, because why is a variable which is defined as "const" treated as a side effect?

If we did something like "window.someVar = []" or "var someVar = []" then I could understand that.

Although we do transpile through babel which converts the const to var. so I think something can be done on the consumer's end.

@saurabhdaware can you push your testing setup on the /packages/example folder?

@saurabhdaware
Copy link
Member Author

saurabhdaware commented Mar 13, 2023

I feel like something is wrong with our rollup setup or maybe with how the consumer end's config, because why is a variable which is defined as "const" treated as a side effect?

Let me check this actually. I didn't try to fix it yet and assumed it is because of that variable (don't see anything else as of now)

Update: It might be because we're outputting ES5 but with ESM modules. I can see all variables turn into var in output.

* Once we upgrade to rollup 3.5.0, we can use treeshake.manualPureFunctions config from rollup instead of this plugin.
* https://rollupjs.org/configuration-options/#treeshake-manualpurefunctions
*/
const manualPureFunctions = () => ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:p why is this "manual", shouldn't this be "auto"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause we manually define which functions should be pure 😆

This is what rollup follows - https://rollupjs.org/configuration-options/#treeshake-manualpurefunctions (we're just on older version so can't use this yet)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we upgrade our rollup version and use this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can. It's a major version bump though so didn't spend much time right now. can pick it up next quarter

anuraghazra
anuraghazra previously approved these changes Mar 20, 2023
Copy link
Member

@anuraghazra anuraghazra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, check bundling the code and do a sanity on the examples package.

@saurabhdaware
Copy link
Member Author

LGTM, check bundling the code and do a sanity on the examples package.

Yup did. Working 👯

@chaitanyadeorukhkar
Copy link
Collaborator

I think its high time we add bundle size check on our PRs now 😅

@saurabhdaware
Copy link
Member Author

I think its high time we add bundle size check on our PRs now 😅

Yes!! Definitely required!


const CardHeaderIcon: WithComponentId<{ icon: IconComponent }> = ({ icon: Icon }) => {
const _CardHeaderIcon = ({ icon: Icon }: { icon: IconComponent }): JSX.Element => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why JSX.Element and not ReactNode or ReactElement

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is what we use generally for return type of React components so used the same

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert to earlier React.ReactElement otherwise this may break typings for consumers in a patch release

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been adding JSX.Element on all components I built 🙈 and there are few other instances of it in code.

I reverted the change here to how it was to not create breaking changes.

Created this issue to later change all JSX.Element instances to React.ReactElement #1089

Copy link
Contributor

@divyanshu013 divyanshu013 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, one comment to make the return types same as earlier.


const CardHeaderIcon: WithComponentId<{ icon: IconComponent }> = ({ icon: Icon }) => {
const _CardHeaderIcon = ({ icon: Icon }: { icon: IconComponent }): JSX.Element => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert to earlier React.ReactElement otherwise this may break typings for consumers in a patch release

Copy link
Contributor

@divyanshu013 divyanshu013 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@saurabhdaware saurabhdaware merged commit fe89e6f into master Mar 23, 2023
5 checks passed
@saurabhdaware saurabhdaware deleted the saurabh/fix/tree-shaking branch March 23, 2023 11:40
anuraghazra pushed a commit that referenced this pull request Apr 9, 2024
* fix: tree-shaking in blade components

* fix: displayName getting removed

* feat: add assignWithoutSideEffects function

* feat: remove .componentId assignments

* feat: eslint rule

* fix: baseinput test

* Create .changeset/red-trees-hear.md

* feat: revert to using React.ReactElement

* fix: revert some JSX.Element changes

* fix: revert ActionListHeader type
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Performance Issue: Doesn't support tree shaking
5 participants