Bundling packages into single files is preventing useful tree-shaking
See original GitHub issueVersion
react-router-dom@4.4.0-beta.6
Test Case
https://github.com/billyjanitsch/react-router-tree-shaking
Steps to reproduce
Clone the repo above, run npm run build, then observe the output in dist/main.js.
Expected Behavior
The biggest gains to be had from tree-shaking in react-router/history are probably removing whichever of browser/hash/memory histories are not used by the app. For example, if only BrowserRouter is imported from react-router-dom, one would expect the hash and memory routers, along with their associated histories, to be tree-shaken.
Actual Behavior
Neither of the unused histories are tree-shaken. You can verify this by searching for hashType in the build output, which only appears in createHashHistory and therefore should have been tree-shaken from the bundle. As a result, the output bundle is quite large: importing only BrowserRouter yields a 33.2 kB bundle, whereas importing the entire library yields a barely-larger 37.9 kB bundle.
I believe this happens because webpack is generally not good at tree-shaking unused parts of a module – it’s much better at tree-shaking entire unused modules (particularly when sideEffects: false has been set). For example, if createHashHistory lived in a separate module, webpack would know that the module import was unused and prune the entire module.
What was the motivation for bundling history, react-router, and react-router-dom into single files on npm? I understand why it’s more efficient to do this for CJS modules, but for ESM, it seems strictly better to leave the bundling to the bundler, since modern bundlers are capable of inlining ESM without adding glue code, with the advantage that they can be smarter about pruning entire modules.
(Another theory is that this re-export is the problem.)
Issue Analytics
- State:
- Created 5 years ago
- Reactions:2
- Comments:28 (26 by maintainers)

Top Related StackOverflow Question
@TrySound I made an example repo to demonstrate. Please compare the multiple-files branch to the single-file branch.
Both branches contain an identical index.js which has
import {foo} from './react-router'. This represents a top-level import from a package (similar toimport {BrowserRouter} from 'react-router'). There are no “path imports” in either branch.The difference is that the single-file branch’s
react-routerdirectory contains only anindex.jsfile which defines and exportsfooandbar. This represents the way the react-router ESM build is currently shipped (bundled into a single file).The multiple-files branch’s
react-routerdirectory instead containsfoo.jsandbar.jsand anindex.jswhich re-exportsfooandbar. This represents the way I think the react-router ESM build should be shipped (left as multiple files).Comparing the build output of the multiple-files branch to the single-files branch, you’ll notice that only the multiple-files branch was able to tree-shake
bar. This is because webpack handled the tree-shaking on the module level – it realized through static analysis that thebarimport was unused, so it pruned thebar.jsmodule entirely. (It only does this when{sideEffects: false}is set.) It can’t do this whenfooandbarare defined in the same file, and Uglify isn’t smart enough to tree-shakebarduring the DCE phase, sobaris leftover in the single-file branch.I hope this helps you understand what I mean by module-level tree-shaking. It does not affect how users consume or import from the package, and I am not recommending path imports.
I believe all our code is tree-shakable for now, except for
Routerwhich still has one static method. All other static methods/properties are now gone as of 9d278b4b110f38479725fe5a1104990c2f3e76c2.I’m going to push a new beta release today (beta 7) that should be fully tree-shakable. Please let me know if you see any issues, @billyjanitsch.
Closing for now.