# Large JavaScript bundles

> Oversized JS bundles delay interactivity and waste mobile data. Code-split by route, tree-shake imports, and lazy-load heavy components.

_Category: Performance · Detector `resource-bloat` · Severity: warning_

Every kilobyte of JavaScript has to be downloaded, parsed, compiled, and executed before the page is interactive — and on a mid-range phone that work is many times slower than on your laptop. When one bundle ships the whole app up front, first interaction is delayed for everyone. AI-built apps balloon here by importing entire libraries for one helper and never code-splitting by route.

## Symptoms

- A single .js chunk is hundreds of KB or more in the Network panel
- Slow time-to-interactive; the page looks ready but doesn’t respond yet
- The whole app’s code loads on the first route, before it’s needed
- A bundle analyzer shows one or two dependencies dominating the graph

## How VibeCheck detects it

The `resource-bloat` detector flags this live in the browser and reports it to the widget's Problems list — and to your coding agent over MCP.

- **Issue string:** `Large JS resource (250KB)`
- **Threshold:** ≥ 100KB transferred per JS/CSS resource in production (≥ 500KB in dev)

## Root causes

- No route-level code splitting — the entire app ships in one chunk
- Barrel/whole-library imports that defeat tree-shaking
- Heavy components (editors, charts, maps) bundled into the initial load
- Duplicate copies of a dependency at different versions

## The fix

Split code so each route and heavy component loads on demand, import only what you use so tree-shaking can drop the rest, and run a bundle analyzer to find the biggest offenders. Ensure the server sends gzip/brotli-compressed assets.

### Steps

1. Run a bundle analyzer to identify the largest chunks and dependencies
2. Code-split by route and lazy-load heavy, non-critical components
3. Replace whole-library imports with named/deep imports and remove duplicates

_Import only what you use (tree-shakeable)_

```js
// ✗ pulls the whole library into the bundle
import _ from 'lodash'
// ✓ only debounce ships
import debounce from 'lodash-es/debounce'
```

## Framework-specific fixes

### React

Split at route and component boundaries with `React.lazy` + `Suspense`, and analyze the build with `rollup-plugin-visualizer` (Vite) to see what’s big.

```tsx
import { lazy, Suspense } from 'react'
const Editor = lazy(() => import('./Editor')) // its own chunk, loaded on demand

<Suspense fallback={<Skeleton />}>
  <Editor />
</Suspense>
```

[React docs](https://react.dev/reference/react/lazy)

### Next.js

Next code-splits per route automatically; use `next/dynamic` for heavy client components and `@next/bundle-analyzer` to inspect chunks. Prefer Server Components so code never reaches the client.

```tsx
import dynamic from 'next/dynamic'
const Map = dynamic(() => import('./Map'), { ssr: false, loading: () => <Skeleton /> })
```

[Next.js docs](https://nextjs.org/docs/app/guides/lazy-loading)

### Vue

Use `defineAsyncComponent` for on-demand components and Vue Router’s dynamic imports for route-level splitting.

```ts
import { defineAsyncComponent } from 'vue'
const Editor = defineAsyncComponent(() => import('./Editor.vue'))

// route-level split:
const routes = [{ path: '/edit', component: () => import('./pages/Edit.vue') }]
```

[Vue docs](https://vuejs.org/guide/components/async.html)

### Vanilla JS

Use dynamic `import()` to load heavy modules only when needed; modern bundlers split them into separate chunks automatically.

```js
button.addEventListener('click', async () => {
  const { openEditor } = await import('./editor.js') // separate chunk
  openEditor()
})
```

## FAQ

### Why is the threshold higher in development?

Dev bundles are unminified and include source maps and HMR runtime, so they’re legitimately large. VibeCheck raises the bar to 500KB on localhost and uses 100KB for production, where the size actually ships to users.

### What’s the fastest win?

Route-level code splitting. It ensures a user landing on one page doesn’t download the code for every other page. After that, lazy-load the heaviest individual components (editors, charts, maps).

### How do I find what’s making the bundle big?

Run a bundle analyzer (`@next/bundle-analyzer`, `rollup-plugin-visualizer`, or `source-map-explorer`). It shows a treemap of every module so you can spot an oversized dependency or an accidental whole-library import.

## Related problems

- [Heavy dependencies](https://vibecheck.wcgw.fun/fix/heavy-dependencies.md) — Performance
- [Long tasks blocking the main thread](https://vibecheck.wcgw.fun/fix/long-tasks.md) — Performance
- [Duplicate network requests](https://vibecheck.wcgw.fun/fix/duplicate-network-requests.md) — Performance
- [Excessive DOM size](https://vibecheck.wcgw.fun/fix/excessive-dom-size.md) — Performance

---

Fix guide from VibeCheck — https://vibecheck.wcgw.fun/fix/large-javascript-bundles. Full site index for LLMs: https://vibecheck.wcgw.fun/llms.txt
