2

tldr; what is preventing React from being dead-code eliminated in my project? It is not being used, apart in an unused utility in a library I am consuming.


I recently created a little library for retrying failing dynamic imports. I made a non-react-dependant module and another one that just handled the react specifics. This was to ensure a consumer would not pull in React if not using the react utils. I then created an index module that pulled the two in and exported named constants

import _createDynamicImportWithRetry from "./dynamic-import-with-retry";
import _reactLazy from "./react-lazy";

export const reactLazyWithRetry = _reactLazy;
export const createDynamicImportWithRetry = _createDynamicImportWithRetry;

This is the main field in package.json.

I assumed from previous reading that when Vite consumes this library it should be able to gather that just importing the named export createDynamicImportWithRetry would only require pulling in "./dynamic-import-with-retry", not the other import. Unfortunately, that does not seem to be the case: I published a project that shows it not to be the case: the transitive React dependency is being bundled, no matter if it is unused.

I would have assumed that dead-code analysis during bundling for production would have pruned the unused imports of the tree. How can I ensure pruning of dead branches take place?

Reproduction:

mkdir tmp-proj && cd tmp-proj && mkdir src

echo '<script type="module" src="./src/index.js"></script>' > index.html

echo "import * as o from '@fatso83/retry-dynamic-import'
o.reactLazyWithRetry( () => Promise.resolve())" > src/index.js

npm i @fatso83/retry-dynamic-import; npm i vite

npx vite build 

vite v4.3.5 building for production...
✓ 13 modules transformed.
dist/index.html                0.08 kB │ gzip: 0.09 kB
dist/assets/index-4e07d3ed.js  9.27 kB │ gzip: 3.89 kB

The 9KB there is the problem. It should be closer to 500 bytes, but it pulls in all of the React production build :/

oligofren
  • 20,744
  • 16
  • 93
  • 180
  • 1
    The semantics of this are always quite awkward. Try changing the index module to use `export { default as createDynamicImportWithRetry } from "./dynamic-import-with-retry"` and `export { default as reactLazyWithRetry } from "./react-lazy";`. Nothing else, just those two lines. See https://github.com/rollup/rollup/pull/4867. Also related is the `moduleSideEffects` of rollup, which vite uses for prod builds. – adsy May 13 '23 at 16:35
  • `moduleSideEffects` just produced empty files in the repro no matter what I did and no matter the content :( Now looking at the exports. – oligofren May 13 '23 at 16:59
  • Tried directly replacing the contents of `node_modules/@fatso83/retry-dynamic-import/dist/index.js` with those two lines (even if this means one export is lost), but it still includes React in the resulting bundle. Only when removing the react export is it not included. – oligofren May 13 '23 at 17:05
  • If I understand correctly, you wrap your retry import logic in React for those that use your lib in their React projects right? Why don't you externalize React and let your users supply it instead? I see you are not doing any packaging in your `build:dist`, just TS compilation. You can use Vite to package it (and do the TS comp as well) and externalize React from your dist. – Dan Macak May 19 '23 at 15:20
  • Hi, Dan. Good point. I did actually try externalizing it in a later version, but I pulled those later versions (because of other, export/packaging related issues), so I just pointed to the published version (which bundled React). Seeing that this got me false positives, I updated the link to point to v1.1.1 (instead of 1.0.7). Still, if tree shaking _had worked_, the consumer should have been able to trim off that branch, since the export using React is unused. – oligofren May 22 '23 at 09:23

2 Answers2

2

You've got to tell whichever bundler you are using that react is supposed to be provided by the client of your lib, ie. that it shouldn't be bundled with your lib. This is called externalization.

To get a general idea how this is done in both package.json and in your bundler, you can check my previous answer using Rollup. If you are using Vite, the externalization is even simpler to configure.

Dan Macak
  • 16,109
  • 3
  • 26
  • 43
  • Actually, I already externalized the library in a later version, so the lib itself does no longer bundle React. I updated the link now to point to the version built by Vite that externalizes React. Still, the same result applies: It is Vite in the _consumers_ of that library that fails to tree-shake the unused export. – oligofren May 22 '23 at 09:15
1

I posted this on the Vite discussion board and it turns out that Rollup cannot track assignments yet (per May 2023).

Rollup cannot track assignments (yet), so we deoptimize all properties of the right-hand side on assignments. Also, Rollup deoptimization does not know the execution flow, so it assumes that lazy might be a setter in require$$0.lazy=()=>{}, and that is the side effect. Probably not too easy to fix.

Since Vite relies on Rollup for bundling, there is nothing to do, apart from moving the export out of the root module and exporting it as its own specific export in package.json (which is what I did for version 2).

oligofren
  • 20,744
  • 16
  • 93
  • 180