All NextJS pages have almost the same JS bundle size

Viewed 3446

So, I've been working with NextJS for a while now and I never really looked at the build folder nor the info shown in the console when the build succeeds but I did notice something strange the one time I did look at it.

I have been using TypeScript and wrote a little babel config with TS's config to use absolute paths. So the way I am importing components is by having an absolute path ~/components which is an alias of ./src/components. All imports throughout the app are using components imported from ~/components and there's other absolutes paths in there as well. So, here's what I saw when I built my app

enter image description here

Notice how every single page is almost of the same size? Even the about and 404 pages are HUGE. So my guess was that importing modules using the absolute imports is what's causing the issue and because of that, each page is linking to a JS file that has the entire app code in it . I am not exactly sure and I can't seem to find any resources online that can help me optimize these pages. Honestly, I don't even know what part of the app needs to be configured. Whether it's webpack or babel, I have no clue. So, I was hoping to get some help on this and if there's some further configuration that I have to add so each page uses only as much JavaScript as it needs.

Here's my typescript and babel configs:

TypeScript

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "baseUrl": "./src",
    "paths": {
      "~/components": ["./components"],
      "~/styles": ["./styles"],
      "~/assets": ["./assets"],
      "~/types": ["./types"],
      "~/pages": ["./pages"],
      "~/utils": ["./utils"],
      "~/context": ["./context"],
      "~/data": ["./data"],
      "~/services": ["./services"]
    },
    "allowJs": false,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "target": "ESNext",
    "jsx": "preserve",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitAny": true
  },
  "exclude": ["node_modules"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

Babel

{
  "presets": ["next/babel"],
  "plugins": [
    ["styled-components", { "ssr": true }],
    [
      "module-resolver",
      {
        "alias": {
          "~/components": "./src/components",
          "~/styles": "./src/styles",
          "~/types": "./src/types",
          "~/pages": "./src/pages",
          "~/assets": "./src/assets",
          "~/utils": "./src/utils",
          "~/context": "./src/context",
          "~/data": "./src/data",
          "~/services": "./src/services"
        }
      }
    ],
    ["import", {
      "libraryName": "antd",
      "style": true
    }]
  ]
}

2 Answers

So here's what I have been able to figure out after doing some research. When you're importing stuff from a file that exports out a bunch of React components, your module bundler is not intelligent enough (yet) to figure out which components you'll be using so it includes all of them by default and therefore you have a massive bundle.

So regardless of whether you're using the components, they will all still be included in one giant bundle. Nextjs does Code Splitting by default using Webpack but since every single component is coming from the same path which is my case was ~/components, Next (or Webpack) couldn't split it up so it included them all in one bundle which got added to every page regardless of whether the page was importing one component or a 100 from that path.

So that's the reason why I was seeing huge bundle sizes for every page. I had close to a 1000 components living inside ~/components and I had an index file in that directory that exported out all the them. This way I could simply do

import { Component101, Component204 } from '~/components'

But the bundle size was MASSIVE. So, to solve this, I added Tree Shaking to my Webpack config. The term comes from Rollup originally which is another popular module bundler like Webpack. If you're using Webpack, more info on Tree Shaking can be found here

With Tree Shaking, any piece of code that isn't being imported anywhere will be shaved off and not be included in the bundle so you will get smaller bundles. But do keep an eye out for any side-effects. Here's what my bundle size for all pages looks like after adding Tree Shaking:

After Tree Shaking

If you don't want to use Tree Shaking, another way would be to just use relative imports but I am not sure if that would be the recommended approach for large scale apps.

The comments of this pull request explain everything.

This pull request updates a page's "Size" to only be its own set of dependencies.

This PR also adds a new "First Load" column which includes shared size, and moves coloring to that column (since first load is what matters for budgets).

Related