๐Ÿณ IntuitiveFE
Login
โ† All concepts

Code Splitting & Tree Shaking

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿงณ Pack for your trip as you go โ€” instead of one giant suitcase (all JS upfront), ship boxes to your destination as you need them.

The airport security analogy

The initial page load is like airport security โ€” the less you carry, the faster you get through. A 180KB main bundle clears security quickly. An 800KB monolithic bundle makes every user wait while the entire app downloads, parses, and executes โ€” even the pages they'll never visit.

Monolithic bundle

All pages, all features, all libraries in main.js. User at /home downloads the checkout page's payment processor code. 800KB parsed before anything renders.

Split bundle

180KB main bundle โ€” renders the home page. Checkout code lives in checkout.js โ€” downloads only when user navigates to /checkout. Home renders in 2x less time.

Code splitting vs tree shaking โ€” what's the difference?

Code splitting

  • โ€ข Divides code into multiple chunks
  • โ€ข Chunks loaded on demand at runtime
  • โ€ข Reduces initial JS parsed
  • โ€ข Dynamic import() triggers it
  • โ€ข The code IS in your bundle โ€” just deferred

Tree shaking

  • โ€ข Eliminates unused exports
  • โ€ข Happens at build time โ€” static analysis
  • โ€ข Reduces total bundle size
  • โ€ข Requires ESM (import/export)
  • โ€ข The dead code is NEVER in the bundle

How React.lazy works under the hood

js
1// React.lazy wraps a dynamic import
2const MyComponent = React.lazy(() => import('./MyComponent'));
3// โ†‘ This is equivalent to:
4ย 
5let loadedModule = null;
6let loadingPromise = null;
7ย 
8function MyComponent(props) {
9 if (!loadedModule) {
10 if (!loadingPromise) {
11 loadingPromise = import('./MyComponent');
12 }
13 throw loadingPromise; // Suspense catches this!
14 }
15 return loadedModule.default(props);
16}
17// When Suspense catches the thrown Promise, it shows the fallback.
18// When the Promise resolves, React retries rendering MyComponent.
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Splitting technique

Before

800KB
main.js
800KB

After

700KB
main.js
180KB
45KB (lazy)
product.js
220KB (lazy)
checkout.js
160KB (lazy)
account.js
95KB (lazy)
Total: 800KB โ†’ 700KB
Initial load: 180KB
lazy (loads on demand)
Loading strategy: on route change

Code example

js
1// React Router v6 with lazy()
2import { lazy, Suspense } from 'react';
3import { Routes, Route } from 'react-router-dom';
4ย 
5const Home = lazy(() => import('./pages/Home'));
6const Product = lazy(() => import('./pages/Product'));
7const Checkout = lazy(() => import('./pages/Checkout'));
8ย 
9function App() {
10 return (
11 <Suspense fallback={<PageSkeleton />}>
12 <Routes>
13 <Route path="/" element={<Home />} />
14 <Route path="/product/:id" element={<Product />} />
15 <Route path="/checkout" element={<Checkout />} />
16 </Routes>
17 </Suspense>
18 );
19}
20// Each page is a separate chunk โ€” only loaded on navigation
Gotcha: Route-level splitting creates a waterfall: user navigates โ†’ route bundle downloads โ†’ component mounts โ†’ data fetch starts. Preload the next likely route chunk while user is on the current page to hide this latency.
Insight: Route-level splitting is the highest ROI code-splitting technique. It's the natural boundary โ€” users don't visit all routes in one session. The initial bundle drops from 800KB to ~180KB, dramatically improving LCP.
Explored:๐Ÿ›ฃ๏ธ๐Ÿงฉ๐Ÿ“ฆ๐ŸŒณ๐Ÿ”ฌ
๐ŸŽฏ

Challenge

Explore all 5 code splitting patterns. Focus on the before/after bundle charts โ€” how much does each technique reduce the initial load? Which technique applies at build time vs runtime?

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: What is the difference between code splitting and tree shaking?

Tree shaking removes code that's NEVER used โ€” it's eliminated at build time and never shipped. Code splitting defers code that's not needed on initial load โ€” it IS shipped, just as a separate chunk loaded later. They complement each other: tree shaking reduces total bundle size, code splitting defers the remaining code. Both together give the smallest possible initial parse time.

Q: When does lazy loading hurt performance?

When it creates a network waterfall: route loads โ†’ route JS downloads โ†’ component mounts โ†’ data fetch starts. The user sees three sequential waits instead of one. Solution: preload the next route's chunk while the user is still on the current page. Use <link rel="modulepreload"> or the router's built-in prefetching (React Router's <Link prefetch>, Next.js automatic prefetching).

Bug: Declaring React.lazy inside a component

js
1// โœ— Declared inside component โ€” new lazy() on every render
2function Dashboard() {
3 const Chart = React.lazy(() => import('./Chart')); // BAD
4 return <Chart />;
5}
6ย 
7// โœ“ Declared at module level โ€” created once
8const Chart = React.lazy(() => import('./Chart'));
9ย 
10function Dashboard() {
11 return (
12 <Suspense fallback={<Spinner />}>
13 <Chart />
14 </Suspense>
15 );
16}
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Preloading chunks for zero-latency navigation

js
1// 1. rel="modulepreload" in HTML
2<link rel="modulepreload" href="/chunks/checkout.js" />
3ย 
4// 2. Programmatic preload on hover
5function ProductPage() {
6 const preloadCheckout = () => {
7 import('./CheckoutPage'); // triggers download, not render
8 };
9 return (
10 <button
11 onMouseEnter={preloadCheckout} // preload on hover
12 onClick={() => navigate('/checkout')}
13 >
14 Buy Now
15 </button>
16 );
17}
18ย 
19// 3. Next.js router.prefetch()
20const router = useRouter();
21router.prefetch('/checkout'); // preloads the route bundle

Named chunks for long-term caching

js
1// Without chunk name: chunk-abc123.js (hash changes every build)
2import('./HeavyComponent');
3ย 
4// With chunk name: chart-library.js (stable name โ†’ better caching)
5import(/* webpackChunkName: "chart-library" */ './ChartLibrary');
6ย 
7// Vite: use rollupOptions for manual chunks
8// vite.config.ts
9build: {
10 rollupOptions: {
11 output: {
12 manualChunks: {
13 'react-vendor': ['react', 'react-dom'],
14 'chart': ['recharts'],
15 'editor': ['@tiptap/react', '@tiptap/starter-kit'],
16 },
17 },
18 },
19}
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

Tree shaking eliminates dead code at build time; code splitting defers live code into separate chunks loaded on demand at runtime.

React.lazy wraps a dynamic import(); when the component renders before the chunk loads, it throws the Promise โ€” Suspense catches it and shows the fallback until the Promise resolves.

Each render creates a new lazy() wrapper and a new import() call, defeating caching and re-triggering the loading state on every render.

Without preloading, route navigation triggers: route chunk download โ†’ mount โ†’ data fetch โ€” three sequential waits. Preload the chunk while the user is on the previous page.

Tree shaking requires static import/export analysis at build time; CommonJS require() is dynamic and can run any code path at runtime, making static analysis impossible.

Run webpack-bundle-analyzer or rollup-plugin-visualizer, look for unexpectedly large dependencies (moment.js, full icon libraries, duplicate React), then replace or lazy-load them.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

Which package should you use instead of `lodash` to get tree-shaking support?