DNA🚀 PerformanceBundle Optimization & Code Splitting
ðŸĶ–DinosaurPerformanceBuild ToolsOptimization

Bundle Optimization & Code Splitting

Every kilobyte you ship is parse time, compile time, and execution time on the user's device. Bundle optimization is the discipline of shipping less.

Bundle Optimization & Code Splitting

The average website ships 500KB+ of JavaScript. On a mid-range phone over 3G, that's 15+ seconds before interactive. Bundle optimization isn't about saving bytes — it's about respecting your users' time and hardware.

The Cost of JavaScript

JavaScript is uniquely expensive compared to other resources of equal size:

200KB image:   Download → Decode → Paint     (GPU-assisted, fast)
200KB JS:      Download → Parse → Compile → Execute    (main thread, blocking)

A 200KB JavaScript bundle takes 3-4x longer to process than a 200KB image on the same device.

Code Splitting Strategies

Route-Based Splitting (Highest Impact)

Every route becomes a separate chunk:

import { lazy, Suspense } from 'react';
 
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Products = lazy(() => import('./pages/Products'));
const Settings = lazy(() => import('./pages/Settings'));
 
function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/products" element={<Products />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Next.js App Router does this automatically — each page.tsx is a separate chunk.

Feature-Based Splitting

Heavy features that aren't always used:

const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartDashboard = lazy(() => import('./components/ChartDashboard'));
const PDFExporter = lazy(() => import('./components/PDFExporter'));
 
function PostEditor() {
  return (
    <Suspense fallback={<EditorSkeleton />}>
      {mode === 'edit' && <RichTextEditor />}
    </Suspense>
  );
}

Conditional Splitting (Role/Feature Flag)

async function loadAdminTools() {
  if (!user.isAdmin) return null;
  const { AdminPanel } = await import('./admin/AdminPanel');
  return AdminPanel;
}

Prefetch on Hover/Focus

function NavLink({ to, children }) {
  const prefetch = useCallback(() => {
    import(`./pages/${to}`);
  }, [to]);
 
  return (
    <Link to={to} onMouseEnter={prefetch} onFocus={prefetch}>
      {children}
    </Link>
  );
}

By the time the user clicks, the chunk is likely cached.

Tree Shaking

Tree shaking removes dead code — exports that are never imported. It requires ES modules (static import/export):

// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function complexAnalysis(data) { /* 2000 lines */ }
 
// app.js
import { add } from './math.js';
// subtract and complexAnalysis are tree-shaken out

Why Tree Shaking Fails

Side effects:

// polyfill.js — has side effects (modifies globals)
import './polyfill.js'; // Bundler can't remove this
 
// package.json — tell bundler what's safe to shake
{
  "sideEffects": false
}
// or specify files with side effects:
{
  "sideEffects": ["*.css", "./src/polyfills.js"]
}

Barrel files:

// components/index.js (barrel)
export { Button } from './Button';
export { Modal } from './Modal';       // 15KB
export { DataTable } from './DataTable'; // 80KB
export { ChartWidget } from './Chart';   // 120KB
 
// Importing Button may pull in ALL of them if bundler
// can't trace through the barrel
import { Button } from './components';
 
// Safer: direct imports
import { Button } from './components/Button';

CommonJS modules:

// ❌ Not tree-shakeable — dynamic, runtime resolution
const { pick } = require('lodash');
 
// ✅ Tree-shakeable — static, compile-time analysis
import { pick } from 'lodash-es';
 
// ✅ Direct import (always works)
import pick from 'lodash/pick';

Import Cost Awareness

LibraryFull ImportOptimized ImportSavings
lodash~72KBlodash-es or direct~90%
date-fns~80KBDirect function imports~95%
moment~67KBUse dayjs (2KB)~97%
rxjs~47KBrxjs/operators~80%
// ❌ Imports entire library
import { format } from 'date-fns'; // Pulls in more than needed
 
// ✅ Direct import
import format from 'date-fns/format'; // Just the format function

Compression

Original JavaScript:  500KB
Minified (Terser):    200KB (60% reduction)
Gzip compressed:      55KB  (89% total reduction)
Brotli compressed:    45KB  (91% total reduction)

Brotli is 15-25% smaller than Gzip. Both are supported by all modern browsers over HTTPS.

Enabling Brotli

# Nginx
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json;

Bundle Analysis

You can't optimize what you can't see:

# Next.js
ANALYZE=true npx next build
 
# Webpack
npx webpack-bundle-analyzer stats.json
 
# Vite
npx vite-bundle-visualizer

What to Look For

  1. Duplicate dependencies — Two versions of the same library
  2. Surprisingly large modules — A "small utility" pulling in 100KB
  3. Client-only code in server bundles (and vice versa)
  4. Unused code — Large libraries where you use 5% of the exports
  5. Uncompressed comparison — Transfer size vs parsed size

Dynamic Import Patterns

Library-On-Demand

async function handleExport() {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();
  doc.text('Report', 10, 10);
  doc.save('report.pdf');
}
 
async function showChart(data) {
  const { Chart } = await import('chart.js/auto');
  new Chart(canvas, { type: 'line', data });
}

Feature Detection Loading

async function initializeAnalytics() {
  if (window.location.hostname === 'localhost') return;
 
  const { initGA } = await import('./analytics/google');
  const { initSentry } = await import('./analytics/sentry');
  initGA(config.gaId);
  initSentry(config.sentryDsn);
}

PRPL Pattern

PRPL is a loading strategy that maximizes performance on constrained devices:

LetterStepImplementation
PushPush critical resources for initial route<link rel="preload">, HTTP/2 push
RenderRender initial route ASAPInline critical CSS, SSR/SSG
Pre-cachePre-cache remaining routesService Worker caches other route chunks
Lazy-loadLazy-load remaining routes on demandimport() on navigation
First Load:
  Push: hero.css, app.js, route-home.js (preload)
  Render: Show home page immediately
 
Background:
  Pre-cache: route-about.js, route-products.js (Service Worker)
 
On Navigation:
  Lazy-load: Already cached → instant transition

List Virtualization

For long lists (1000+ items), only render what's visible in the viewport:

import { useVirtualizer } from '@tanstack/react-virtual';
 
function VirtualList({ items }) {
  const parentRef = useRef(null);
 
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });
 
  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: virtualItem.start,
              height: virtualItem.size,
              width: '100%',
            }}
          >
            {items[virtualItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Impact: 10,000 items → 20-30 DOM nodes. Eliminates layout thrashing and memory bloat.

Import-On-Visibility / Import-On-Interaction

Load code only when the user can see or interacts with the component:

// Import on visibility (uses IntersectionObserver)
function LazyChart({ data }) {
  const [Chart, setChart] = useState(null);
  const ref = useRef(null);
 
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        import('chart.js/auto').then(m => setChart(() => m.Chart));
        observer.disconnect();
      }
    });
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
 
  return <canvas ref={ref} />;
}
 
// Import on interaction
function CommentSection() {
  const [Editor, setEditor] = useState(null);
 
  const handleFocus = () => {
    if (!Editor) {
      import('./RichTextEditor').then(m => setEditor(() => m.default));
    }
  };
 
  return Editor
    ? <Editor />
    : <textarea onFocus={handleFocus} placeholder="Write a comment..." />;
}

Performance Budget Enforcement

{
  "budgets": [
    {
      "resourceType": "script",
      "budget": 200,
      "unit": "kb"
    },
    {
      "resourceType": "total",
      "budget": 500,
      "unit": "kb"
    },
    {
      "metric": "first-contentful-paint",
      "budget": 1500,
      "unit": "ms"
    }
  ]
}

Integrate into CI:

  • Bundle size check on every PR
  • Lighthouse CI with performance budgets
  • Alert when any chunk exceeds the threshold

The Optimization Checklist

  1. Analyze — Run bundle analyzer, identify largest chunks
  2. Split routes — Each route is a separate chunk
  3. Audit imports — Replace heavy libraries, use direct imports
  4. Lazy-load features — Charts, editors, PDFs, maps — on demand
  5. Check barrel files — Ensure they don't prevent tree shaking
  6. Enable compression — Brotli > Gzip
  7. Set budgets — Fail CI on violations
  8. Monitor — Track bundle size over time, catch creep early

Interview Signal

Senior candidates demonstrate:

  1. Cost awareness — JS costs more than equal-size images to process
  2. Strategy hierarchy — Route splitting first, then feature splitting, then micro-optimization
  3. Tree shaking knowledge — Why ESM is required, what defeats tree shaking
  4. Tooling fluency — Bundle analysis, Lighthouse CI, performance budgets
  5. Measurement discipline — Analyze before optimizing, monitor after shipping