Page speed, redirects, Core Web Vitals
Remove unused JavaScript to cut bundle size and TBT
Unused JavaScript bloats bundles, blocks the main thread, and tanks INP. Code-split, tree-shake, and lazy-load to ship only the code each route actually needs.
What's happening
Lighthouse's "Reduce unused JavaScript" audit flags chunks where most bytes never run. A typical SPA in 2026 ships 800KB-2MB of JS, of which 60-80% is unused on the landing route. JavaScript is more expensive than CSS — it has to be downloaded, parsed, compiled, and executed, and each step is CPU-bound on the main thread.
Chrome DevTools' Coverage tab tracks JS execution alongside CSS. The Performance panel's Bottom-Up tab shows which scripts spent the most main-thread time. Lighthouse's TBT (Total Blocking Time) score directly correlates with how much JS the page parses and executes during load.
The leading cause is importing whole libraries when you need one function: import _ from "lodash" pulls in 70KB when you only used.debounce. Component libraries without tree-shaking, polyfills shipped to modern browsers, and forgotten A/B testing scripts compound the problem.
Why it matters
Unused JavaScript directly hurts INP through main-thread blocking and TBT. It also slows FCP and LCP because parsing big bundles steals CPU from rendering. Pages with 1MB+ of compressed JS routinely fail Core Web Vitals at the 75th percentile in CrUX.
Beyond Core Web Vitals, JS bundle size affects every subsequent navigation in an SPA. Every route adds more JS to the cumulative parse cost, and mid-tier Android phones can spend 5-10 seconds parsing and executing on cold load. Bounce rates climb sharply when total blocking time exceeds 600ms.
Common causes
- Importing whole libraries (lodash, moment) instead of specific functions.
- Component libraries without tree-shaking (sideEffects: false missing in package.json).
- Polyfills and transpilation targeting browsers that don't need them.
- Routes that import all SPA pages eagerly instead of lazy-loading them.
- Third-party scripts (analytics, A/B testing, chat widgets) loaded for every visitor.
- Build configurations missing production minification or dead-code elimination.
- Server-rendered pages that ship the full client bundle even when no interactivity is needed.
Detect this on your site
Run a quick scan with the Speed Test. The tool surfaces this exact issue with the records and context needed to apply the fix below.
Open Speed TestHow to fix it
- 1
Audit with Coverage and Bundle Analyzer
Use Chrome DevTools' Coverage tab to find JS files with low usage. Run @next/bundle-analyzer (or webpack-bundle-analyzer) to visualize what's actually in your bundles. Look for big surprise dependencies — that's where the wins are.
- 2
Replace whole-library imports with named imports
Change import _ from "lodash" to import debounce from "lodash/debounce" or migrate to lodash-es with proper tree-shaking. For date utilities, replace moment (300KB) with date-fns or Day.js (under 10KB). Many libraries publish ESM builds that tree-shake automatically.
- 3
Code-split by route
In Next.js App Router, every route is automatically code-split. For client-only components inside a route, use next/dynamic with ssr: false to lazy-load on first interaction. React.lazy + Suspense achieves the same in plain React.
- 4
Lazy-load heavy components
Charts, rich-text editors, video players, and code editors are obvious lazy-load candidates. Wrap them in dynamic() with a placeholder skeleton. The user clicks a tab → JS arrives → editor mounts. Initial bundle drops by hundreds of KB.
- 5
Move third-party scripts to a Web Worker
Partytown relocates analytics, tag managers, and tracking scripts into a Web Worker. The main thread stays free for your app. Vercel and Cloudflare have first-class Partytown integrations.
- 6
Configure browserslist for modern targets
If your audience is evergreen browsers, set browserslist to last 2 versions, not dead, > 0.5%. Babel and SWC ship far less polyfill code, which can cut the bundle by 50-100KB. Your build output should not include core-js for ES2020 features.
- 7
Use Server Components for static parts
Next.js Server Components execute on the server and ship zero JavaScript to the client for that subtree. Default to Server Components — only use "use client" where you genuinely need interactivity. Initial bundle drops dramatically on content-heavy routes.
- 8
Track bundle size in CI
Bundlewatch, size-limit, or Next.js's Bundle Analyzer in CI prevents regression. Set a budget per route (e.g. 100KB compressed) and fail the build when a PR exceeds it. The conversation about bundle size moves from after-the-fact firefighting to during code review.
Example
// Bad: pulls in 70KB of lodash
import _ from "lodash";
const debounced = _.debounce(handler, 300);
// Good: tree-shakeable named import
import debounce from "lodash/debounce";
const debounced = debounce(handler, 300);
// Lazy-load heavy components
import dynamic from "next/dynamic";
const RichEditor = dynamic(() => import("./RichEditor"), {
loading: () => <Skeleton />,
ssr: false,
});
// next.config.js: bundle analyzer
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
module.exports = withBundleAnalyzer({});Tree-shake imports, lazy-load heavy components, audit with bundle analyzer.
Frequently asked
Yes for ESM modules with sideEffects: false in package.json. Tree-shaking fails silently when modules use CommonJS when sideEffects is true or when the import statement isn't statically analyzable.
For a content site under 100KB compressed for the initial route. For an SPA under 200KB. Anything past 500KB compressed is a red flag — investigate via bundle analyzer.
Modern Webpack and Next.js automatically split common code into shared chunks. Don't manually configure vendor chunks unless you have a specific caching strategy in mind — manual splits often hurt more than they help.
Related fixes
Page speed, redirects, Core Web Vitals
Remove unused CSS to speed up first paint
Page speed, redirects, Core Web Vitals
Fix main thread blocked: cut Total Blocking Time and INP
Page speed, redirects, Core Web Vitals
Eliminate render-blocking resources slowing first paint
Page speed, redirects, Core Web Vitals
Fix INP poor: get Interaction to Next Paint under 200ms