Concept lesson

Modern ESM & Tree-Shaking

ECMAScript module resolution, TypeScript bundler mode, and tree-shaking dead code elimination.

lesson
Freshness: current14 min read
Mastery
not started · 0%

Learning outcomes

  • Configure TypeScript for modern bundlers
  • Optimize static export graphs for tree-shaking

Mental model

ESM (import/export) evaluates static module dependency graphs before code execution, whereas CommonJS (require()) loads modules dynamically at runtime.

Static Import Declaration
Build Static Dependency Graph
Tree-Shaking Dead Code Elimination
Emit Bundled Output Asset
Conceptual teaching model synthesized from:TypeScript 5.4 Language Specification & Type System Mechanics

Theory

Because ESM imports must appear at top-level, modern bundlers (Vite, Rollup, Turbopack, esbuild) construct a static Abstract Syntax Tree (AST) graph of all exported symbols. Unreferenced exports are eliminated during dead code elimination (Tree-Shaking). In TypeScript 5.0+, setting moduleResolution: "bundler" aligns type resolution with modern web build tools.

export function add(a: number, b: number): number {
  return a + b;
}

export function unusedHeavyMath(x: number): number {
  return Math.pow(x, 100);
}

import { add } from "./mathUtils";
console.log(add(5, 10));

Alternatives and trade-offs

  • CommonJS (require): Supports dynamic path evaluation (require('./' + name)), but prevents static analysis and dead code elimination.
  • ESM (import): Static, tree-shakeable, and natively supported across modern browsers and Node.js 18+.

Failure modes and misconceptions

  1. Side-effect imports: Importing files with top-level side effects (e.g. import './polyfills') prevents bundlers from shaking those modules. Mark "sideEffects": false in package.json.
  2. CJS/ESM dual package hazard: Mixing CJS require and ESM import for the same library can load two separate singleton instances into memory.
Reflect before revealing the guide

Decision scenario

Configure TypeScript applications targeting modern bundlers (Vite, Next.js) with "moduleResolution": "bundler" and "sideEffects": false in library packages.

Learning outcomes

  • Contrast static ESM module graphs with dynamic CommonJS require paths.
  • Configure moduleResolution: "bundler" for modern frontend tooling.
  • Optimize export structures to enable tree-shaking dead code elimination.

Trade-offs

ESM enables aggressive tree-shaking and smaller production bundle sizes, but requires consistent package exports configuration across full-stack repositories.

Evidence assessment

Theory and decision mastery

not-started · 0%
theory0%
decision0%
activityNot mapped
projectNot mapped
1. Why can bundlers perform tree-shaking dead code elimination on ESM but not easily on CommonJS?
2. What setting in package.json signals to bundlers that unreferenced sub-modules can be safely removed?
3. What issue can arise from dual-publishing both CJS and ESM formats in a single library package?

Decision scenario

An engineering team is upgrading a TypeScript app targeting modern Vite and Next.js bundlers.

Which tsconfig.json moduleResolution setting correctly models modern bundler import rules?

Primary sources