Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production build tree shaking not working #9717

Closed
joshribakoff-sm opened this issue Apr 6, 2022 · 15 comments · Fixed by #17599
Closed

Production build tree shaking not working #9717

joshribakoff-sm opened this issue Apr 6, 2022 · 15 comments · Fixed by #17599
Labels
outdated scope: react Issues related to React support for Nx type: bug

Comments

@joshribakoff-sm
Copy link

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
@AgentEnder AgentEnder added the scope: react Issues related to React support for Nx label Apr 6, 2022
@philipjfulcher
Copy link
Collaborator

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.

@joshribakoff-sm
Copy link
Author

@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/

@joshribakoff-sm
Copy link
Author

joshribakoff-sm commented Apr 7, 2022

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 #9662

@vlad-khitev-axon
Copy link

vlad-khitev-axon commented Apr 15, 2022

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.

@rudfoss
Copy link

rudfoss commented May 27, 2022

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?

@rudfoss
Copy link

rudfoss commented May 29, 2022

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

@LeonardoGobbiLopez
Copy link

LeonardoGobbiLopez commented Jun 22, 2022

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!

@joshribakoff-sm
Copy link
Author

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.

@LeonardoGobbiLopez
Copy link

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

@joshribakoff-sm
Copy link
Author

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

@wobo-nate
Copy link

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.

@Jasonkoolman
Copy link

Jasonkoolman commented Feb 9, 2023

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.

@janeklb
Copy link
Contributor

janeklb commented Apr 28, 2023

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'];

@nemanjazajk
Copy link

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

@github-actions
Copy link

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.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated scope: react Issues related to React support for Nx type: bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants