["Framework (React, Next.js, Vue, Svelte)","Build tool (Vite, webpack, Turbopack)","Current bundle size and performance metrics","Route structure and component hierarchy","Critical vs. non-critical features"]
outputs
["Code splitting strategy with chunk boundaries","Dynamic import implementation for routes and components","Suspense boundaries with loading states","Bundle analysis report and optimization recommendations","Preloading and prefetching strategy for split chunks"]
Code splitting breaks a monolithic JavaScript bundle into smaller chunks loaded on demand. Instead of shipping 2MB of JavaScript upfront — most of which the user may never need — you load only the code required for the current view and defer the rest. This directly improves Time to Interactive (TTI), Largest Contentful Paint (LCP), and Total Blocking Time (TBT).
Key Concepts
Splitting Strategies
Strategy
Granularity
Best For
Route-based
Per page/route
SPAs with distinct pages
Component-based
Per feature/widget
Heavy components (editor, chart, map)
Library-based
Per vendor package
Large dependencies (moment, lodash, d3)
Interaction-based
On user action
Features triggered by click/hover
Viewport-based
On scroll into view
Below-the-fold content
How Dynamic Imports Work
Static import:
import { heavyFn } from './heavy'; // Included in main bundle ALWAYS
Dynamic import:
const { heavyFn } = await import('./heavy'); // Separate chunk, loaded on demand
Bundler sees dynamic import → creates separate chunk → loads via <script> tag at runtime
Critical Path vs. Lazy Path
CRITICAL (load immediately):
- App shell / layout
- Current route's above-the-fold content
- Authentication state
- Design system primitives (Button, Input)
LAZY (load on demand):
- Other routes
- Modals, dialogs, drawers
- Admin panels, settings pages
- Heavy editors (rich text, code editor)
- Charts, data visualization
- PDF/export functionality
Target chunk sizes:
Main bundle: < 100KB gzipped (critical path)
Route chunks: < 50KB gzipped each
Vendor chunk: < 150KB gzipped (shared dependencies)
Total initial: < 200KB gzipped (for fast TTI on 3G)
Best Practices
Split at route boundaries first. This is the highest-impact, lowest-risk splitting strategy. Every page the user does not visit is JavaScript they never download.
Add Suspense boundaries at each split point. Always provide a meaningful loading state — skeleton screens over spinners. Nested Suspense boundaries prevent full-page loading states.
Preload on hover/focus, not just on navigation. The ~200ms between hover and click is enough to start loading the chunk, making navigation feel instant.
Analyze regularly. Run bundle analysis after adding new dependencies. A single import dayjs in a shared utility can add 20KB to every chunk.
Use React.lazy for client components, next/dynamic for Next.js. Next.js dynamic imports support SSR control and have built-in loading prop support.
Do not over-split. Each chunk has HTTP overhead (connection, headers, parsing). Splitting a 2KB component into its own chunk is counterproductive. Target chunks >10KB.
Common Pitfalls
Pitfall
Symptom
Fix
No Suspense boundary
Unhandled error or blank screen on lazy load
Wrap every lazy() component in <Suspense> with fallback
Splitting tiny modules
More HTTP requests than bytes saved
Only split components/routes >10KB; use bundle analyzer to verify
Missing error boundary
White screen on chunk load failure (network error)
Add <ErrorBoundary> around <Suspense> with retry UI
Import in render path
New chunk created every render, never cached
Define lazy() outside component body, at module level
Re-exporting barrel files defeat tree shaking
Entire module included despite importing one export
Import directly from source file, not barrel index.ts
Waterfall loading
Parent chunk loads, then child chunk loads sequentially
Use Promise.all or prefetch to load parallel chunks; flatten lazy boundaries
Dynamic import path not statically analyzable
Bundler cannot create chunk
Use string literal in import(), not variables. Template literals with webpack require care
SSR mismatch with client-only lazy components
Hydration error
Use next/dynamic with ssr: false or check typeof window