The average website ships 400KB of JavaScript, but only 100-200KB executes during page load. That unused code still downloads, parses, and compiles—wasting time before your LCP can paint.
Lighthouse flags scripts with more than 20KB of unused bytes. This happens when you bundle code for features the current page doesn't need: routes the user hasn't visited, components that only appear on interaction, or library functions you never call.
Modern bundlers make it easy to ship everything in one file. Convenient for developers, expensive for users. A visitor to your homepage downloads JavaScript for your checkout flow, admin panel, and every product page—even though they'll likely never see most of it.
Why it hurts LCP: JavaScript is render-blocking by default and expensive even when deferred. The browser must download, parse, and compile all JavaScript before it can execute. Parse and compile time scales with file size regardless of whether the code runs. On mobile devices, parsing 1MB of JavaScript can take 2-4 seconds of main thread time.
Look for the audit "Reduce unused JavaScript" under Opportunities. It shows:
Lighthouse ignores scripts with less than 20KB unused—below that threshold, the overhead of splitting isn't worth it.
The Coverage tab shows exactly which bytes execute during page load.
Reading the results:
Pro workflow:
Visualize what's in your bundles:
npx webpack-bundle-analyzer stats.json
npx vite-bundle-visualizer
npx rollup-plugin-visualizer
Look for:
Split your bundle by route so each page loads only what it needs.
React Router:
import { lazy, Suspense } from 'react'
import { Route, Routes } from 'react-router-dom'
// Dynamic imports create separate chunks
const Home = lazy(() => import('./pages/Home'))
const Product = lazy(() => import('./pages/Product'))
const Checkout = lazy(() => import('./pages/Checkout'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:id" element={<Product />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</Suspense>
)
}
Vue Router:
// router.js
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/product/:id',
component: () => import('./views/Product.vue')
},
{
path: '/checkout',
component: () => import('./views/Checkout.vue')
}
]
Lazy load heavy components that don't appear immediately.
// React
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
const [showChart, setShowChart] = useState(false)
return (
<div>
<button onClick={() => setShowChart(true)}>Show Analytics</button>
{showChart && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</div>
)
}
<!-- Vue -->
<script setup>
import { defineAsyncComponent, ref } from 'vue'
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
const showChart = ref(false)
</script>
<template>
<button @click="showChart = true">
Show Analytics
</button>
<HeavyChart v-if="showChart" />
</template>
Tree shaking removes unused exports, but only works with ES modules.
Enable ES modules in your library imports:
// Bad: imports entire library
import _ from 'lodash'
_.debounce(fn, 300)
// Good: imports only what you use
import debounce from 'lodash/debounce'
debounce(fn, 300)
// Best: use lodash-es for full tree shaking
import { debounce } from 'lodash-es'
Verify tree shaking works in your bundler:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: undefined // Let Rollup tree shake freely
}
}
}
}
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // Mark unused exports
sideEffects: true, // Enable tree shaking
}
}
Mark your package as side-effect free:
// package.json
{
"sideEffects": false
}
Or specify which files have side effects:
{
"sideEffects": ["*.css", "./src/polyfills.js"]
}
Swap large libraries for lighter alternatives.
| Heavy Library | Size | Lighter Alternative | Size |
|---|---|---|---|
| moment | 300KB | date-fns (tree-shakeable) | 13KB used |
| moment | 300KB | dayjs | 2KB |
| lodash | 70KB | lodash-es (tree-shakeable) | varies |
| axios | 13KB | native fetch | 0KB |
| jQuery | 90KB | native DOM | 0KB |
| chart.js | 200KB | uPlot | 35KB |
// After: dayjs (2KB)
import dayjs from 'dayjs'
// Before: moment (300KB)
import moment from 'moment'
moment().format('YYYY-MM-DD')
dayjs().format('YYYY-MM-DD')
Use coverage data to find and eliminate unused code.
npx ts-prune
# src/utils/legacy.ts:15 - unusedFunction
# src/components/OldButton.tsx:1 - default
Review and delete files that Coverage shows as 100% unused.
Less JavaScript means:
Parse and compile time particularly impact mobile devices. V8 (Chrome's engine) can parse JavaScript at roughly 1MB/second on a mid-range phone. A 500KB bundle takes 500ms of main thread time just to parse—before any code executes.
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <Spinner />,
ssr: false // Skip server rendering for client-only components
})
ANALYZE=true npm run build
next.config.js:const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({})
<script setup>
// Lazy-loaded component (automatic code splitting)
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
</script>
Lazy prefix for automatic async loading:<template>
<!-- Automatically lazy-loaded -->
<LazyHeavyChart v-if="showChart" />
</template>
npx nuxi analyze
// Async component with loading state
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
errorComponent: ErrorDisplay
})
After implementing:
Targets:
Quick validation:
Total JS: 500KB (340KB unused)
Initial JS: 150KB (30KB unused)
Route chunks: load on navigation
Often appears alongside:
Different pages have different JavaScript requirements—your homepage might be optimized while product pages load unused checkout code. Unlighthouse analyzes every page and shows exactly which routes have the most unused JavaScript.
Unminified JavaScript
How to minify JavaScript files to reduce payload sizes, improve parse time, and accelerate your Largest Contentful Paint.
Render-Blocking Resources
Eliminate render-blocking CSS and JavaScript to improve LCP. Learn async/defer scripts, critical CSS extraction, and font loading strategies.