Your app ships one 800KB JS bundle and time-to-interactive is poor. How does code-splitting help, and where do you split?
Understanding that you should ship only the code a given screen needs, and the mechanics of doing so.
A single bundle forces every visitor to download, parse, and execute ALL your code before anything is interactive — even code for routes they'll never visit. Parsing and executing JS is CPU work that blocks the main thread, so a big bundle hurts time-to-interactive and INP especially on cheap phones. Code-splitting breaks the bundle into chunks loaded on demand, so the initial download is just what the first screen needs. The natural seams: (1) Per route — the single highest-leverage split. Lazy-load each route's component so visiting /settings fetches settings code only when navigated to. (2) Below-the-fold or interaction-gated components — a heavy chart, a rich editor, a modal — load when they're about to be shown, not on initial load. (3) Large third-party libraries used in one place — dynamically import the date-picker or the markdown renderer at the point of use. The mechanism is the dynamic `import()` expression, which returns a promise and tells the bundler to emit a separate chunk. Frameworks wrap this: React.lazy + Suspense render a fallback while the chunk loads. Two caveats: don't over-split (each chunk is a request and a potential waterfall — splitting a 2KB component is pure overhead), and prefetch likely-next chunks during idle time (e.g., <link rel=prefetch> or on hover) so the lazy load doesn't cost a visible delay when the user actually navigates. The goal is a small, fast initial bundle plus just-in-time loading of the rest.
Cutting initial bundle size; improving TTI/INP on a JS-heavy app.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Each route becomes its own chunk, fetched only when visited
const Dashboard = lazy(() => import('./routes/Dashboard'));
const Settings = lazy(() => import('./routes/Settings'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Interaction-gated: load a heavy editor only when opened
async function openEditor() {
const { RichEditor } = await import('./RichEditor'); // separate chunk
mount(RichEditor);
}