Component-level — heavy + below-fold/conditional (modal, map, editor, charts).
Prefetch likely-next chunk so the split is invisible.
Vendor split = caching play (not size): stable node_modules in own content-hashed chunk → survives app deploys. Configure via splitChunks/manualChunks.
Spinner on first nav? You moved cost to nav, not added it. Prefetch on intent (hover/viewport) + startTransition + layout skeleton. Don't undo the split.
Tree-shaking
= dead-code elimination across modules.
Needs ESM — import/export are static (known at build). CJS require() = dynamic → can't shake.
sideEffects:false = "nothing happens on import" → drop whole unused files, not just exports.
Side effect = runs on import (CSS import, polyfill, global mutation).
Clean ESM + best tree-shaking → publishing libraries; was Vite's prod bundler
esbuild
Go
Extreme speed; was Vite's dep pre-bundler · thin plugin/split support
Oxc+Rolldown
Rust
Oxc = Rust toolchain (parser, oxlint, fmt, minifier, resolver); Rolldown = bundler on it, Rollup-compatible. New engine under Vite
Vite
—
Dev = native ESM (no bundling). Vite 8 (Mar'26) unified on Rolldown (was esbuild-dev/Rollup-prod). SPA default
Turbopack
Rust
Webpack's successor, incremental, Next.js/Vercel
Why Vite is instant: Webpack bundles whole app before serving (start scales w/ size). Vite serves source over native ESM, transforms a module only when requested; HMR invalidates one module. 2026: toolchain consolidating onto one Rust core — Oxc powers Rolldown + oxlint (≈50–100× faster than ESLint); Vite 8 unified dev+prod on Rolldown, killing the dev/prod "split-brain." SWC = Rust Babel-replacement.
Swap heavy libs → moment→date-fns/dayjs/Intl; full lodash→per-method/lodash-es.
De-dupe multiple versions → one (resolutions / single-version monorepo policy).
CI budget → size-limit / bundlesize / Lighthouse-CI fails the PR.
Wire compression (Brotli/gzip) = different axis from bundling — both matter (Lesson 16).
Lead one-liners (memorize)
Goal “JS is parsed & run on the main thread — the most expensive byte. So I optimize what we ship: route-split, tree-shake, and a CI size budget.”
Tree-shake “Only works because ESM imports are static — CJS can't be shaken. sideEffects:false drops whole unused files. Killers: barrel files & whole-namespace imports.”
Vite “Webpack bundles the whole app before serving; Vite serves source over native ESM — that's the near-instant dev start. Vite 8 unified dev+prod onto Rolldown (Rust, on Oxc), ending the esbuild/Rollup split-brain.”
2026 “The toolchain is consolidating onto one Rust core — Oxc (parser/resolver) powers Rolldown, oxlint (~50–100× ESLint), and oxfmt — shared infra instead of re-implemented per tool.”
Governance “Across many teams it's a budget, not a cleanup: route-split + de-dupe + a size-limit CI gate that fails the PR.”
Split spinner “Splitting moved the cost from load to navigation; I pay it back with prefetch-on-intent (hover/viewport) and keep the old page alive with startTransition — instant click, no re-bloat.”
1.JS is the most expensive byte (executed on main thread) — "ship less JS," not "fewer bytes."
2.Split point = dynamic import(). Route-split first, then heavy below-fold. Static import = initial bundle.
3.Tree-shaking needs ESM — CommonJS can't be shaken. Prefer the -es/ESM build of a dep.
4.sideEffects:false can delete your CSS — list it: "sideEffects":["*.css"].
5. Tree-shaking killers: CJS deps · barrel index.js · whole-namespace import (import _ from 'lodash'). Verify in analyzer.
6.Over-splitting = request waterfall (many round trips + spinners). Don't lazy-load per component. Same for vendor: one giant chunk busts cache on any dep bump, too many = waterfall — split into a few groups.
7. Don't migrate bundlers for fashion — what you ship beats which tool ships it. Migrate on measured pain.
8. Route-split spinner = cost moved to navigation, not added. Fix with prefetch-on-intent + startTransition, not by reverting. Don't eager-prefetch everything (mobile data; respect Save-Data).