next.js: 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]
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 174
- Comments: 80 (13 by maintainers)
Commits related to this issue
- Tree shaking with typescript sideEffects: false, makes tree shaking work with barrels files https://github.com/vercel/next.js/issues/12557 Mapping only needed data from posts Change first render tweet... — committed to MarcoMadera/Blog by MarcoMadera 3 years ago
- Fix tree shaking for barrel files See: vercel/next.js#12557 vercel/next.js#27814 https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free — committed to LacHex/7-news by LacHex 2 years ago
Barrel files are effectively an API. A specific interface that distinguishes private vs public code. It especially makes sense in a monorepo with local packages. The same concept (why do packages have a specific import interface) pros/cons applies to monorepo packages. Being in the package mindset is helpful in its own right re; reusability/scalability too.
My opinion: barrels are annoying (having to create them, maintain them, deal with circular imports), but denoting public vs private code is helpful in maintenance just like
privateandpublicclass methods.Less refactoring required when files change; only the “API” (barrel file) needs to be updated.
Hides implementation from consumers.
Clear what changes are necessary if anything in public API changes, or if nothing in public API changes.
Arbitrary files imported by arbitrary other files is harder to maintain and understand than a single barrel. The possible scope of any modification is larger without barrels.
Ultimately, we all use barrels, via libraries. The same reasoning “why do libraries not just expose all file paths” applies to our own code as well.
There are cons, but that’s not your question. Those are just my 2cents that come to mind right now.
Feel free to investigate and solve
This has become a bigger issue with the introduction of App directory.
In the
pagesdirectory (or client components underapp), importing from barrel files works. It might not tree shake anything but at least the build is successful.With server components, if we import a component from an npm package, and even a single export of that library uses a client-side hook like
useState, Next.js will throw an error for all imports from the barrel file of that npm package.This means the only way right now to use imports from component libraries in a server component is to ask library authors to provide deep imports to the component files just for Next.js. We could alternatively mark the whole app as client rendered with
"use client"but that defeats the purpose of using theappdirectory.Feedback with example reported here: https://github.com/vercel/next.js/discussions/41745#discussioncomment-4884625
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.
I have spent days trying basically everything I can think of to get Tree Shaking to work with the app router and barrel files, including all the suggested answers here, and have hit a dead end.
Half my pages use the Pages Router, and half use the App Router. The Pages Router pages seem to perform tree shaking on the barrel files properly, but the App Router pages are all double the kB of the Pages Router pages as soon as I import a single component.
Here’s what I’ve tried so far:
sideEffects: falseto that package.sideEffects: false, both when the components were in the same package and in the new package. Interestingly, addingsideEffects: truedoubles the size of the Pages Router pages, but the App Router pages stay the same.The only thing that works is bypassing the barrel files and importing components one by one… but I love barrel files and this seems like a (very time-consuming) step backwards. I think Babel Direct Import Plugin may have worked, but I couldn’t get it to work.
I am pretty amazed that it doesn’t just work out of the box as it seems like such a common thing to do and the whole point of NextJS is things are supposed to work out of the box.
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 likesections, and each of these folders have an index.ts file presented like thisAnd then I’m using my components like this :
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
I’m sorry but I can’t believe that this issue has been opened for more than 3 years now and that there is still no solution 🤷♂️
For tree-shaking to work with a barrel file you need
"sideEffects": falsein package.json)/*#__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 rightAn update on the above: I couldn’t wait for a fix so I spent a day refactoring out my beloved barrel files so all my files now have tons of separate imports. My App Router routes dropped from ~640kB to ~80kB first load JS.
This just trimmed 200kb off my bundled, thanks a million.
I have to agree. All these micro-optimisations and then you have this which would probably save 100s of kb being sent on every route on most nextjs apps. Don’t get it.
If you are using a monorepo with a component library and are still having problems, you can try
n.b. you can change
mPath.includes('component-library/src')to a glob to capture multiple folders/files etc.You can also place a
console.log(mPath)to print out the exact file paths, if you have any confusion of the location of your files relative to next.I’m so happy I decided to make the changes early even if it took me some time to do it. It would have taken me more time today.
Crazy to see this is still not resolved.
Next step might be to move the project to Astro 🤔
Regardless of whether it was or wasn’t done automatically previously, can you explain why it makes sense to manually specify a list of package to optimize? Isn’t it always a good default to optimize dependencies? From the docs, it is not clear in what scenario it is helpful to “not optimize packages”.
From what I can tell, the default should be to optimize/treeshake all dependencies, at least for production, with perhaps an opt-out, although I don’t see a reason for that either. Again, maybe I’m missing some context but this seems to me like really awful API design when considering the principle of “choose good defaults” and “allow users to opt into necessary exceptions”.
The very idea that we have to manually list libraries that were previously tree-shaken automatically is… not inspiring.
+1 here, using a
Nxmonorepo and no tree-shaking is being done on the app router for custom librariesI am curious as to why people use barrel files. I feel like it’s just adding an extra file that has a non-informational name to your project, and often complicates keyboard navigation (open definition, find file). The big advantage is making imports prettier? I spend very little time looking at imports.
Is there a benefit I’m missing on this or is it really about reducing lines of code in the import block?
Same experience here. I was pulling my hair out trying to figure this out. I have a monorepo, and in the
index.tsof each package, I was re-exporting my components. Not only did this break tree shaking: it also madeyarn next devtake 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.2and webpack 4, for context.For those who have hypothesized that it’s a Webpack issue, I cannot reproduce the issue in Webpack alone, treeshaking barrel files seems to work fine there: https://replit.com/@JorenBroekema/barrel-treeshaking
In fact, I’ve also tested this in Rollup and esbuild and they can also treeshake it just fine. Parcel/Browserify not so much
So seems like a NextJS specific problem.
Randomly stumbled upon this, but I highly recommend ESLint rule that restricts use of barrel files
https://github.com/gajus/eslint-plugin-canonical#no-barrel-import
It is auto-fixable so you don’t need to do much to leverage it and it will prevent bloat of your bundle size.
@leerob - any ETA on a solution for this?
@IonelLupu Thank you for re-iterating your issue involving react-aria-components and creating a detailed discussion post. I have created https://github.com/vercel/next.js/issues/60246 from your discussion post and will be taking a look!
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?
Just chiming in to say that adding
"sideEffects": falseto my app’spackage.jsonsolved this issue for me(for
next@12.2.*)@stevethatcodes @majelbstoat Tree shaking seems to work for me as long as I specify side effects in package.json:
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.
Hmm, either something was fixed or I don’t understand.
I have set up very simple Next.js app using
create-next-app(using Next 13.5.6 with pages directory) and created 5 files:First file (
/components/OneComp.tsx):Second file (
/components/CoupleComp.tsx):Third file (
/components/Sub/SubComp.tsx):Nested barrel file (
/components/Sub/index.ts):And the last one - main barrel file (
/components/index.ts):It looks like this in the end:
Then I have imported and used 2 from 5 components imported from the main barrel file into
_app.tsxfile (similar scenario to the one provided by the author on the whole thread):…and the end file generated after running
npm run buildand thennpm startcontains ONLY imported component’s code.This is the content of generated
/_next/static/chunks/pages/_app-cfb55d8006062ffe.jsfile:As you can see, it only contains code of
OneCompandComp3which were imported. You can easily double check that by searching forconsole.loginstances.Anticipating questions: only this file contains code related to components created by me.
Is there something I don’t understand here or what I see here is tree shaking in action? 🤔
Still not working for me. I had to add
"sideEffects": falseto package json. It reduced all my pages by 50% or more.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.
@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
Because that plugin simply “Restricts the use of barrel files”. I don’t see barrel files as something bad that should be avoided, me/my teams in the present/past heavily used them outside of NextJS context, with great success. They are convenient, and I’m not aware of any cons, except that they cripple NextJS… So why I should lint myself against NextJS issue? 🤔 If they aren’t going to fix… ok, we have a growing list of complaints against this framework and React anyway, might be a good final reason to switch out.
@cseas in my project this option did NOT affect the bundle size. I listed about 10 new libraries along with
@headlessuiand@heroiconsthat I’m using and that should be auto-included… – no effect. Might be because I already hadsideEffects: falseand other webpack gimmicks – I dunno.And total bundle size with Next 13.5 actually increased by 2kb 🤦♂️
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.
What you want to do is in your
next.config.jsadd 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.
That it’s, hope this helps. 😁
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:
Or in each of the files:
etc.
Or create a Codemod for transforming barrel imports.
@JoeyFenny I managed to get this working by setting
"sideEffects": falsein 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-696749484Based 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.
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.
@ivan-kleshnin As far as I’m aware, there is a cost to using barrel files, which is why we recently introduced
optimizePackageImports.I agree that it is not ideal to manually have to add packages to
optimizePackageImportsif the package in question is not already manually listed here. → https://github.com/vercel/next.js/blob/12e888126ccf968193e7570a68db1bc35f90d52d/packages/next/src/server/config.ts#L710-L765Ideally, we just automatically detect packages that need to use
optimizePackageImports, so the user doesn’t have to even concern themselves with this. Will be discussing this with the team to see how we can achieve that!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.
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’ve got similar situation to @nandorojo but I managed to get rid of
next-transpile-modulesand replace it withexperimental.externalDirand sometscconfig.jsonpathsconfiguration.Setting
sideEffectsfalse 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).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.
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.Maybe this isn’t related, but I came across this issue when using plaiceholder, which is supported only in server components.
I have barrel/index file which re-exports both server and client “elementary” components. During development when some page imports client component from the barrel file, the app crashes on error “ReferenceError: require is not defined”. So it looks like the server component is automatically imported/loaded and gets into the browser despite the fact that it is marked as server component (without “use client” at the top) and is not used anywhere on the page.
Simplified components are:
Error:
When I comment the re-export of the server component from
index.ts, the application works.@Sodj
Adding to the Array prototype IS considered a side effect, you should not tree shake that if needed globally.
@pipech I have a lot of barrel files, I tried putting the regex in webpackconfig like suggested above but it didn’t work