Picovert

JavaScript Bundle Size Optimization: From 500 KB to 80 KB

2026-04-248 min read

JavaScript is the most expensive resource on the web — byte for byte, a JavaScript file costs more than an equivalent image because it must be parsed and executed, not just decoded. A 500 KB JavaScript bundle can block the main thread for 2–4 seconds on a mid-range phone. Here's how to get it under 100 KB.

Step 1: Measure what you have

Before optimizing, audit your current bundle. Run:

npx @next/bundle-analyzer

Enable it in next.config.js with ANALYZE=true npm run build. The treemap shows which packages are largest and which pages include which modules.

Step 2: Remove unused dependencies

The most common bundle bloat sources:

PackageSizeReplacementReplacement size
moment.js67 KBdate-fns (tree-shaken)3–8 KB
lodash72 KBlodash-es (tree-shaken)1–5 KB
axios14 KBfetch (native)0 KB
react-icons (all)340 KBIndividual SVG imports0.5 KB/icon

Step 3: Tree-shaking

Tree-shaking removes exports that are never imported. For it to work:

  • Use named imports, not default imports from barrel files. import { format } from 'date-fns' tree-shakes; import dateFns from 'date-fns' does not.
  • Avoid side-effect imports in utility modules. Files with import 'some-polyfill' at the top are treated as having side effects and won't be tree-shaken.
  • Check the library's package.json for sideEffects: false. Libraries that don't declare this are excluded from tree-shaking by Webpack/Turbopack.

Step 4: Code splitting

Next.js automatically splits code at the page level. For sub-page splitting, use dynamic imports:

const HeavyEditor = dynamic(() => import('./Editor'), { loading: () => <Spinner /> });

Good candidates for dynamic import: rich text editors, chart libraries, PDF viewers, map components, video players, and any component behind a tab or modal that isn't visible on initial load.

Step 5: Optimize third-party scripts

Use Next.js's <Script> component for third-party scripts:

  • strategy="lazyOnload" — loads after everything else, doesn't affect INP.
  • strategy="afterInteractive" — loads after hydration, the right choice for most analytics.
  • strategy="worker" — loads in a Web Worker (experimental), runs off the main thread entirely.

Step 6: Reduce React's footprint

  • Move data-fetching components to Server Components. Server Components don't ship React runtime code for themselves.
  • Replace React context with Zustand or Jotai for complex state. Context with many providers can cause excessive re-renders, indirectly inflating the cost of the JavaScript already in the bundle.
  • Use React.memo() on expensive pure components that render frequently with the same props.

Before and after: real numbers

MetricBeforeAfter
Homepage JS (gzipped)520 KB82 KB
Main thread blocking time3.2 s0.4 s
INP (75th percentile)480 ms95 ms
Lighthouse Performance6194

The changes: replaced moment.js with date-fns, replaced lodash with native JS, moved the data table to a Server Component, and deferred the analytics scripts.