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:

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]

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 174
  • Comments: 80 (13 by maintainers)

Commits related to this issue

Most upvoted comments

I 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?

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 private and public class 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 pages directory (or client components under app), 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 the app directory.

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.

 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,
            },
        ]

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:

  • Moved all components into another package in my monorepo, and added sideEffects: false to that package.
  • Many variations of editing the webpack config rules with sideEffects: false, both when the components were in the same package and in the new package. Interestingly, adding sideEffects: true doubles the size of the Pages Router pages, but the App Router pages stay the same.
  • Many attempts with both Babel and SWC
  • I think I am unable to use modularizeImports as I split my components into feature folders with a few layers of barrel files.

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

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

  • 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

An 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.

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.

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 🤷‍♂️

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

  webpack(config) {
    config.module.rules.push({
      test: /index\.(js|mjs|jsx|ts|tsx)$/,
      include: (mPath) => mPath.includes('component-library/src'),
      sideEffects: false,
    });

    return config;
  },

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”.

Please let us know if https://nextjs.org/docs/app/api-reference/next-config-js/optimizePackageImports resolves your issue with regards to proper tree shaking with Typescript barrel files. If yes, we will proceed with moving this issue to Discussions.

The very idea that we have to manually list libraries that were previously tree-shaken automatically is… not inspiring.

+1 here, using a Nx monorepo and no tree-shaking is being done on the app router for custom libraries

I 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?

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.

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.

Correction: Parcel’s treeshaking is built into it’s minification thing, so if you run it with minification on, it’ll treeshake just fine as well.

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": false to my app’s package.json solved 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:

"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.

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

export const OneComp = () => {
  console.log('first');

  return <div>OneComp</div>
}

Second file (/components/CoupleComp.tsx):

export const Comp1 = () => {
  console.log('fst');

  return <div>OneComp</div>
}

export const Comp2 = () => {
  console.log('sec');

  return <div>OneComp</div>
}

export const Comp3 = () => {
  console.log('thrd');

  return <div>OneComp</div>
}

Third file (/components/Sub/SubComp.tsx):

export const SubComp = () => {
  console.log('sub');

  return <div>OneComp</div>
}

Nested barrel file (/components/Sub/index.ts):

export * from './SubComp'

And the last one - main barrel file (/components/index.ts):

export * from './CoupleComp'
export * from './OneComp'
export * from './Sub'

It looks like this in the end:

image

Then I have imported and used 2 from 5 components imported from the main barrel file into _app.tsx file (similar scenario to the one provided by the author on the whole thread):

import { OneComp, Comp3 } from '@/components'
import '@/styles/globals.css'

export default function App() {
  return (
    <>
      <OneComp />
      <Comp3 />
    </>
  )
}

…and the end file generated after running npm run build and then npm start contains ONLY imported component’s code.

This is the content of generated /_next/static/chunks/pages/_app-cfb55d8006062ffe.js file:

(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{6840:function(n,e,_){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return _(3564)}])},3564:function(n,e,_){"use strict";_.r(e),_.d(e,{default:function(){return App}});var c=_(5893);let Comp3=()=>(console.log("thrd"),(0,c.jsx)("div",{children:"OneComp"})),OneComp=()=>(console.log("first"),(0,c.jsx)("div",{children:"OneComp"}));function App(){return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(OneComp,{}),(0,c.jsx)(Comp3,{})]})}_(415)},415:function(){}},function(n){var __webpack_exec__=function(e){return n(n.s=e)};n.O(0,[774,179],function(){return __webpack_exec__(6840),__webpack_exec__(9974)}),_N_E=n.O()}]);

As you can see, it only contains code of OneComp and Comp3 which were imported. You can easily double check that by searching for console.log instances.

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": false to 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.

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

@ivan-kleshnin out of curiosity, why did you thumbs down ESLint plugin I suggested? https://github.com/vercel/next.js/issues/12557#issuecomment-1754370032 It is working great for us

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 @headlessui and @heroicons that I’m using and that should be auto-included… – no effect. Might be because I already had sideEffects: false and other webpack gimmicks – I dunno.

And total bundle size with Next 13.5 actually increased by 2kb 🤦‍♂️

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. 😁

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.

@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

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.

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.

…They are convenient, and I’m not aware of any cons

@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 optimizePackageImports if 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-L765

Ideally, 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.

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

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

module.exports = withTM(config);

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

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.

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

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:

// ServerImageWithBlur.tsx
import { getPlaiceholder } from "plaiceholder"

export const ServerImageWithBlur = async (props: ImageProps) => {
  const { base64, img } = await getPlaiceholder(props.src)
  return <Image placeholder="blur" blurDataURL={base64} {...props} {...img} />
}
// Navbar.tsx
"use client" 

export const Navbar = () => {
  return <div>...</div>
}
// index.ts
export { ServerImageWithBlur } from "./ServerImageWithBlur"
export { Navbar } from "./Navbar"

Error:

Unhandled Runtime Error
ReferenceError: require is not defined

When I comment the re-export of the server component from index.ts, the application works.

// index.ts
// export { ServerImageWithBlur } from "./ServerImageWithBlur"
export { Navbar } from "./Navbar"

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

@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