Single-file bundling is still preventing useful tree-shaking
See original GitHub issueVersion
react-router-dom@4.4.0-beta.7
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/Actual Behavior
Continuation of https://github.com/ReactTraining/react-router/issues/6464.
@mjackson thanks for trying to resolve this. Unfortunately, the same issue as originally described for 4.4.0-beta.6 persists in 4.4.0-beta.7.
I’ve updated the example repo to show this. The repro instructions are the same as before, and as before, all three history types are included in the bundle even though only BrowserRouter is imported.
I still think the proper fix is to stop bundling the libraries into single JS files, outlined in https://github.com/ReactTraining/react-router/issues/6464#issuecomment-437133689.
To summarize, {sideEffects: false} allows Webpack to prune unused files (rather than relying on a minifier to prune unused code paths, which doesn’t work very well). But it can only prune entire files, so it only works when code is split between multiple files. That’s why {sideEffects: false} is not useful if the library is bundled into a single file.
Issue Analytics
- State:
- Created 5 years ago
- Comments:5 (5 by maintainers)

Top Related StackOverflow Question
@mjackson I look directly at the output file (dist/main.js), but I realize this is a pain.
I made an
unmanglebranch of my repo that should help you explore. 🙂 I configured the minifier to avoid mangling variable names and set up Prettier to run on the output. I also committed the output file for reference. (Note that the bundle size info is no longer accurate since avoiding mangling obviously results in a larger bundle, but the output is accurate as far as which code gets eliminated.) Here’s the example I gave of code that should be tree-shaken but isn’t (createHashHistory, as explained in #6464).@StringEpsilon as you point out, this code is coming from the
historypackage. But, no, this shouldn’t be reported there, because tree-shaking works better if you import directly from that package. See this branch of my repo, where I’ve updated the entry file toimport {createBrowserHistory} from 'history'instead of importing anything from react-router. In this case, you can see thatcreateHashHistoryis successfully tree-shaken from the output, sohistoryitself is not the problem (although it may also benefit further from being split into multiple files).The problem is that when
react-routeris bundled into a single file, all of thehistoryimports are consolidated in that file. This forces webpack to mark all of the associatedhistoryexports as used (see https://github.com/ReactTraining/react-router/issues/6464#issuecomment-436797286) because it has no way of knowing at the time that some of those uses will be eliminated later by the minifier. Instead, if react-router consisted of several modules which each imported the needed bits ofhistory, then if/when webpack prunes one of those modules, its associatedhistoryexports would never be marked as used. For example, pruning react-router’sHashHistorymodule would let webpack mark thecreateHashHistoryhistory export as unused, allowing it to be eliminated (either via further module pruning ifhistoryalso consisted of multiple files, or at least via dead code elimination if not).I hope that makes sense – it’s hard to explain but I tried my best. 😅
@mjackson I understand your point of view, but the way I think about it is that tree-shaking (with
{sideEffects: false}) is an opt-in optimization feature provided by webpack that you can choose to use if you want. But it fundamentally works by statically analyzing the module import/export graph as opposed to the module contents, so if you don’t give it a “graph” (just a single file), there’s not much it can do.FWIW, even if it worked as intended, I feel like modifying source code to make Uglify happier (e.g. #6465, which is detrimental because
defaultPropsare useful in the React dev tools) is an uglier workaround than not rolling up the library. But I realize the subjectivity of that preference. 🙂In any case, thanks again for looking into this!
@mjackson once again, thanks so much for the time you’ve spent looking into this. ☺️
I totally agree that there’s no reason for this to block 4.4, and I apologize if I came across as suggesting that 4.4 shouldn’t be released until this was resolved. Congratulations on shipping. 🙂
That said, now that 4.4 is out, would you consider re-opening this to continue discussion for future versions?
Unfortunately, this isn’t the case. See https://github.com/mjackson/react-router-tree-shaking/pull/1, where I’ve replaced Webpack with Rollup and still found that tree-shaking doesn’t work. This doesn’t seem to be an issue of Rollup vs. Webpack, but rather a limitation in static module analysis + DCE when packages are pre-bundled into single files.
Thanks for suggesting. It’s a good idea, but this workaround prevents
LinkandNavLinkfrom ever being used (see https://github.com/mjackson/react-router-tree-shaking/pull/2 for details). Would you agree that this is a pretty big pitfall for typical apps?(⚠️ Below is a bit of a tangent, just responding to your point.)
That’s true, but I’m not sure it’s a fair comparison. Tree-shaking mostly benefits libraries which expose multiple, somewhat independent pieces of similar size which aren’t all likely to be used, and React is not such a library. Also, the advantages that React buys itself by pre-bundling are larger than those received by libraries which only use Rollup to put everything in one file.
To elaborate, the
reactpackage is small (4x smaller thanreact-router-dom) and most of it is used in a typical app, so there’s comparatively little to gain from tree-shaking. Most of the size is inreact-dom, in the form of a single, indivisible object (the renderer), so tree-shaking wouldn’t provide value there either. That’s why it hasn’t been a priority for the React team.There’s also the fact that React uses advanced compile-time tooling (e.g. GCC) to produce a maximally small/performant build, using optimizations that would be very slow or impossible to reproduce in users’ bundling setups. This necessitates shipping a single file. This is different from plain Rollup, which, when bundling libraries, roughly just concatenates the modules in the right order. i.e. React gets something from doing this that other libraries don’t.