next.js
next.js copied to clipboard
Tree shaking doesn't work with Typescript barrel files
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:
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:
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]
Feel free to investigate and solve
https://github.com/vercel/next.js/discussions/13490
Same happening with jsx also. version: 9.4.2
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.
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 @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 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.
With webpack 5, tree shaking seems to work. https://webpack.js.org/blog/2020-10-10-webpack-5-release/#major-changes-optimization
@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 @sphilee I have concluded the same. Webpack 5 didn't solve this issue for me.
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.
@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?
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';
I actually started doing the same recently:

But it turns out, no luck:

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
I actually started doing the same recently:
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.
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
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
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.
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
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.
For tree-shaking to work with a barrel file you need
- flag the file and/or the child files as side effect free (
"sideEffects": falsein 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
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?
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).
not bundle the library (it must be in separate files for sideEffects to work
@sokra I want to have barrel files as the
mainfile for each of my monorepo's packages. If I am adding these monorepo packages tonext-transpile-modules, would that constitute "bundling" the library?
no. I was talking about double bundling, so using a pre-bundled library.
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?
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);
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,
},
]
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.
^ yessss, trimmed 200kb from my bundled too, thanks mates
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?
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.
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.
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
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
This sounds promising,
do I have to add the path of each index.ts file?
or would this work /src\/.*index.ts/i ?
Just chiming in to say that adding "sideEffects": false to my app's package.json solved this issue for me
(for [email protected].*)
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
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. 😅
@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
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 was able to add a regex that works for me ... thanks
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?
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.
I am relieved to hear this @Sh1d0w and will have a look to see if this is true
Still not working for me. I had to add "sideEffects": false to package json. It reduced all my pages by 50% or more.