Redux-toolkit: Tree shaking is hindered by dependencies

Created on 11 Jan 2019  路  15Comments  路  Source: reduxjs/redux-toolkit

I just checked why tree shaking is not working correctly as mentioned by @markerikson in his tweet. I tested using the following small example project.

__package.json__

{
  "name": "example",
  "private": true,
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "redux-starter-kit": ".."
  },
  "devDependencies": {
    "webpack": "^4.28.4",
    "webpack-cli": "^3.2.1"
  },
  "sideEffects": false
}

__webpack.config.js__

module.exports = {
  mode: 'production'
}

__src/index.js__

import { createAction } from 'redux-starter-kit'

const foo = createAction('foo')
console.log(foo(123))

When building, _all of redux-starter-kit ends up in the output_. Looking more at the Webpack tree-shaking optimization, I eventually found the Debugging Optimization Bailouts section, which suggests running Webpack with --display-optimization-bailout to find out when optimizations don't get applied. This is what I got:


> example@ build /Users/denis/Projects/redux-starter-kit/example
> webpack --display-optimization-bailout

Hash: 6728bcf74b0489ce1cf3
Version: webpack 4.28.4
Time: 390ms
Built at: 01/11/2019 10:56:01 PM
  Asset      Size  Chunks             Chunk Names
main.js  23.9 KiB       0  [emitted]  main
Entrypoint main = main.js
 [8] (webpack)/buildin/global.js 472 bytes {0} [built]
     ModuleConcatenation bailout: Module is not an ECMAScript module
 [9] (webpack)/buildin/harmony-module.js 573 bytes {0} [built]
     ModuleConcatenation bailout: Module is not an ECMAScript module
[14] ./src/index.js + 2 modules 13.4 KiB {0} [built]
     ModuleConcatenation bailout: Cannot concat with ../node_modules/immer/dist/immer.module.js (<- Module uses injected variables (process))
     ModuleConcatenation bailout: Cannot concat with ../node_modules/redux-devtools-extension/index.js (<- Module is not an ECMAScript module)
     ModuleConcatenation bailout: Cannot concat with ../node_modules/redux-immutable-state-invariant/dist/index.js (<- Module is not an ECMAScript module)
     ModuleConcatenation bailout: Cannot concat with ../node_modules/redux/es/redux.js (<- Module is referenced from these modules with unsupported syntax: ../node_modules/redux-devtools-extension/index.js (referenced with cjs require))
     | ./src/index.js 104 bytes [built]
     |     ModuleConcatenation bailout: Module is an entry point
     | ../dist/redux-starter-kit.esm.js 12.8 KiB [built]
     |     + 1 hidden module
    + 25 hidden modules

As ModuleConcatenationPlugin is needed for tree-shaking, this suggests that some of the dependencies using require() prevent tree-shaking from happening.

bug

Most helpful comment

Just filed https://github.com/immerjs/immer/issues/500 to see if they can maybe improve Immer's behavior by adding sideEffects: false and fixing the process reference or something.

All 15 comments

Tagging #55 as a link.

The issue isn't _immediately_ critical, I guess, but it'd be nice if we can figure it out.

Related to this, I've been trying to conditionally import redux-immutable-state-invariant in dev only so that it doesn't get pulled in to a production app build. I _think_ I've got it working.

So I went through these module concat errors one-by-one. Here is what I found:

  • __immer__ (Module uses injected variables (process)): This is because the immer source code has process.env checks. Apparently this will fix itself with Webpack 5, which can concatenate (and hence tree-shake) in the presence of process.env.

  • __redux-devtools-extension__ (Module is not an ECMAScript module): There is simply no module version of this. I guess someone would need to contribute the necessary build system changes (they use Webpack and on top of that Gulp).

  • __redux-immutable-state-invariant__ (Module is not an ECMAScript module): Same, no module version. Their current build system is to just process all source files with Babel CLI and output the resulting CommonJS module files.

  • __redux__ (Module is referenced from these modules with unsupported syntax: ../node_modules/redux-devtools-extension/index.js (referenced with cjs require): Follows from the redux-devtools-extension problem. As that's CommonJS, it pulls in all of Redux. The extension (and most apps) will need all of Redux anyway, though, so maybe not much of an issue.

I just experimented with this a little bit, and I stumbled at redux-devtools-extension: This makes up 6.7kb of the 24-kb-build, and in most cases is absolutely unneccessary, as we could be using window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ instead.

I guess there are still use-cases for the module (electron apps?), but including it by default takes up waaaay too much space IMHO.

@markerikson what do you think - would removing that and using the extension-provided global variable instead be an option?
Alternatively, we could include the module in the development build and the global in the production build?

That doesn't sound right. Bundlephobia says it's like 600 bytes:

https://bundlephobia.com/[email protected]

Ditto for the index file:

https://unpkg.com/browse/[email protected]/index.js

How are you seeing 6.7K?

And here's the Bundlephobia report for RSK 1.0.1:

https://bundlephobia.com/[email protected]

11.5K Immer, 6.65K Redux, 5K self.

In building the project posted by @denisw above twice, once with redux-devtools-extension and once without.

Here are the results:
-rw-r--r-- 1 weber weber 24653 Nov 4 06:47 main.js_1.0.1.js
-rw-r--r-- 1 weber weber 17840 Nov 3 19:09 main.js_without_devtools.js

(second build based on this branch: https://github.com/phryneas/redux-starter-kit/tree/tree-shaking )

Huh. That seems... odd.

We'll have to dig into this further.

Okay, a little experimentation later:
The example above uses only createAction, which does not reference redux itself.
So redux could be tree-shaken (the 6.7kb improvement I saw above). But composeWithDevTools is imported from redux-devtools-extension, which is not marked as side-effect-free. So there could be something happening there with redux - because redux is a dependency of redux-devtools-extension. Thus, webpack does not reduce redux from the bundle.

Ah, interesting.

Now, _realistically_, I'd expect this to be a moot point because most apps would be using configureStore, which does reference createStore from Redux.

Still, it'd be interesting to figure out what things _could_ and _should_ be tree-shaken, both from RSK and our deps.

Yup, but it also depends on how much redux could be tree-shaken again.
With redux-devtools-extension in the loop, webpack has to assume that everything in redux is being used. Without it, the code of RSK can be analyzed and just the methods of redux actually being used by RSK can be included.

Another point here is immer - it's being included 100% even if it isn't called at all (when just using createAction). Not sure why though.

Just filed https://github.com/immerjs/immer/issues/500 to see if they can maybe improve Immer's behavior by adding sideEffects: false and fixing the process reference or something.

Michel Weststrate is doing some work on improving Immer's tree-shaking, and just tweeted that the Rollup --preserveModules flag seems to be a minimum requirement to get any actual improvement:

https://twitter.com/mweststrate/status/1228056670447730688

And he also dropped this neat-looking tool:

https://github.com/mweststrate/import-size

This will be resolved once 1.3 comes out, so closing.

Was this page helpful?
0 / 5 - 0 ratings