nx
nx copied to clipboard
Production build tree shaking not working
Tree shaking does not work. I created a React app with both Nx and create-react-app, and imported a single lodash function.
Current Behavior
Nx bundle:

Expected Behavior
Create react app bundle:

Steps to Reproduce
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.scss';
import NxWelcome from './nx-welcome';
import { get } from 'lodash-es';
export function App() {
console.log(get({ foo: true }, 'foo'));
return (
<>
<NxWelcome title="lodash" />
<div />
</>
);
}
export default App;
Environment
> NX Report complete - copy this into the issue template
Node : 16.13.2
OS : darwin x64
npm : 8.1.2
nx : 13.9.7
@nrwl/angular : Not Found
@nrwl/cypress : 13.9.7
@nrwl/detox : Not Found
@nrwl/devkit : 13.9.7
@nrwl/eslint-plugin-nx : 13.9.7
@nrwl/express : 13.9.7
@nrwl/jest : 13.9.7
@nrwl/js : 13.9.7
@nrwl/linter : 13.9.7
@nrwl/nest : Not Found
@nrwl/next : Not Found
@nrwl/node : 13.9.7
@nrwl/nx-cloud : Not Found
@nrwl/nx-plugin : Not Found
@nrwl/react : 13.9.7
@nrwl/react-native : Not Found
@nrwl/schematics : Not Found
@nrwl/storybook : 13.9.7
@nrwl/web : 13.9.7
@nrwl/workspace : 13.9.7
typescript : 4.4.4
rxjs : 7.5.4
---------------------------------------
Community plugins:
@nx-tools/nx-docker: 2.3.0
Tree-shaking lodash depends on the build process. In this case, you need to import it slightly differently:
import get from 'lodash/get';
This is the most consistent way I've found to ensure tree-shaking for lodash no matter the build process. I'm not sure what create-react-app has configured that might be doing this for you, but there are a few different babel and webpack plugins that will help with tree-shaking lodash.
@philipjfulcher Deep imports are not the same thing as tree shaking. The babel plugin you're most likely referring to is the one that rewrites the imports to deep imports, but that is not the same as tree shaking, and it is not present in create react app. Lodash is also not the only thing a user would want optimized so a plugin that rewrites Lodash imports doesn't really solve the issue with tree shaking here.
It is a very complicated thing, and I was confused when learning about this, but quoting the webpack docs:
import { cube } from './math.js';
Note that we did not import the square method from the src/math.js module. That function is what's known as "dead code", meaning an unused export that should be dropped.
In my opinion, this is pretty unambiguous that unused exports should optimize things without resorting to writing deep imports.
One thing on Nx's side that would be useful to look into, which may be related to this issue:
Ensure no compilers transform your ES2015 module syntax into CommonJS modules (this is the default behavior of the popular Babel preset @babel/preset-env - see the documentation for more details).
https://webpack.js.org/guides/tree-shaking/
https://webpack.js.org/configuration/optimization/#optimizationsideeffects
By logging out the webpack configs that nx is using, I see it has optimization: { sideEffects: false }. Is there a reason this is turned off?
Additionally, Nx has my tsconfig.json set to "target": "es2015",.
I fully expect both of these issues to prevent tree shaking.
After I fixed both of these issues, I can confirm Nx properly tree shakes 👍 . All I did was:
optimization: {
sideEffects: true,
},
(see https://nx.dev/guides/customize-webpack)
and in tsconfig.base.json I set "target": "esnext", :)
In turn these changes break Jest, to fix this I added
jest.preset.js:
transformIgnorePatterns: ['<rootDir>/node_modules/(?!lodash-es)'],
babel.config.json:
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic"
}
]
],
"plugins": []
}
And now jest works again
After making these changes and switching branches nx build also broke with this:
npx nx build app
/Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/utilities/project-graph-utils.js:9
return project.data && project.data.targets && project.data.targets[target];
^
TypeError: Cannot read properties of undefined (reading 'data')
at projectHasTarget (/Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/utilities/project-graph-utils.js:9:20)
at addTasksForProjectDependencyConfig (/Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/tasks-runner/run-command.js:220:64)
at addTasksForProjectTarget (/Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/tasks-runner/run-command.js:157:13)
at createTasksForProjectToRun (/Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/tasks-runner/run-command.js:133:9)
at /Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/tasks-runner/run-command.js:58:23
at Generator.next (<anonymous>)
at /Users/jribakoff/groundwater/node_modules/tslib/tslib.js:117:75
at new Promise (<anonymous>)
at __awaiter (/Users/jribakoff/groundwater/node_modules/tslib/tslib.js:113:16)
at runCommand (/Users/jribakoff/groundwater/node_modules/@nrwl/workspace/src/tasks-runner/run-command.js:53:34)
After rm -fr node_modules/.cache this error was also resolved, and is tracked in a separate bug I reported here https://github.com/nrwl/nx/issues/9662
A full solution that worked for me. Nx version: 13.10.2
apps/<app>/project.json
{
"targets": {
"build": {
"options": {
"webpackConfig": "apps/<app>/custom-webpack.config.js"
}
}
}
}
apps/<app>/custom-webpack.config.js
const { merge } = require('webpack-merge')
const getWebpackConfig = require('@nrwl/react/plugins/webpack')
module.exports = (config, _context) => {
return merge(getWebpackConfig(config), {
optimization: {
sideEffects: true,
},
})
}
package.json
{
"sideEffects": false
}
If your app has side effects, for example, global styles, you need to specify an array of effectful files in the package.json:
{
"sideEffects": ["apps/<app>/src/styles/index.css"]
}
I didn't have problems with Jest or "target": "es2015" in the tsconfig.base.json file as described in the comment above.
I've encountered the same problem with a plain react application using the @nrwl/react generator. I've set up a reproducible sample project here: https://github.com/rudfoss/nx-bundling-issue It is a clean nx workspace project with a vanilla generated react application (using SWC compiler) so if it should work I would expect it to here.
The problem occurs when trying to import from react-icons which does not tree-shake correctly. I tried the fix suggested by @vlad-khitev-axon above, but it does not seem to work.
Any tips on how to resolve this?
I've moved from swc to babel and that seems to have resolved the tree shaking problem. Still seems strange that it does not work with swc, but at least we can bundle it now :)
A full solution that worked for me. Nx version: 13.10.2
apps/<app>/project.json{ "targets": { "build": { "options": { "webpackConfig": "apps/<app>/custom-webpack.config.js" } } } }
apps/<app>/custom-webpack.config.jsconst { merge } = require('webpack-merge') const getWebpackConfig = require('@nrwl/react/plugins/webpack') module.exports = (config, _context) => { return merge(getWebpackConfig(config), { optimization: { sideEffects: true, }, }) }
package.json{ "sideEffects": false }If your app has side effects, for example, global styles, you need to specify an array of effectful files in the
package.json:{ "sideEffects": ["apps/<app>/src/styles/index.css"] }I didn't have problems with Jest or
"target": "es2015"in thetsconfig.base.jsonfile as described in the comment above.
Actually you only need to add the "sideEffects": false to the package.json
Also thanks a lot for the response, it helped me a lot!
Actually you only need to add the "sideEffects": false to the package.json
That was not my experience. In my original issue report, I found that Nx is passing optimization: { sideEffects: false } to webpack, which explicitly turns off tree shaking regardless of your package.json contents. The library that was not being tree shaken in my original issue (lodash-es) already has it's own package.json where it specifies the required setting to facilitate optimal tree shaking, however Nx was turning off tree shaking in Webpack globally, so that it doesn't happen at all for any library or any code in the project.
Actually you only need to add the "sideEffects": false to the package.json
That was not my experience. In my original issue report, I found that Nx is passing
optimization: { sideEffects: false }to webpack, which explicitly turns off tree shaking regardless of your package.json contents. The library that was not being tree shaken in my original issue (lodash-es) already has it's ownpackage.jsonwhere it specifies the required setting to facilitate optimal tree shaking, however Nx was turning off tree shaking in Webpack globally, so that it doesn't happen at all for any library or any code in the project.
Got it! In my case simply adding to package.json made me get the desired result, I did as you said and I got no difference from just adding it to the package.json. I think it's because webpack already takes the package.json sideeffect into consideration
https://webpack.js.org/guides/tree-shaking/
But either way this was a great find! Thanks a lot
Yeah, depending on the Nx generator that created your webpack config, or your version of Nx, you may or may not be experiencing the issue with it being turned off in webpack. Separately, you may find you also need package.json changes, it depends :)
const { merge } = require('webpack-merge') const getWebpackConfig = require('@nrwl/react/plugins/webpack') module.exports = (config, _context) => { return merge(getWebpackConfig(config), { optimization: { sideEffects: true, }, }) }
@LeonardoGobbiLopez Thank you! 🎉 the webpack optimization.sideEffects: true change was exactly what I was missing.
Tree shaking does not work for me on a freshly installed nx workspace using Webpack and Babel.
Reproduce issue
- Create workspace by running
npx create-nx-workspace@latest monorepo - Select integrated repo > app boilerplate
- Create React application by running
npx nx generate @nrwl/react:application shell --bundler=webpack --compiler=babel --unitTestRunner=jest --e2eTestRunner=none
Import a library which exports a barrel file. I'll use date-fns as an example. Open the App component of the newly generated React application and import a function from date-fns:
import { getDate } from 'date-fns';
console.log(getDate); // use it somewhere
export function App() {
return null;
}
Now let's look at both the development and production builds in dist/apps/shell/main[.id].js build and see what happened.
Development build
- The build is huge (~1.8MB vendor)
- All
date-fnsfunctions are imported. Notice the import in the main bundle:
// EXTERNAL MODULE: ../../node_modules/date-fns/index.js
var date_fns = __webpack_require__(3753);
Production build
- The build is huge (~900KiB)
- All
date-fnsfunctions are imported - Code doesn't seem to be minified (i.e. comments, original function names, identations and newlines are still present)
You should see something like:

The fix
I did some extensive digging and managed to solve the issue by extending/overriding the default webpack config:
// Fix 1: resolves tree-shaking issue
// The default in NX is [ 'browser', 'main', 'module' ]. Thus, 'main' had preference over 'module' when Webpack reads the `package.json` files, which is not what we want. Module should become before main - the order matters!
// See https://webpack.js.org/configuration/resolve/#resolvemainfields
config.resolve.mainFields = ['browser', 'module', 'main'];
// Fix 2: resolves minification issue by adding Terser. Terser is also capable of eliminating dead code.
// TerserJS is the Webpack 5 default minifier but we have to specify it explicitly as soon as we include more minifiers
config.optimization.minimizer.unshift(new TerserJSPlugin());
You will now see that the module is concatenated and only the required date functions are imported:

Full code of webpack.config.js:
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
const TerserJSPlugin = require('terser-webpack-plugin');
module.exports = composePlugins(withNx(), withReact(), (config) => {
config.resolve.mainFields = ['browser', 'module', 'main'];
config.optimization.minimizer.unshift(new TerserJSPlugin());
return config;
});
After altering the Webpack configuration, the bundle size went from 900kb to a mere 140kb 🥳
Tree shaking lodash
Tree shaking lodash by using named imports (i.e. import { set } from "lodash") still didn't work for me as lodash exports its barrel file as CJS. There are three ways to fix this that I know of:
- Use deep imports like
import set from "lodash/set" - Use
lodash-esas it exports ES modules instead of CJS - Import lodash functions from your own barrel file, which exports lodash functions (example here)
Using Vite
I can confirm that tree shaking and minification does work out-of-the-box on a fresh workspace using Vite + SCW. For some this might be the way to go. In my case, however, I need Webpack as I want to experiment with Module Federation.
Thanks @Jasonkoolman - that did the trick for me!
note to anyone using webpack-merge: make sure you specify custom merging rules for the mainFields array (or simply set it directly) since
merge(config, { resolve: { mainFields: ['browser', 'module', 'main'] } })
will yield
config.resolve.mainFields === ['browser', 'main', 'module', 'browser', 'module', 'main'];
I've found solution for angular apps: "app": { "architect": { "configurations": { "production": { "optimization": true, "buildOptimizer": true, }}}}
Just add "optimization": true and "buildOptimizer": true in your angular.json or project.json
This issue has been closed for more than 30 days. If this issue is still occuring, please open a new issue with more recent context.