nx: 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: Screen Shot 2022-04-06 at 2 00 07 PM

Expected Behavior

Create react app bundle: Screen Shot 2022-04-06 at 2 00 52 PM

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

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 15 (1 by maintainers)

Most upvoted comments

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.

Tree shaking does not work for me on a freshly installed nx workspace using Webpack and Babel.

Reproduce issue

  1. Create workspace by running npx create-nx-workspace@latest monorepo
  2. Select integrated repo > app boilerplate
  3. 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-fns functions 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-fns functions 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: image

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: image

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:

  1. Use deep imports like import set from "lodash/set"
  2. Use lodash-es as it exports ES modules instead of CJS
  3. 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.

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.

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

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

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.

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.

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.

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!