next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Tree shaking doesn't work with Typescript barrel files

Open majelbstoat opened this issue 5 years ago • 48 comments

Bug report

I originally raised this as a discussion, but now I think it's a bug.

Describe the bug

When using a barrel file to re-export components from a single location, tree-shaking does not function correctly.

To Reproduce

I'm using Next 9.3.6 and I've arranged my components like:

  components/
    Header/
      Header.tsx
    Sidebar/
      Sidebar.tsx
    index.ts

Each component file exports a single component, like this:

export { Header }

index.ts is a barrel file that re-exports from each individual component file:

  export * from './Header/Header.tsx'
  export * from './Sidebar/Sidebar.tsx'
  // ...

I then use a couple of components in _app.tsx like:

import { Header, Sidebar } from "../components"

There's about 100 components defined, and only a couple are used in _app.tsx. But when I analyze the bundle I have a very large chunk, shared by all pages, and it contains all my components, resulting in an inflated app page size:

Screenshot 2020-05-06 09 35 32

I use a similar import strategy within pages, and every one of them appears to contain every component.

my tsconfig.json is:

  "compilerOptions": {
    "allowJs": true,
    "baseUrl": ".",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "es5"
  }

and I'm using next's native support for the baseUrl field. I haven't changed the module or the target.

When I change the _app.tsx imports to:

import { Header } from "../components/Header/Header"
import { Sidebar } from "../components/Sidebar/Sidebar"

the common bundle and app page size drops dramatically, as I would expect it to:

Screenshot 2020-05-06 09 34 21

Expected behavior

The app page size should be the same using both import strategies.

System information

  • OS: [e.g. macOS]
  • Version of Typescript: [e.g. 3.8.3]
  • Version of Next.js: [e.g. 9.3.6]

majelbstoat avatar May 06 '20 13:05 majelbstoat

Feel free to investigate and solve

timneutkens avatar May 25 '20 12:05 timneutkens

https://github.com/vercel/next.js/discussions/13490

Same happening with jsx also. version: 9.4.2

rahul3103 avatar May 28 '20 10:05 rahul3103

Has anyone been able to solve this? In one of our projects we seem to be getting no tree shaking at all for any TS application code, only for third party library code.

Alternatively, could any of the maintainers provide some hints as to how one would best go about investigating and solving such an issue?

EDIT: For posterity, the problem in our case was that the bundle analyzer's output is harder to interpret correctly than it appears, which we failed to do initially. It seems that tree shaking was not the problem.

seeekr avatar Sep 10 '20 15:09 seeekr

Has anyone been able to solve this? In one of our projects we seem to be getting no tree shaking at all for any TS application code, only for third party library code.

Alternatively, could any of the maintainers provide some hints as to how one would best go about investigating and solving such an issue?

@seeekr We actually ended up just removing our barrel files.

I did a simple experiment with Webpack and a couple of JavaScript modules + a barrel file (no Next, or Typescript) and the same behaviour was present with limited options to enable tree shaking to occur.

If you have no side effects in your code then you may be able to look into the Webpack side effects setting to allow this to work for you, but as soon as you're importing a module classed as a side effect, you may start to notice issues. For us, our polyfill imports stopped working along with a whole host of other stuff.

I'm not sure this is something Next can/should solve though, as it seems to be inherent to Webpack

stevethatcodes avatar Sep 10 '20 16:09 stevethatcodes

@stevethatcodes @majelbstoat Tree shaking seems to work for me as long as I specify side effects in package.json:

"sideEffects": [
    "./src/some-side-effectful-file.js"
  ]

This is according to the Webpack docs linked above. I think you would just have to determine where your polyfills and other side effects are happening and add those files to the side effects list.

VWSCoronaDashboard8 avatar Sep 22 '20 14:09 VWSCoronaDashboard8

@VWSCoronaDashboard8 Yeah, that's an approach we tried, though still had issues with the polyfills not loading. In fairness, we took the approach to remove barrel files as the path of least resistance, as they didn't add much value and we didn't need to do anything else to our Next/Webpack setup to bring back tree-shaking.

stevethatcodes avatar Sep 22 '20 21:09 stevethatcodes

With webpack 5, tree shaking seems to work. https://webpack.js.org/blog/2020-10-10-webpack-5-release/#major-changes-optimization

sphilee avatar Nov 08 '20 14:11 sphilee

@sphilee I don't actually think it works / resolves the issue, I've installed Webpack 5 on my current Next.js projects but it doesn't have any positive impact on the bundle size, at least for barrel files structured as the issue example by @majelbstoat.

RobbyUitbeijerse avatar Dec 14 '20 13:12 RobbyUitbeijerse

@RobbyUitbeijerse @sphilee I have concluded the same. Webpack 5 didn't solve this issue for me.

VWSCoronaDashboard8 avatar Dec 14 '20 14:12 VWSCoronaDashboard8

Based on the following issue https://github.com/webpack/webpack/issues/11821 , Webpack 5 should actually eliminate dead code with the minimize option being enabled (which is the case by default for next build). Let's assume that it does (I haven't actually verified it myself) -

What I'm concerned about is that it might actually work for the shared bundle which is loaded for all pages, but doesn't actually consider the page by page chunks, meaning that while dead code is actually eliminated from the shared bundle - you are still left with a single bundle containing everything instead for smaller chunks per page that only contain what you need for that page. Quite unsure whenever it's possible to actually resolve that or that changing the imports is the only way to go.

RobbyUitbeijerse avatar Dec 15 '20 09:12 RobbyUitbeijerse

@timneutkens Webpack is not my strong suit, but do you have any clue if my comment above makes any sense in terms of what we are seeing?

RobbyUitbeijerse avatar Jan 14 '21 09:01 RobbyUitbeijerse

I had similar issues in the past when using export * from 'xxx';. Have you tried explicitly re-exporting your named and default exports in the barrel files?

In the past we also had issues with babel when it came to exporting types. Since then we started exporting types explicitly using export type.

// index.ts
export {
  default as Header,
  HeaderX,
  HeaderY,
} from './Header/Header.tsx';

export type {
  Props as HeaderProps,
  AdditionalHeaderTypeX,
} from './Header/Header.tsx';

export {
  default as Sidebar,
  SidebarX,
  SidebarY,
} from './Sidebar/Sidebar.tsx';

export type {
  Props as SidebarProps,
  AdditionalSidebarTypeX,
} from './Sidebar/Sidebar.tsx';

kelvinlouis avatar Jan 18 '21 15:01 kelvinlouis

I actually started doing the same recently:

image

But it turns out, no luck:

image

What I did find tho, that when I put all of the components in an external library and build that library using Rollup while preserving the modules, tree shaking of the same components seems to work properly, meaning that it should be possible to get it all working one way or the other.

[edit] Same result with webpack 5

RobbyUitbeijerse avatar Feb 03 '21 12:02 RobbyUitbeijerse

I actually started doing the same recently:

image

Same experience here. I was pulling my hair out trying to figure this out. I have a monorepo, and in the index.ts of each package, I was re-exporting my components. Not only did this break tree shaking: it also made yarn next dev take like a minute longer to load. Changing my imports directly to the root file of the components solved this. Wish I'd known this sooner – I have a lot of imports to change.

I can make a repro in a monorepo this weekend.

I'm using Next 10.1.4-canary.2 and webpack 4, for context.

nandorojo avatar Apr 08 '21 23:04 nandorojo

This has been open for over a year. Has anyone found a solution yet? Using a barrel file results in my bundle reaching 12mb whereas without I'm less than 1mb

JoeyFenny avatar Jun 04 '21 15:06 JoeyFenny

@JoeyFenny I managed to get this working by setting "sideEffects": false in the package.json of the package with the barrel export. You may also want to check this comment: https://github.com/vercel/next.js/issues/12557#issuecomment-696749484

R-Bower avatar Jun 05 '21 01:06 R-Bower

I am running into simular problems in a non next.js project. Upgrading from webpack 4 to webpack 5 did not fix it, sideEffects: "false" in the package.json does enable tree shaking but the barrel pattern still fails.

I think this is a webpack problem, maybe we should move the dicussion there? Though it would really help if we got a minor example repository that shows the problem.

Important edit: It seems I was wrong that tree shaking wasn't fully working in my webpack project because of the barrel pattern. The barrel pattern (vs direct importing) made a lot more code be processed which increased the chance of tree shaking not being applied because of sideEffects within that code.

I don't know OP's exact case or next.js's handling when it comes to tree shaking vs webpack 5 but I did came accross a blog post that goes more in-depth on tree shaking that I would recommend reading: https://dev.to/livechat/tree-shaking-for-javascript-library-authors-4lb0

In the end I had to add /*#__PURE__*/ to some function calls to let tree shaking be applied correctly.

Again, I don't know if OP has the same issue, or there is some config issue or if it is next.js that is failing, but what I did found out is that webpack 5 does support the barrel pattern.

jrmyio avatar Jun 05 '21 17:06 jrmyio

settings sideEffects to false and adding /*#__PURE__*/ to the function call did not work either. But I agree that this discussion should perhaps be moved to webpack

JoeyFenny avatar Jun 09 '21 09:06 JoeyFenny

We dont have any news or ideas about this? I am really struggling to fix this since its having a big impact in our page load.

b2rsp avatar Jun 14 '21 15:06 b2rsp

For tree-shaking to work with a barrel file you need

  • flag the file and/or the child files as side effect free ("sideEffects": false in package.json)
  • build in production mode webpack 4 or 5
  • not bundle the library (it must be in separate files for sideEffects to work)
  • Alternatively to side effect free flagging you can use /*#__PURE__*/ to carefully flag statements that might be considered as having side effects. If you do that, you must minimize to file to see the effect. This is very tricky to get right

sokra avatar Jun 21 '21 15:06 sokra

not bundle the library (it must be in separate files for sideEffects to work

@sokra I want to have barrel files as the main file for each of my monorepo's packages. If I am adding these monorepo packages to next-transpile-modules, would that constitute "bundling" the library?

nandorojo avatar Sep 02 '21 20:09 nandorojo

I've got similar situation to @nandorojo but I managed to get rid of next-transpile-modules and replace it with experimental.externalDir and some tscconfig.json paths configuration.

Setting sideEffects false seemed to help but I still need to investigate if I'm not getting too much into the shared code. it's definitely not all of it per each page but seems like 90% code is shared (which is possible I guess).

pawelphilipczyk avatar Oct 24 '21 06:10 pawelphilipczyk

not bundle the library (it must be in separate files for sideEffects to work

@sokra I want to have barrel files as the main file for each of my monorepo's packages. If I am adding these monorepo packages to next-transpile-modules, would that constitute "bundling" the library?

no. I was talking about double bundling, so using a pre-bundled library.

sokra avatar Nov 24 '21 08:11 sokra

Apologize if this has already been gone over, but experiencing this with my react-component library, which uses a barrel file. Using rollupjs, and importing this component library into my NextJS app -- and its not tree shaking. Even with sideEffects set properly. ...... anyone using their react-component library effectively in a NextJS app that tree shakes?

dsacramone avatar Nov 29 '21 05:11 dsacramone

I was using rollup to bundle a library as well, also with a barrel file. It wasn't tree shaking, it also put server-side code into client bundle, like the modules i m using inside getStaticProps etc..

If u do not bundle the library, and literally just copy the source code into your package, and use next-transpile modules with it, it does tree-shake.

Currently, i couldnt find a better solution, all other options are creating page files with larger size.

const withTM = require("next-transpile-modules")(["your-library"]);

const config = {
  // ... your config
};

module.exports = withTM(config);

feluna avatar Nov 29 '21 11:11 feluna

Disabling side effects for my imported barrel libs works for me.

I don't use side effects, seems like a lib thing to rely on that. Unless i'm sorely mistaken about side effects.

 webpack: (config, { dev }) => {
        config.module.rules = [
            ...config.module.rules,
            // ensure our libs barrel files don't constitute imports
            {
                test: /libs\/.*src\/index.ts/i,
                sideEffects: false,
            },
        ]

yuchant avatar Dec 15 '21 04:12 yuchant

Disabling side effects for my imported barrel libs works for me.

I don't use side effects, seems like a lib thing to rely on that. Unless i'm sorely mistaken about side effects.

 webpack: (config, { dev }) => {
        config.module.rules = [
            ...config.module.rules,
            // ensure our libs barrel files don't constitute imports
            {
                test: /libs\/.*src\/index.ts/i,
                sideEffects: false,
            },
        ]

This just trimmed 200kb off my bundled, thanks a million.

alessandrojcm avatar Jan 07 '22 14:01 alessandrojcm

^ yessss, trimmed 200kb from my bundled too, thanks mates

mfv-brian avatar Apr 20 '22 02:04 mfv-brian

is that webpack config put in nextjs... because if you have a node_modules thats bundles and doesnt treeshake how does that pointer to libs/src/index.ts help?

daphnesmit avatar Apr 26 '22 07:04 daphnesmit

is that webpack config put in nextjs... because if you have a node_modules thats bundles and doesnt treeshake how does that pointer to libs/src/index.ts help?

This issue isn't about tree shaking node_modules, it's about tree shaking barrel imports from your own code.

yuchant avatar Apr 26 '22 15:04 yuchant

Tree-shaking barrel files could lead to possibly different behavior in development and production. Since imported files can have side effects that would be introduced in development but not in the tree-shaken version.

Since side effects are not identifiable at the moment, maybe introduce an easier way to mark the file as side-effect-free?

For example in a barrel file:

// @ignore-side-effects

Or in each of the files:

// @side-effects: false

etc.

Or create a Codemod for transforming barrel imports.

aboqasem avatar May 31 '22 03:05 aboqasem

Happy 2 years to this issue 🥳

I'm still facing this issue in my project when using barrel files coupled with the tsconfig path option. I have a folder components, which has subfolders like sections, and each of these folders have an index.ts file presented like this

import SectionCarousel from './SectionCarousel'
import SectionDemo from './SectionDemo'
import SectionInfiniteCarousel from './SectionInfiniteCarousel'
import SectionIntro from './SectionIntro'

export {
	SectionCarousel,
	SectionDemo,
	SectionInfiniteCarousel,
	SectionIntro,
}

And then I'm using my components like this :

import { SectionCarousel, SectionDemo, SectionInfiniteCarousel, SectionIntro } from '@/components/sections'

I've been trying everything for 2 weeks now and I can't find a solution without rewriting all of my imports in every components and pages, which I obviously can't do right now. It's a ticking bomb, since every new line of code I write, is being sent to every pages

Slowl avatar Jun 03 '22 14:06 Slowl

Disabling side effects for my imported barrel libs works for me.

I don't use side effects, seems like a lib thing to rely on that. Unless i'm sorely mistaken about side effects.

 webpack: (config, { dev }) => {
        config.module.rules = [
            ...config.module.rules,
            // ensure our libs barrel files don't constitute imports
            {
                test: /libs\/.*src\/index.ts/i,
                sideEffects: false,
            },
        ]

This does works. But let me elaborate a bit for anyone who might not get it at first glance like myself.

For example this is your project structure.

├── ...
├── src
│   ├── pages
│          ├─ index.tsx
│          └─ somepage.tsx
│   └── components
│          ├─ index.ts
│          ├─ ComponentA.tsx
│          └─ ComponentB.tsx
│   └── ...
├── package.json
└── next.config.js

What you want to do is in your next.config.js add this webpack config. Inside test list you can add regex path to your barrel file.

This will tell webpack that this file is side-effect free, please go ahead and tree-shake this thing.

      webpack(c) {
        c.module.rules.push({
          test: [
            /src\/components\/index.ts/i,
          ],
          sideEffects: false,
        });

        return c;
      },

That it's, hope this helps. 😁

pipech avatar Jul 27 '22 15:07 pipech

@pipech This sounds promising, do I have to add the path of each index.ts file? or would this work /src\/.*index.ts/i ?

Sodj avatar Aug 01 '22 17:08 Sodj

Just chiming in to say that adding "sideEffects": false to my app's package.json solved this issue for me

(for [email protected].*)

joe-bell avatar Aug 03 '22 07:08 joe-bell

Adding sideEffects: false did improve build final size, but the server side render on localhost is still super slow.

Also, for some reason, the prototypes I added to the Array object stopped working

Sodj avatar Aug 03 '22 10:08 Sodj

I did try adding sideEffects: false to package.json but some how it broke the ui on my apps, not sure what happen there.

@Sodj I think you should play around with it a bit, but for my case it should only point to file that is barrel file index when regex touch anything else it broke the ui. Not sure if it because my poor code standard. 😅

pipech avatar Aug 05 '22 04:08 pipech

@Sodj I think you should play around with it a bit, but for my case it should only point to file that is barrel file index when regex touch anything else it broke the ui. Not sure if it because my poor code standard. 😅

@pipech I have a lot of barrel files, I tried putting the regex in webpackconfig like suggested above but it didn't work

Sodj avatar Aug 05 '22 08:08 Sodj

@Sodj

Also, for some reason, the prototypes I added to the Array object stopped working

Adding to the Array prototype IS considered a side effect, you should not tree shake that if needed globally.

zomars avatar Aug 09 '22 12:08 zomars

@zomars was able to add a regex that works for me ... thanks

Sodj avatar Aug 15 '22 12:08 Sodj

Am unbarreling my barrels now after finding this, am disappointed that so much complexity has to be added to get barrels to work in NextJS... does anyone have a simple alternative indexing method besides breaking components into libraries separate from the repo being bundled?

n-glaz avatar Aug 31 '22 18:08 n-glaz

I am using nx with webpack 5 and tree shaking works just fine. You can create libraries in the workspace and export them from single barrel index file and import them on other places.

Sh1d0w avatar Aug 31 '22 18:08 Sh1d0w

I am relieved to hear this @Sh1d0w and will have a look to see if this is true

n-glaz avatar Sep 01 '22 14:09 n-glaz

Still not working for me. I had to add "sideEffects": false to package json. It reduced all my pages by 50% or more.

bruceharrison1984 avatar Sep 04 '22 02:09 bruceharrison1984