DNA🚀 PerformanceBundle Optimization
ðŸĶ–DinosaurPerformanceBuild ToolsOptimization

Bundle Optimization

Your users download your JavaScript. Every kilobyte matters. Code splitting, tree shaking, and lazy loading are how senior engineers keep bundles lean.

Bundle Optimization

The average website ships over 500KB of JavaScript. On a 3G connection, that's 10+ seconds of parsing and execution before the page is interactive. Bundle optimization isn't premature optimization — it's table stakes.

Code Splitting

Code splitting breaks your bundle into chunks that load on demand instead of all upfront.

Route-Based Splitting

The highest-impact split — each route loads its own code:

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

Component-Based Splitting

For heavy components that aren't always visible:

const MarkdownEditor = lazy(() => import('./components/MarkdownEditor'));
const ChartLibrary = lazy(() => import('./components/ChartDashboard'));
 
function PostEditor({ mode }: { mode: 'write' | 'preview' }) {
  return (
    <Suspense fallback={<EditorSkeleton />}>
      {mode === 'write' ? <MarkdownEditor /> : <Preview />}
    </Suspense>
  );
}

Prefetching Splits

Load chunks before the user needs them:

const Settings = lazy(() => import('./pages/Settings'));
 
function NavLink() {
  const prefetch = () => import('./pages/Settings');
 
  return (
    <Link
      to="/settings"
      onMouseEnter={prefetch}
      onFocus={prefetch}
    >
      Settings
    </Link>
  );
}

On hover or focus, the chunk starts downloading. By the time the user clicks, it's likely already cached.

Tree Shaking

Tree shaking eliminates dead code — exports that are imported but never used. It requires ES modules (import/export), not CommonJS (require).

// math.js — ES module
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function complexMatrix(data) { /* 500 lines */ }
 
// app.js — only `add` is used
import { add } from './math';
// multiply and complexMatrix are tree-shaken out

Tree Shaking Pitfalls

Side effects prevent tree shaking:

// This import has a side effect — it modifies a global
import './polyfills'; // bundler can't remove this
 
// Mark packages as side-effect-free in package.json
{
  "sideEffects": false
}
// Or specify which files have side effects
{
  "sideEffects": ["*.css", "./src/polyfills.js"]
}

Barrel files can defeat tree shaking:

// components/index.js — barrel file
export { Button } from './Button';
export { Modal } from './Modal';
export { DataTable } from './DataTable'; // 50KB
 
// If your bundler can't trace through the barrel, importing Button
// may pull in DataTable too. Direct imports are safer:
import { Button } from './components/Button';

Lazy Loading Beyond Components

Dynamic Imports for Libraries

async function generatePDF(data: ReportData) {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();
  doc.text(data.title, 10, 10);
  doc.save('report.pdf');
}

The PDF library (200KB+) only loads when the user actually exports. This keeps the initial bundle lean.

Intersection Observer for Below-the-Fold

function LazySection({ children }: { children: ReactNode }) {
  const [visible, setVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) setVisible(true); },
      { rootMargin: '200px' }
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
 
  return <div ref={ref}>{visible ? children : <Placeholder />}</div>;
}

Analysis Tools

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

webpack-bundle-analyzer

npx webpack-bundle-analyzer dist/stats.json

Generates a treemap showing exactly what's in your bundle. Look for:

  • Duplicate dependencies (two versions of lodash)
  • Unexpectedly large packages
  • Packages that should be lazy-loaded

import cost (VS Code extension)

Shows the gzipped size of imports inline. Catch heavy imports before they ship.

Lighthouse CI

# .lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "resource-summary:script:size": ["error", { "maxNumericValue": 200000 }],
        "total-byte-weight": ["warn", { "maxNumericValue": 500000 }]
      }
    }
  }
}

Optimization Checklist

  1. Analyze first — run bundle analyzer before making changes
  2. Route-split — every route should be a separate chunk
  3. Audit dependencies — replace heavy libraries with lighter alternatives (date-fns over moment, clsx over classnames)
  4. Check barrel files — ensure they don't prevent tree shaking
  5. Lazy-load heavy features — charts, editors, PDF generators, maps
  6. Set budgets — fail CI when bundle size exceeds thresholds
  7. Monitor over time — bundle size creep is invisible without tracking

Interview Signal

Bundle optimization questions test engineering discipline:

  1. Measurement-first mindset — analyzing before optimizing, not guessing
  2. Trade-off awareness — more splits mean more network requests; there's an optimal granularity
  3. Tooling fluency — knowing how to configure splitting, analyze output, and set up CI budgets
  4. Impact prioritization — route-level splitting first, then heavy components, then micro-optimizations