Bundling & code-splitting economics
Bundling work answers one question: what code must be downloaded, parsed, compiled, and executed before the user can complete the task? A senior answer is not “split everything.” It is a measured plan that reduces critical-path JavaScript without creating slow follow-up navigations.
Core details
| Concern | What it means | Failure mode |
|---|---|---|
| Initial bundle | Code required for first render and first interaction | Great Lighthouse screenshot, bad real-device time-to-interactive |
| Route split | Load route-specific code on navigation | Extra round trip on a route users open immediately |
| Component split | Lazy-load rare panels, editors, maps, charts | Spinner where the main task should be |
| Vendor split | Share stable dependencies across routes | Large common chunk becomes everyone’s tax |
| Tree-shaking | Remove unused exports through ESM analysis | Barrel imports or side effects retain whole packages |
| Prefetch | Speculatively fetch likely next code/data | Wastes bandwidth and battery on low-intent paths |
Analyzer workflow: capture production build stats, sort by parsed size and compressed size, group modules by feature owner, then ask whether each large module is needed before the first meaningful task. Chase the largest cohorts before tiny syntax-level changes.
Import boundaries: dynamic import() is useful when a dependency is genuinely conditional: admin-only screens, rich text editors, map SDKs, charting, search overlays, heavy date/time libraries, or payment flows after checkout intent.
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("./RevenueChart"), {
loading: () => <ChartSkeleton />,
});Tree-shaking requirements: use real ESM imports, avoid catch-all barrels for hot packages, and keep package.json sideEffects accurate. Setting sideEffects: false blindly can remove CSS registration, polyfills, telemetry setup, or custom element registration.
Prefetch policy: prefetch when intent is visible: hovered links, near-viewport links, predictable wizard steps, or idle time after the current task is stable. Disable or narrow prefetch for expensive routes, metered networks, or personalized data that may go stale before use.
Understanding
Splits move cost from initial load into later navigations. That improves outcomes only when deferred routes aren't on your activation critical path. Otherwise you lengthen time-to-task with extra round trips—you prove the trade against navigation profiles tied to KPIs—not gzip bragging alone.
Teams accumulate duplicate transitive dependencies (five date/format/icon stacks) disguised behind convenience imports; analyzers show clusters ripe for consolidation and internal shared utilities.
The hard part is that compression size is not the whole cost. JavaScript also costs parse, compile, execution, hydration, memory, and invalidation. A 60 KB library that runs expensive initialization may hurt more than a 120 KB module loaded after a clear user action.
Good splitting follows task boundaries, not file boundaries. A checkout route should not wait for the marketing carousel. A text editor should not ship to users who only read comments. A dashboard’s charting library may be acceptable if the dashboard is the task, but unacceptable if it blocks login or onboarding.
Practical examples
| Scenario | Good move | Risk to check |
|---|---|---|
| Rare rich text editor | Lazy-load editor after “Edit” intent | Preserve focus and avoid layout jump when editor arrives |
| Large icon library | Import named icons from the package entry recommended by the library | Barrel import may retain the full icon set |
| Heavy date library | Use platform Intl or a smaller scoped helper | Locale/timezone correctness still matters |
| Micro-frontend shell | Share stable runtime dependencies intentionally | Version skew can duplicate React or design-system runtime |
| Above-the-fold chart | Keep in initial route if it is the product task | Skeleton must reserve dimensions to avoid CLS |
Senior understanding
| Signal | What staff-level answers include |
|---|---|
| Design-system drift | token/CSS pipeline unintentionally duplicated runtime blobs |
| Regressions in CI | optional route-level artifact budgets or smoke perf harness—honest about flakiness trade |
| Micro-frontend edges | shared vendor dedupe story + version skew governance |
Call out tail latency on low-end devices. Thin medians can hide expensive cold starts during real navigation sequences, so compare field RUM with a repeatable lab trace before making the optimization permanent.
Staff operating model: set budgets by route group, fail builds only for stable signals, allow explicit budget waivers with an owner and expiry, and review dependency additions as architecture changes rather than “just npm install.”
Failure modes
- Splitting a component that appears immediately after hydration, causing a waterfall.
- Prefetching every route and making mobile users pay for pages they never open.
- Shipping duplicate design-system packages because multiple teams pin different versions.
- Marking a package side-effect-free when it registers CSS, globals, custom elements, or polyfills.
- Measuring only gzip while ignoring parse/compile/execute and hydration cost.
Interview drill
Question: "A route became slower after adding charts and a rich editor. How do you reduce JS cost?"
Model answer structure:
- Start from production build stats and field symptoms, not package-name intuition.
- Group large modules by route and feature owner, including duplicate transitive dependencies.
- Ask whether each module is needed for the first meaningful task or can wait behind user intent.
- Split rare/editor/map/chart flows, preserve layout with skeletons, and tune prefetch by intent and network cost.
- Guard route budgets with owners, expiry for waivers, and field monitoring for parse/execute regressions.
Follow-ups to expect:
- "Why is gzip size not enough?"
- "How do barrel imports defeat tree-shaking?"
- "When can code splitting make navigation worse?"
Diagram
See also
Mark this page when you finish learning it.
Spotted something unclear or wrong on this page?