ESM vs CJS output for a published library
ESM (import/export) is statically analyzable → tree-shakable, the modern standard, works in browsers/bundlers. CJS (require/module.exports) is dynamic, the legacy Node format, not tree-shakable. Best practice: ship BOTH via package.json `exports` conditional exports, plus type declarations.
A published library has to decide what module format(s) to ship — and the modern answer is both, properly configured.
ESM — ECMAScript Modules
import/export. The modern standard:
- Statically analyzable — imports are top-level and known at build time → tree-shakable, so consumers' bundlers can drop unused exports.
- Native in browsers and all modern bundlers; the future of Node too.
- Supports async loading, top-level await.
CJS — CommonJS
require()/module.exports. The legacy Node format:
- Dynamic —
requireis a runtime function call, can be conditional/computed → not statically analyzable → not tree-shakable. - Still everywhere: older Node apps, older tooling, lots of existing infrastructure.
Why ship both
If you ship only ESM, older CJS consumers (and some tooling) break. If you ship only CJS, modern consumers lose tree-shaking and ESM benefits — your library bloats their bundle. So a well-published library ships both and lets the consumer's environment pick.
How: package.json conditional exports
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs", // ESM consumers get this
"require": "./dist/index.cjs" // CJS consumers get this
}
},
"main": "./dist/index.cjs", // legacy fallback
"module": "./dist/index.mjs", // older bundler hint
"types": "./dist/index.d.ts"
}The exports field's conditional exports route import vs require to the right build. main/module are legacy fallbacks. Always ship type declarations too.
The pitfalls
- The dual-package hazard — if some code paths load the ESM build and others the CJS build, you can end up with two copies of your library (separate module state) — bad for anything stateful (singletons, context). Mitigate by keeping shared state minimal or having one build re-export the other.
.mjs/.cjsextensions or"type": "module"to disambiguate.- Build tooling — tsup, unbuild, Rollup commonly produce the dual output.
The trend
The ecosystem is moving ESM-first; some new libraries ship ESM-only now. But for broad compatibility today, dual output via exports is the safe default.
The framing
"ESM — import/export — is statically analyzable so it's tree-shakable, and it's the modern standard for browsers and bundlers. CJS — require — is dynamic, not tree-shakable, but still everywhere in legacy Node and tooling. So a well-published library ships both and uses package.json conditional exports to route import vs require to the right build, plus main/module fallbacks and type declarations. The thing to watch is the dual-package hazard — loading both builds gives you two copies with separate state — so keep shared state minimal. The ecosystem is trending ESM-first, but dual output is the safe default today."
Follow-up questions
- •Why is ESM tree-shakable but CJS isn't?
- •What is the dual-package hazard?
- •What does the package.json `exports` field do?
- •Would you ever ship ESM-only?
Common mistakes
- •Shipping only ESM and breaking CJS consumers (or vice versa).
- •Not configuring the exports field, so resolution is wrong.
- •Forgetting to ship type declarations.
- •Ignoring the dual-package hazard for stateful libraries.
Performance considerations
- •Shipping ESM lets consumers tree-shake your library, directly reducing their bundle size. CJS-only forces the whole library into their bundle. The exports field ensures each consumer gets the optimal build.
Edge cases
- •A stateful library loaded as both ESM and CJS — two instances.
- •Old bundlers that only understand main/module, not exports.
- •A consumer in a CJS project importing an ESM-only package.
- •Mixed .js / .mjs / .cjs resolution.
Real-world examples
- •Modern libraries using tsup/unbuild to emit dual ESM+CJS with conditional exports.
- •Some newer packages going ESM-only as the ecosystem shifts.