# Frontend Performance: Code Splitting and Lazy Loading

Source: https://www.techinterview.org/post/3233475191/frontend-performance-code-splitting-lazy-loading/
Updated: 2026-07-12 · techinterview.org

Code splitting is the highest-impact performance optimization for modern web apps. The wrong bundling strategy ships 5MB of JavaScript on the first page; the right one ships 200KB and lazy-loads the rest. Senior frontend interviews probe whether you understand the patterns and know when to apply each.

## Why code splitting

- First page does not need the entire app's code

- Less code to download = faster Time to Interactive

- Parser/compiler work decreases with smaller bundles

- Better caching: smaller chunks change less often

## Levels of splitting

### Route-based splitting

Each route is its own chunk. Going to /dashboard does not download /admin code.

React: `const Dashboard = lazy(() => import('./Dashboard'))`

Next.js, Remix, React Router 7: built-in

### Component-level splitting

Heavy components (chart libraries, rich text editors, video players) loaded on demand.


```
const HeavyEditor = lazy(() => import('./HeavyEditor'));
<Suspense fallback={<Spinner />}>
  <HeavyEditor />
</Suspense>
```


### Vendor splitting

Separate vendor (npm packages) from your app code:

- Vendor chunk caches longer (rarely changes)

- App code changes frequently; cache misses are smaller

## Tree shaking

Remove unused exports at build time. Requirements:

- ES modules (not CommonJS)

- Side-effect free (mark in package.json)

- Modern bundler (Vite, Rollup, Webpack 4+)

Without tree shaking, importing one function from lodash bundles the entire library.

## Dynamic imports

Triggered on user action:


```
button.addEventListener('click', async () => {
  const { showModal } = await import('./modal');
  showModal();
});
```


Modal code only loads when needed.

## Preloading and prefetching

Hint the browser to fetch resources before they are needed:

- `<link rel="preload">`: this resource is needed soon (high priority)

- `<link rel="prefetch">`: maybe needed (low priority)

- `<link rel="modulepreload">`: ES module preload

Use case: user is likely to navigate to /dashboard next; prefetch its bundle while they are on /home.

## Critical rendering path

Optimize what blocks first paint:

- Inline critical CSS in HTML

- Defer non-critical CSS

- Async/defer non-critical JS

- Server-render the HTML for above-the-fold content

## Bundle analysis

Always run a bundle analyzer:

- vite-bundle-visualizer

- rollup-plugin-visualizer

- webpack-bundle-analyzer

Look for: unexpected libraries, duplicates, oversized dependencies.

## The "everything is dynamic" antipattern

Some engineers split everything. The result: many tiny chunks, each adding HTTP overhead. Net slower.

Sweet spot: 5–15 chunks for typical apps. Vendor + app + a few large feature chunks.

## Avoiding waterfall loading

Bad pattern: chunk A imports chunk B; loading A triggers loading B; B imports C; etc. Sequential network dependencies.

Counter:

- Use modulepreload for chained imports

- Use HTTP/2 or HTTP/3 (multiplexing)

- Bundle related modules together

## Server-side rendering and chunks

SSR + code splitting: hydration must include the same chunks as the server rendered. Frameworks (Next.js, Remix) handle this automatically.

## Common antipatterns

- One massive bundle (no splitting)

- Splitting too aggressively (waterfall)

- Not preloading critical chunks

- Cache-busting wrong (vendor chunk re-downloads on every deploy)

- Lazy-loading above-the-fold content

## Frequently Asked Questions

### How small should chunks be?

50–200KB compressed is a good range. Below 50KB, HTTP overhead dominates. Above 500KB, parsing becomes slow on mobile.

### Should I split CSS too?

Yes. Use CSS-in-JS or scoped CSS that ship with their components. Avoid one massive global CSS file.

### What about for SEO?

Server-render the first page. Code-split the rest. Crawlers see the rendered HTML; users get fast hydration.
