Ship Less JavaScript

Your app downloads its whole codebase before a single pixel renders: every page, every admin screen, every heavy third-party widget, in the same bundle, because nothing was ever split into separate chunks. AI assistants are great at making a new route "just work" and bad at thinking about who ends up downloading it. Here is how to find the damage and fix it properly, not just for the routes that were easy to spot.

Watch for the halfway fix. A codebase that lazy-loads legal pages and admin but leaves the dashboard, search, and anything with a calendar or chart library as static imports has split the easy 20% and left the expensive 80% untouched. Check which routes are lazy, not just whether any are.

Paste into your AI chat

Copy the whole guide as markdown to use as context in Cursor, Claude, ChatGPT, or any AI tool.

What it looks like

A router file where every page, public or private, light or heavy, is a plain top-level import.

One big eager bundle

Every import in the router gets bundled into (or very close to) the same entry chunk. A visitor who only ever looks at the landing page and the pricing page still pays the download cost for the calendar widget, the video editor, and the admin charting library.

tsx
// src/App.tsx (everything loads eagerly, in one bundle)
import { BrowserRouter, Routes, Route } from 'react-router-dom';

import LandingPage from './pages/LandingPage';
import BookingPage from './pages/BookingPage';       // pulls in a full calendar library
import DashboardPage from './pages/DashboardPage';    // the entire authenticated app
import AdminPage from './pages/AdminPage';             // charts, tables, rarely visited

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<LandingPage />} />
        <Route path="/booking" element={<BookingPage />} />
        <Route path="/dashboard" element={<DashboardPage />} />
        <Route path="/admin" element={<AdminPage />} />
      </Routes>
    </BrowserRouter>
  );
}

The sneaky version: lazy() on the wrong routes

A codebase can already use lazy() for some routes, usually the obvious, low-traffic ones like legal pages, help docs, and admin, and still leave the actual heaviest routes (the main dashboard, a detail view, search) as static imports because they "already worked" before code-splitting was introduced and nobody circled back. The file looks fixed at a glance, since you can see lazy() calls right there, but the biggest chunks never actually got split out.

Spotting it in a diff or a review

Compare the ratio of plain import X from lines to lazy(() => import(...)) lines in the router, then check which routes are which. A diff that lazy-loads five legal and help pages but statically imports the dashboard, the search results page, and anything pulling in a calendar or chart library has fixed the easy 20% and left the expensive 80% untouched.

Why AI tools generate this

Static imports are the path of least resistance, and nothing in the editor tells the assistant it just made the app slower.

What it costs

Slower Core Web Vitals, mobile visitors paying the biggest price, and eager routes with no crash recovery.

How to measure it in your app

Run a bundle analyzer against the production build, check DevTools Coverage, and read the Lighthouse report.

The fix

Convert routes to React.lazy(), pair every lazy boundary with Suspense and an error boundary, and split heavy vendor libraries into their own chunk.

A representative before and after

Illustrative numbers, not one specific audited project, but the shape repeats across almost every unsplit React app.

MetricBeforeAfter
Entry chunk, gzipped~480KB~150-220KB
Time to Interactive, throttled 4G~4.5s~1.8s
Routes imported eagerly at the entry point122 (landing + login)
Heavy third-party libraries in the entry chunk3 (calendar, charts, editor)0, loaded on demand

The exact numbers depend entirely on your dependencies. The pattern does not: a small, always-needed core plus on-demand chunks for everything else.

Checklist

Work through this before you call the routing layer done.

Audit

  • Run a bundle analyzer against the production build and identify the entry/main chunk size
  • List every top-level route import in your router file

Split and guard

  • Convert every route except the true "always needed on cold load" ones (landing, login) to lazy() / next/dynamic
  • Wrap every lazy route in Suspense with a real loading fallback, not a blank screen
  • Wrap every lazy route in an error boundary, and audit whether your eager routes have one too
  • Split large, stable third-party libraries (calendar, charts, editors) into their own vendor chunk

Verify

  • Re-run the bundle analyzer and confirm the entry chunk shrank and heavy libraries moved to on-demand chunks
  • Re-run Lighthouse and confirm LCP / TBT improved on an anonymous, logged-out page load
  • Re-check this after every few feature additions, since bundle bloat is incremental and easy to miss one PR at a time

Earlier in this track

Start with the design principles that keep pages fast before the bundle even loads.

Browser-Aware Web Design

Designing pages that work with the browser's rendering pipeline instead of against it: LCP, compositor-friendly motion, and layout stability.

Want the steps as context for your AI coding assistant?