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

🌲 Reduce bundle size on Web by 80% #3278

Merged
merged 11 commits into from Jun 28, 2022
Merged

Conversation

nandorojo
Copy link
Contributor

@nandorojo nandorojo commented May 31, 2022

Description

Fixes #2843.

Reduces Reanimated's bundle size on Web from 85kb to 17kb.

Changes

Zero user-facing changes are in this PR. It is strictly file organization changes to optimize Reanimated for tree shaking. After extensive testing, this PR reduced Reanimated's bundle size on Web by almost 80%.

In package.json, you'll notice that there is a new sideEffects array. This is a way to tell Webpack which files need to run global code rather than as standalone modules. I looked through every file that runs on Web which edits global at the top-level and found 2 files. Ideally, we could move all side effects into a single file rather than colocating them with other code that may go unused. But for now, this works. (Update: I moved some into their own file)

More on Webpack tree shaking and the sideEffects field can be found here: https://webpack.js.org/guides/tree-shaking/

Screenshots / GIFs

I created a fresh repo to fix tree shaking for Reanimated. The playground tests it by building in a Next.js app (the most common framework for Web).

Prior to this PR, this imported all code from Reanimated:

import Animated from 'react-native-reanimated'

This resulted in an absolutely massive impact on the Web bundle size, approximately 85kb, including tons of unused code (such as layout animations).

This PR reduces the Reanimated overhead down to about 17kb. In the future, I may be able to investigate further improvements by seeing where that size is actually coming from. But this PR is a massive step forward, since it provides no changes to the user.

I wrote about my findings here. There are plenty of detailed screenshots there.

Before

Massive bundle size on Web, nearly unusable.

After

It's now very optimized for tree shaking.

Checklist

  • Included code example that can be used to test this change
  • Updated TS types
  • Added TS types tests
  • Added unit / integration tests
  • Updated documentation
  • Ensured that CI passes

@nandorojo nandorojo changed the title Reduce bundle size on Web Reduce bundle size on Web by 80% May 31, 2022
@RazaShehryar
Copy link

We need this approved!

@nandorojo nandorojo changed the title Reduce bundle size on Web by 80% 🌲 Reduce bundle size on Web by 80% May 31, 2022
@piaskowyk piaskowyk self-requested a review May 31, 2022 13:04
@piaskowyk piaskowyk self-assigned this May 31, 2022
@piaskowyk
Copy link
Member

piaskowyk commented May 31, 2022

@nandorojo wow, so impressive! 🤯 I will test, merge, and release it after App.js conf. Is it acceptable for you?

@nandorojo
Copy link
Contributor Author

Sounds good!

@hirbod
Copy link
Contributor

hirbod commented Jun 12, 2022

@natew just stumbled across your Tamagui documentation (especially the bundle size part with reanimated).
I think this is also interesting for you. (hope you don't mind the random tagging)

@piaskowyk
Copy link
Member

Is it possible to replace export * as default with something that doesn't require an additional babel plugin? The directive as default needs the proposal-export-namespace-from plugin. I'd rather not force users to install additional dependencies if it isn't necessary. Apart from it, everything is great!

@nandorojo
Copy link
Contributor Author

Interesting, I didn’t know this required an extra plugin. It worked fine in my expo app — does expo’s babel preset include this?

@nandorojo
Copy link
Contributor Author

The benefit of export * as default is that it maintains tree shaking, since it doesn't pair the imported variables into a single object.

The current approach of making a unified Animated variable is that all the code of that object gets included.

I'll see if there is an alternative approach.

@nandorojo
Copy link
Contributor Author

I'm going to try by doing this:

import * as Animated from './Animated'

export default Animated

I think it should still tree shake, will let you know.

@piaskowyk
Copy link
Member

I tested:

import * as Animated from './Animated'
export default Animated

and the size increased from 17.1 kB to 37.6 kB 😕

@nandorojo
Copy link
Contributor Author

Hmm yeah I found the same thing...seems like export * as default is the only way to tree shake that I see.

I'll keep poking around.

@nandorojo
Copy link
Contributor Author

Related: facebook/metro#760

@piaskowyk
Copy link
Member

piaskowyk commented Jun 14, 2022

Summary:
export * as default requires @babel/plugin-proposal-export-namespace-from plugin, but we want to avoid additional manual user setup.

I tried to replace export * as default directive with equivalent syntax:

const rea2 = require('./reanimated2')
const Animated = require('./Animated')
const exports = rea2
exports.default = Animated
module.exports = exports

it works correctly, but usage of require breaks tree shaking - more about it here

Finally, I decided to call @babel/plugin-proposal-export-namespace-from plugin from Reanimated plugin to avoid additional setup.

@piaskowyk
Copy link
Member

@nandorojo could you verify if everything works for you properly?

@nandorojo
Copy link
Contributor Author

yeah! have some calls soon but I'll test it after

thanks for reviewing this closely

@nandorojo
Copy link
Contributor Author

the plug-in should be a direct dependency of reanimated, rather than a peer dependency, right? the current code looks like users will have to install the new plugin.

@piaskowyk
Copy link
Member

You're right. The plugin is required to run the application.

@nandorojo
Copy link
Contributor Author

ah, I missed the fact that you added it to the dependencies. cool

@nandorojo
Copy link
Contributor Author

nandorojo commented Jun 14, 2022

What's a good way for me to install this PR somewhere and test it?

@piaskowyk
Copy link
Member

I think you can use your repo (https://github.com/nandorojo/reanimated-tree-shaking) to install reanimated from commit:

"react-native-reanimated": "github:software-mansion/react-native-reanimated#e278369025e2abb22258ed3fbd1a78484b9afccf"

and then you just move files from node_modules/react-native-reanimated to lib/react-native-reanimated and everything should work.

I tested it on a new blank react-native app, and on the Example app in the Reanimated repo.

@nandorojo
Copy link
Contributor Author

nandorojo commented Jun 15, 2022

So I tried doing that, but it seems that there is no react-native-reanimated/lib file when I install from that commit. How do I generate lib? Is there a build script? I can't seem to find one.

I want to be sure to test on react-native-reanimated/lib and not on src, since transpilation can potentially break tree shaking, and the build step might transpile the library.

Should I just run yarn bob build?

@nandorojo
Copy link
Contributor Author

nandorojo commented Jun 15, 2022

These are the steps I took to make it work in my Next.js app:

  1. Install the version from this PR in my app
  2. Run npx @react-native-community/bob build inside of node_modules/react-native-reanimated
    i. Is this correct?
    ii. Change main to lib/commonjs/index in the Reanimated package.json
    iii. Change module to lib/module/index in the Reanimated package.json
  3. In my next.config.js:
    i. I added react-native-reanimated to next-transpile-modules
    ii. Maybe we could document this for Next.js users to optimize for tree shaking later

I'm curious if the step I took for 2 above is correct or not. If we're using Bob to build, then we should edit the package.json to point to lib/module and lib/commonjs. If we aren't using Bob – what is being used instead to generate the lib folder? Is it just a symlink?

The result looks correct. The page with reanimated is only 17kb greater than the plain RN page:

image

So, once I know how we do the build script, I can test that and then we'll be good to go.

@nandorojo
Copy link
Contributor Author

nandorojo commented Jun 15, 2022

Ah, I see, the build script is yarn type:generate. I tried this. It looks like yarn type:generate increased the bundle size significantly.

Screen Shot 2022-06-15 at 3 50 51 PM

This is what yarn type:generate results in:

Screen Shot 2022-06-15 at 3 52 00 PM

Notice that it's no longer doing export * as default. That's why it isn't tree shaking. If I manually override that, then it works 😅

If we switch to react-native-builder-bob for the build script, then the tree shaking works for the module output. I'm going to dig into the build script and see if it can preserve export * as default.

@nandorojo
Copy link
Contributor Author

nandorojo commented Jun 15, 2022

I fixed it by changing the module to ESNext in tsconfig.json. The output is now correct:

Screen Shot 2022-06-15 at 4 00 24 PM

I'll add that to the PR.

@nandorojo
Copy link
Contributor Author

Okay, tested & ready to merge!

I did the following since your changes:

  1. Rebase main
  2. Edit tsconfig.json's module to support newer syntax for export * as default
  3. Add lib/index as a side effect in package.json

@piaskowyk
Copy link
Member

It will be available in the next release - it should happen this week. Thanks for the help 🙌

@piaskowyk piaskowyk merged commit d04720c into software-mansion:main Jun 28, 2022
@nandorojo
Copy link
Contributor Author

Thanks for the review and help here @piaskowyk!

piaskowyk added a commit that referenced this pull request Jun 28, 2022
@shamilovtim
Copy link
Contributor

How do you get your editor to work with this export/import style? I've found in the past that this completely busts IDEs and Intellisense.

@nandorojo
Copy link
Contributor Author

maybe set vs code to the newest TS version

@blackbing
Copy link

How to check to bundle size difference?

I update react-native-reaniamted@2.4.1 to 2.9.1, but I didn't see the difference from bundle size via webpack-bundle-analyzer.

@nandorojo
Copy link
Contributor Author

I don't expect this to work on v2, since it includes v1 code that isn't optimized.

@edkimmel
Copy link

Hello, my reanimated web bundle actually increased when moving from 2.x to 3.1. Is there anything we have to do regarding reorganizing imports to get the tree shaking benefits?

@nandorojo
Copy link
Contributor Author

i think there’s been a regression, see #3650

fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
## Description

<!--
Description and motivation for this PR.

Inlude Fixes #<number> if this is fixing some issue.

Fixes # .
-->

Fixes software-mansion#2843.

Reduces Reanimated's bundle size on Web from `85kb` to `17kb`.

## Changes

Zero user-facing changes are in this PR. It is strictly file organization changes to optimize Reanimated for tree shaking. After extensive testing, this PR reduced Reanimated's bundle size on Web by almost 80%.

In `package.json`, you'll notice that there is a new `sideEffects` array. This is a way to tell Webpack which files need to run global code rather than as standalone modules. I looked through every file that runs on Web which edits `global` at the top-level and found 2 files. Ideally, we could move all side effects into a single file rather than colocating them with other code that may go unused. But for now, this works. (Update: I moved some into their own file)

More on Webpack tree shaking and the `sideEffects` field can be found here: https://webpack.js.org/guides/tree-shaking/

## Screenshots / GIFs

I created a fresh [repo](https://github.com/nandorojo/reanimated-tree-shaking) to fix tree shaking for Reanimated. The playground tests it by building in a Next.js app (the most common framework for Web).

Prior to this PR, this imported _all_ code from Reanimated:

```ts
import Animated from 'react-native-reanimated'
```

This resulted in an absolutely massive impact on the Web bundle size, approximately 85kb, including tons of unused code (such as layout animations).

This PR reduces the Reanimated overhead down to about `17kb`. In the future, I may be able to investigate further improvements by seeing where that size is actually coming from. But this PR is a massive step forward, since it provides no changes to the user.

I wrote about my findings [here](nandorojo/reanimated-tree-shaking#1). There are plenty of detailed screenshots there.

### Before

Massive bundle size on Web, nearly unusable.

### After

It's now very optimized for tree shaking.

## Checklist

- [x] Included code example that can be used to test this change
- [x] Updated TS types
- [ ] Added TS types tests
- [ ] Added unit / integration tests
- [ ] Updated documentation
- [x] Ensured that CI passes
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.

None yet

7 participants