The fastest JavaScript is the JavaScript you didn't send. Splitting ships only what the current page needs.
Dynamic import — the primitive
// chart library (180KB) loads ONLY when the user opens the stats tab
statsTab.addEventListener("click", async () => {
const { renderChart } = await import("./charts.js");
renderChart(data);
});Vite automatically turns every dynamic import into a separate file fetched on demand.
Route-based splitting (React)
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./pages/Dashboard"));
<Suspense fallback={<Skeleton />}>
<Route path="/dashboard" element={<Dashboard />} />
</Suspense>
// visitors to the homepage never download dashboard codeFinding the bloat
npx vite-bundle-visualizer # treemap of what's in your bundle
The usual suspects: a whole date library for one format call (use Intl), lodash imported wholesale, an icon pack with 5000 icons for the 12 you use.
The checklist
- Split at routes first (biggest win, zero risk)
- Split heavy widgets behind interaction (charts, editors, maps)
- Preload on hover for perceived speed:
link.onmouseenter = () => import("./page.js") - Budget: initial JS under ~150KB gzipped for content sites