question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Bundling packages into single files is preventing useful tree-shaking

See original GitHub issue

Version

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.)

/cc @TrySound @mjackson

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:2
  • Comments:28 (26 by maintainers)

github_iconTop GitHub Comments

3reactions
billyjanitschcommented, Nov 9, 2018

@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 to import {BrowserRouter} from 'react-router'). There are no “path imports” in either branch.

The difference is that the single-file branch’s react-router directory contains only an index.js file which defines and exports foo and bar. This represents the way the react-router ESM build is currently shipped (bundled into a single file).

The multiple-files branch’s react-router directory instead contains foo.js and bar.js and an index.js which re-exports foo and bar. 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 the bar import was unused, so it pruned the bar.js module entirely. (It only does this when {sideEffects: false} is set.) It can’t do this when foo and bar are defined in the same file, and Uglify isn’t smart enough to tree-shake bar during the DCE phase, so bar is 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.

1reaction
mjacksoncommented, Feb 22, 2019

I believe all our code is tree-shakable for now, except for Router which 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Single-file bundling is still preventing useful tree-shaking #6608
I still think the proper fix is to stop bundling the libraries into single JS files, outlined in #6464 (comment). To summarize, {sideEffects: ......
Read more >
Reduce JavaScript payloads with tree shaking - web.dev
Now that you've identified an opportunity for tree shaking to be useful, how is it actually done? Keeping Babel from transpiling ES6 modules...
Read more >
Tree shaking and code splitting in webpack - LogRocket Blog
Here, we'll explain tree shaking and code splitting in webpack and discuss how to combine them for the most optimal bundle possible.
Read more >
Tree-Shaking: A Reference Guide - Smashing Magazine
In this article, we dive deeper on how exactly it works and how specs and practice intertwine to make bundles leaner and more...
Read more >
Tree Shaking - webpack
webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found