Anup Haldar / blog
ArticlesProjectsAbout
All articles
Next.jsPerformancePageSpeed

"Reduce unused JavaScript" on a Next.js site: how to tell if it's your code

PageSpeed flags unused JavaScript on most Next.js sites. How to find what each chunk contains, which fixes work, and when it is just React.

By Anup Haldar31 October 20266 min read

Run PageSpeed Insights on almost any Next.js App Router site and you'll see "Reduce unused JavaScript", usually with a few dozen KiB of "potential savings" attached. It reads like a to-do item. Often it isn't one: a large share of that number is React and the App Router's own runtime, which no amount of app-level tidying will remove.

Often isn't always, though. Sometimes the audit is pointing straight at a client component that's bigger than it needs to be, or at a third-party script. This post covers how to tell the two apart, the fixes that work when it is your code, and when to stop looking.

What the audit measures

Lighthouse loads the page, records which bytes of each JavaScript file actually execute during that load, and flags files where a large share didn't run. "Unused" here means "not executed during this one page load". It doesn't mean "never needed".

That distinction matters for a framework. React ships the code for state updates, transitions, error boundaries and much more. A mostly-static page won't exercise most of it on first load, so Lighthouse counts it as unused, even though the next click might need it.

Step 1: read which files it names

Expand the audit in PageSpeed Insights. It lists each file with its transfer size and the estimated unused bytes. On a Next.js site those files live under /_next/static/chunks/, with hashed names that don't tell you much.

Here's what it named on this site, which has a homepage and an MDX blog and very little interactivity:

ChunkWhat it turned out to be
~70 KiBReact + ReactDOM, as bundled by Next
~38 KiBThe App Router's client runtime

The total flagged was about 47 KiB of unused JavaScript, all of it inside those two files. Everything I actually wrote that runs in the browser (navigation, the contact form, toasts, the theme toggle) compiles to a single chunk of about 27 KB raw, 8 KiB over the wire, and that chunk wasn't on the list.

Step 2: identify what's inside each chunk

Hashed filenames don't tell you much, so open them up. There are a few ways to do it.

The bundle analyzer. Recent versions of Next.js include an interactive analyzer for Turbopack builds:

npx next experimental-analyze

It opens a web UI showing every module in every chunk, so you can match a flagged filename to its contents. If you're on webpack instead, @next/bundle-analyzer does the same job.

The Coverage panel in Chrome DevTools. Open DevTools, press Ctrl+Shift+P (Cmd+Shift+P on a Mac), run Show Coverage, then reload. You get the same "unused bytes" figure per file that Lighthouse uses, and clicking a file shows exactly which lines ran. This is the fastest way to see whether unused code is concentrated in your component or spread through the framework.

Just read it. Open the chunk URL in a tab and search for a name you recognise: one of your component names, a library you installed, react-dom. Minified code still keeps plenty of strings.

Step 3: rule out the usual culprits

Before writing the audit off as framework overhead, check these. Each one is a real, fixable problem that looks the same in the report.

A "use client" boundary set too high

In the App Router, everything a "use client" file imports becomes client JavaScript. Put the directive at the top of a page or layout to make one button interactive, and the whole tree under it ships to the browser.

The fix is to push the directive down to the smallest component that actually needs state or event handlers, and keep the rest as server components:

Before: the whole section is client code
'use client'
 
export default function Pricing() {
  const [annual, setAnnual] = useState(false)
  return (
    <section>
      {/* lots of static markup */}
      <BillingToggle annual={annual} onChange={setAnnual} />
      {/* more static markup */}
    </section>
  )
}

Moving the state into BillingToggle, and the "use client" with it, leaves Pricing as a server component that sends HTML, not JavaScript.

A heavy library in a client component

A date library, a charting package, a markdown parser or a syntax highlighter imported into a client component ships all of its weight to every visitor. Ask whether the work can happen on the server or at build time instead. On this blog, MDX compilation and code highlighting both run at build time, so the browser receives finished HTML with colour already applied and none of the tooling.

If the library really does need to run in the browser, load it only when it's needed:

import dynamic from 'next/dynamic'
 
const Chart = dynamic(() => import('./Chart'), { ssr: false })

Barrel imports

Importing one icon from a package that re-exports thousands can pull in more than you asked for, depending on how the package is built. Next.js has optimizePackageImports for this, and it already applies it automatically to a list of common libraries such as lucide-react. If a large package of yours isn't on that list, add it:

next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ['some-big-ui-library'],
  },
}

Third-party scripts

Analytics, chat widgets, tag managers and embeds are frequent entries in this audit, and they're the easiest to miss because they aren't in your bundle at all. Google's gtag.js alone is well over 100 KiB.

You can't shrink a third-party script, but you can control when it loads. This site loads gtag.js with next/script's lazyOnload strategy, which waits until the browser is idle after the page has loaded:

src/app/layout.tsx
<Script
  id="ga"
  strategy="lazyOnload"
  src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
/>

The common wrapper component I'd been using before loaded it earlier and preloaded it at high priority. On a throttled mobile run, that competed with the HTML and cost about 1.2 seconds of First Contentful Paint. Moving it to lazyOnload mattered far more for the score than anything in the unused-JavaScript audit.

A polyfill bundle

If you spot a large core-js or polyfill chunk in the build output, check how it's loaded before panicking. On this site it's emitted inside <script noModule>, which modern browsers skip entirely. You can confirm it in the built HTML, and by watching the Network panel in a current browser, where it never appears.

Step 4: know when to stop

If the flagged files are the React and App Router chunks, and you've ruled out everything above, you're done. There's no app-level change that removes React from a React site. The audit will keep showing up on every PageSpeed run, and it'll keep reading like a bug, but the right move is to stop re-profiling it.

Two related things you also don't control from the repo:

  • Compression level. Vercel serves this site with brotli, but not at the maximum level. The React chunk arrives at 70.2 KiB, while compressing the same file locally at brotli's highest quality gives 58.9 KiB. That gap belongs to the host's settings, not to the code.
  • Framework version. Each major Next.js and React release changes the size of the runtime. Upgrading can help, but it's an upgrade decision, not a performance fix.

Look at the metrics, not the audit

"Reduce unused JavaScript" is a diagnostic, not a score. If your Largest Contentful Paint, Total Blocking Time and Interaction to Next Paint are all green, a few dozen KiB of framework code that didn't execute on first load isn't hurting real visitors.

The other things PageSpeed flagged on this site, and which of them turned out to be real, are in the SEO plumbing behind this blog.

Frequently asked questions

Why does PageSpeed say 'Reduce unused JavaScript' on my Next.js site?

Lighthouse flags any JavaScript that didn't execute during the test page load. On a Next.js App Router site, much of that is usually React and the router runtime, which ship with every page but aren't fully used on first load. Check which files the audit names before assuming it's your code.

Can I remove unused React code from a Next.js bundle?

Not in any practical way. React and the App Router runtime are what make the page work after it loads. What you can control is your own client code: where "use client" boundaries sit, which libraries run in the browser, and when third-party scripts load.

How do I see what's inside a Next.js chunk?

Run npx next experimental-analyze for a Turbopack build, or use @next/bundle-analyzer with webpack. Chrome DevTools' Coverage panel also shows used and unused bytes per file, line by line.

Does unused JavaScript hurt SEO?

Not directly. Google uses Core Web Vitals from real visitors, not Lighthouse audits. Unused JavaScript only matters if it slows those metrics down, usually by delaying first paint or blocking the main thread.

Should third-party scripts like Google Analytics use lazyOnload?

For analytics that doesn't need to fire before the page is interactive, lazyOnload is a good default. It keeps the script out of the critical path. The trade-off is that very short visits may not be recorded.

Written by

Anup Haldar

Full Stack Developer in Uttarakhand, India, with 3+ years of experience building production-ready web applications with TypeScript, React, Next.js, and Node.js.

Work with me
PreviousWhich AI tool should you use? A guide sorted by the job, not the brandNext"Page with redirect" in Search Console: when it's fine, and when it isn't

On this page

  • What the audit measures
  • Step 1: read which files it names
  • Step 2: identify what's inside each chunk
  • Step 3: rule out the usual culprits
  • Step 4: know when to stop
  • Frequently asked questions

Anup Haldar

Full Stack Developer in Uttarakhand, India, with 3+ years of experience building production-ready web applications with TypeScript, React, Next.js, and Node.js.

Blog

  • All articles
  • RSS feed
  • Sitemap

Elsewhere

  • Portfolio home
  • Projects
  • Experience
  • Get in touch

© 2026 Anup Haldar.