Modules: import / export
Named vs default, dynamic import, tree shaking and barrel files.
Updated
What a module is
A module is a JS file with its own scope: the variables and functions in it are private until you explicitly export them. Another file uses them through an import.
Why: without modules, all the code would live in the same global space — name clashes, hidden dependencies, impossible to maintain. With modules, each file clearly declares what it offers and what it needs.
// math.js
const secret = 42 // private — not exported
export const sum = (a, b) => a + b // public
// app.js
import { sum } from './math.js'
sum(1, 2)Kinds of export
| Kind | Export syntax | Import syntax | How many per file |
|---|---|---|---|
| named | export const sum = ... |
import { sum } from './math' |
any number |
| default | export default function Page() {} |
import Page from './page' (any name) |
one |
| re-export | export { sum } from './math' |
— | — |
import multiply, { sum, avg as average } from './math.js' // default + named + renaming
import * as math from './math.js' // everything, as an objectNamed vs default — when
| Named | Default | |
|---|---|---|
| The name on import | fixed — the same everywhere | you pick it — it can differ in every file |
| Refactoring, autocomplete, search | better | worse |
| Where it's required | — | page.tsx, layout.tsx, next.config.ts in Next |
Recommendation: named exports for all your code; default only where the framework requires it.
Static vs dynamic import
import x from (static) |
await import() (dynamic) |
|
|---|---|---|
| When it loads | at startup, before the code | on demand, when you reach that line |
| Where | only at the top of the file | anywhere (in functions, conditions) |
| For | almost everything | heavy code that's rarely used: editors, charts, maps |
button.onclick = async () => {
const { Chart } = await import('./chart.js') // downloaded only on click
}In Next: next/dynamic does the same for components — exactly how the code editor in this app is loaded.
Tree shaking
Tree shaking = the bundler removes from the build the exports nobody imports. It works only with static import/export.
import { debounce } from 'lodash-es' // ✓ only debounce ends up in the bundle
import _ from 'lodash' // ✗ the whole library ends up in the bundleBarrel files
A barrel is an index.ts that only re-exports other files (export * from './a'). It seems convenient, but:
- importing a single thing loads everything the barrel re-exports;
- it slows down the dev server and tests;
- it easily creates circular imports.
In our project we import directly from the file — see Feature-Sliced Design.
ESM vs CommonJS
| ES Modules (ESM) | CommonJS (CJS) | |
|---|---|---|
| Syntax | import / export |
require() / module.exports |
| Where | browsers, Next, modern Node | old Node, some libraries |
| Static (tree shaking) | yes | no |
You write ESM. You'll only run into CJS in old configs.
Summary
- A module = a file with its own scope; you export what's public.
- Named exports by default; default only where it's required.
- Dynamic import for heavy code; tree shaking only with static imports; avoid barrels.