August 29, 2025
Next.js bundles JavaScript and CSS to serve pages quickly in development and production. Tree‑shaking is the mechanism modern bundlers use to discard unused code at build time, but barrel files – modules that re‑export hundreds of symbols – can defeat tree‑shaking because the bundler is forced to include the entire barrel in the client bundle. Component libraries (@mui/material, react-icons, date‑fns, etc.) often use barrel files for convenience, so naive imports such as import from '@mui/material' end up shipping the entire library to the browser.
Next.js introduced two compiler options to address this problem:
Both features aim to mitigate the cost of barrel files, but they differ significantly in use‑cases and limitations.
TL;DR
At build time, Next enables an SWC transform with an allow-list of package names (the ones you specify in experimental.optimizePackageImports, plus some defaults). For import lines like:
import { Button, Checkbox as C } from '@mui/material'
It:
import Button from '@mui/material/Button'
import C from '@mui/material/Checkbox'
With this approach, Next.js can optimize libraries whose internal structure you may not know. You simply specify the package name, and the transform conservatively rewrites imports it can prove safe.
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: [
'@phosphor-icons/react',
'@radix-ui/react-icons',
'validator',
'@mantine/core',
],
},
};
The transform only considers bare package specifiers like '@mui/material', 'date-fns', 'rxjs'. It does not look at:
For first-party code, use modularizeImports to map your aliases which we’ll discuss in depth below. If you want the auto optimizer for your own code, publish it as a package and import it as a package (e.g., '@your-org/ui'), then add it to the allow-list.
Only packages in the configured list (plus any framework defaults) are considered. See the defaults here.
The transform applies to static import declarations. It does not rewrite require(...) calls or runtime import('pkg'); use static, named imports for optimization.
There is no built-in report listing which imports were not optimized. Skipped lines are left as-is without a compiler log; verify results by inspecting emitted chunks or enabling a bundle analyzer.
// Good (named, static)
import { addDays } from 'date-fns'
// Not optimized (namespace)
import * as df from 'date-fns'
// Not optimized (dynamic)
const mod = await import('date-fns')
A default import from the package root (e.g., import Foo from 'pkg') often refers to a barrel’s default (which may aggregate multiple files). Unless the transform can prove the default is one leaf file, it won’t rewrite it.
// Stays as-is; default may be an aggregator
import _ from 'lodash-es'
// Already explicit; no rewrite needed
import isEqual from 'lodash-es/isEqual'
Side‑effectful means the module runs code at import time that changes observable program state (e.g., mutating globals, registering polyfills, attaching properties to exports/module.exports, touching the DOM, injecting CSS, or branching on process.env). If a barrel’s export surface depends on those side effects or on evaluation order, the optimizer can’t safely determine a unique leaf per symbol, so it leaves the import as‑is. (Note: a module can be side‑effectful and still be rewritten if its exports are declared statically - the real problem is when side effects define or alter exports.)
A dynamic barrel defines exports at runtime- for example, by adding properties to the exported object or using getters to lazily require files. Because the export graph isn’t purely static, the optimizer can’t prove a single file per symbol.
Common patterns that block optimization:
// Pattern A: lazy getters
Object.defineProperty(exports, 'X', {
enumerable: true,
get: () => require('./x') // runtime getter
})
// Pattern B: attach methods dynamically to an exported object
const api = function () {}
api.Button = require('./button')
api.Card = require('./card')
module.exports = api
Real‑world example: the lodash root import (require('lodash')) exports a function/object and then attaches methods at runtime (e.g., _.chunk = require('./chunk')). From the optimizer’s perspective, that top‑level entry is a dynamic barrel, so it can’t statically map to one ESM leaf. Prefer lodash-es (pure ESM), explicit deep paths like import chunk from 'lodash/chunk', or configure compiler.modularizeImports.
For each named specifier you import, the optimizer only rewrites it if it can resolve that symbol to exactly one concrete file inside the package. If it can’t prove that (e.g., multiple candidates or unclear re-exports), it leaves your import unchanged. This does not require one symbol per file-the target file may export multiple symbols, the key is that the symbol maps to one file.
If multiple files could provide the same symbol via export * chains, or if the transform can’t determine a unique source (e.g., both a.ts and b.ts export Button and index.ts does export * from './a'; export * from './b';), it does not guess. It keeps your original import to avoid changing semantics or side-effects ordering.
Re-export chains are followed up to a fixed depth (10). Beyond that, the import is left intact.
modularizeImports is a pattern-driven SWC transform configured in next.config.js under compiler.modularizeImports. Unlike optimizePackageImports, it does no package analysis. It simply rewrites import sources according to your mapping template.
Config shape (common usage)
// next.config.js
module.exports = {
compiler: {
modularizeImports: {
'@mui/material': { transform: '@mui/material/{{member}}' },
'date-fns': { transform: 'date-fns/{{member}}' },
'@/components': { transform: '@/components/{{member}}' },
},
},
}
How the transform works
Download our service guide to understand how we can help you optimise your site speed
Before => After
// before (first‑party alias)
import { Card } from '@/components'
// after
import Card from '@/components/Card'
Key differences vs optimizePackageImports
Modularize Imports - This option has been superseded by optimizePackageImports in Next.js 13.5. We recommend upgrading to use the new option that does not require manual configuration of import paths.
Docs: https://nextjs.org/docs/architecture/nextjs-compiler#modularize-imports
Scenario | Use | Why | Notes |
---|---|---|---|
First‑party alias/relative barrels(e.g., @/components, ./ui) | modularizeImports | Optimizer targets bare specifiers only; aliases/relative paths need explicit mapping. | Works in Pages Router and older Next.js. |
Third‑party package via bare specifier with static exports (e.g., @mui/material, date-fns) | optimizePackageImports | Automatic deep‑splitting with low maintenance. | Won’t split namespace/default/dynamic imports. |
Internal monorepo/UI package imported as bare specifier (e.g., @your-org/ui) | PreferoptimizePackageImports | It’s a package; add to allow‑list if exports are static. | Use modularizeImports if you need strict, frozen mappings. |
Redirect to a different package/path(e.g., react-icons → @react-icons/all-files/{{member}}) | modularizeImports | Only manual rules can redirect across packages. | Useful for migrations or known better subpaths. |
Dynamic or side‑effectful CJS barrels (e.g., lodash root) | modularizeImports or deep paths | Optimizer can’t statically prove leaves. | Prefer ESM variants like lodash-es. |
Namespace/default/dynamic imports in your source | Refactor | Need static named imports for splitting. | Or import from explicit deep paths. |
Ambiguous export graphs or deep chains (>10) | modularizeImports | Avoids ambiguity and depth cap. | Keep barrels simple to help auto. |
Pages Router or Next.js < 13.5 | modularizeImports | Optimizer may not apply in these setups. | Upgrade later to use the optimizer. |
Compliance/allow-listing / deterministic builds | modularizeImports | Explicit, auditable import surfaces. | Pin versions; CI check for breakage. |
"Mostly works but misses a few" | Combine both | Auto for broad wins; manual for exceptions. | Manual rules override auto where they match. |
Get rid of your own barrel files, or keep barrels simple
in your own code. If you want your internal package to benefit from auto optimization, make exports static and one-symbol-per-file.
Download our service guide to understand how
we can help you optimise your site speed