Building in Public#building-in-public

How to Cut Legacy JavaScript in Next.js With Browserslist

By Mohamed Abdi Guled

PageSpeed Insights had one line item that showed up in every single report, run after run, regardless of what else changed on the page: "Legacy JavaScript — Est savings of 14 KiB." Fourteen kilobytes isn't a dramatic num…

One Line in package.json: Cutting Legacy JavaScript With a Smarter Browserslist

PageSpeed Insights had one line item that showed up in every single report, run after run, regardless of what else changed on the page: "Legacy JavaScript — Est savings of 14 KiB." Fourteen kilobytes isn't a dramatic number by itself. What made it worth chasing down was that it never moved. Other metrics fluctuated between runs. This one sat there, identical, every time — which usually means the cause is static, not environmental.

What "Legacy JavaScript" Actually Means

Modern JavaScript engines support a lot of language features natively now — array methods, object utilities, string helpers — that used to require a polyfill to work reliably across browsers. A polyfill is a small chunk of code that re-implements a feature for browsers old enough not to have it built in. Bundlers add these automatically based on one setting: the list of browsers the project claims to support.

The project's package.json had this:

"browserslist": [
  "> 0.5%",
  "last 2 versions",
  "Firefox ESR",
  "not dead",
  "not IE 11"
]

"last 2 versions" reads like a reasonable, conservative default. What it actually means is: the last two versions of *every* browser tracked by the underlying browser-usage data — some far more obscure than the phrase suggests, and several old enough that the bundler adds polyfills for language features every actual visitor's browser already supports natively. "Firefox ESR" adds an extended-support release track that lags well behind current Firefox specifically so enterprise environments can delay updates — a reasonable thing for some projects to support, and functionally irrelevant for a mobile-first AI tools directory with no enterprise deployment story.

The result: Array.prototype.at, .flat, .flatMap, Object.fromEntries, Object.hasOwn, String.prototype.trimStart/trimEnd — all polyfilled, all shipped to every visitor, for browser versions that make up a vanishingly small fraction of anyone actually loading the site on a phone.

The Fix Is a Config Change, Not a Code Change

Nothing about the application code needed to change. The fix was narrowing the browser target list to modern evergreen browsers — the ones that auto-update and already support these features natively:

"browserslist": [
  "> 0.5%",
  "last 2 chrome version",
  "last 2 firefox version",
  "last 2 safari version",
  "last 2 edge version",
  "not dead",
  "not IE 11",
  "not op_mini all"
]

Naming each browser explicitly — rather than the blanket "last 2 versions" — removes the obscure and legacy targets from the list without removing real support for anyone's actual browser. not op_mini all excludes Opera Mini specifically, which uses a server-side rendering proxy that doesn't execute client JavaScript the same way regular browsers do, and including it in the target list mostly just adds bundler overhead without meaningfully improving anyone's experience.

Verifying Before Trusting the Diff

A config file edit that touches the build target list is exactly the kind of change that looks trivial and can quietly break something if the wrong browsers get excluded. Before committing anything, the JSON itself was validated as parseable — a broken package.json fails every single page on the site, not just the one PageSpeed was measuring:

node -e "JSON.parse(require('fs').readFileSync('package.json', 'utf8')); console.log('Valid JSON')"

Then the diff was reviewed line by line before it shipped, confirming only the browserslist array had changed and nothing else in the file had been touched.

Why the Score Didn't Move as Cleanly as Expected

The honest follow-up: PageSpeed's overall performance score continued to vary between runs after this shipped — sometimes higher, sometimes not — for reasons unrelated to this specific fix. Lighthouse's lab-based testing has its own run-to-run variance from network simulation and CPU throttling that no single config change eliminates. What this fix reliably did was remove the one line item that had been static and consistent across every prior report; it wasn't a claim that the overall score would jump by some fixed amount, because Lighthouse doesn't work that way. Fixing a specific, correctly-diagnosed issue and expecting the aggregate score to move by a predictable amount are two different claims, and only the first one holds up under scrutiny.

The General Lesson

browserslist's defaults are written to be safe for the widest possible range of projects, which makes them wrong-by-default for almost any specific one. A project with real enterprise users stuck on older managed browsers has a real reason to keep Firefox ESR and a wide last 2 versions net. A mobile-first consumer site with no enterprise deployment story is paying a real, measurable cost — bytes shipped to every visitor — for browser support nobody using the site actually needs.

Takeaway: if PageSpeed is flagging legacy JavaScript and the codebase isn't using any actual legacy syntax, check browserslist before looking anywhere else. It's one config array, and it's often the entire explanation.

📬 Get the latest AI tool reviews

Expert picks and comparisons, weekly. No spam.

← Back to Blog