Quick answer: Lighthouse flags polyfills and Babel transforms that modern browsers do not need, such as Array.from or transpiled classes. Lighthouse 13 calls it "Legacy JavaScript". Fix your own bundle with a modern browserslist, @babel/preset-env with useBuiltIns usage, and an ES2020 build target. For third-party scripts, update or remove the tag.
This audit flags polyfills and Babel transforms that modern browsers do not need. Lighthouse 13 shows it as the Legacy JavaScript insight. Older Lighthouse versions, PageSpeed Insights reports, GTmetrix, and Screaming Frog still print the old wording: "Avoid serving legacy JavaScript to modern browsers". It is the same check. In our study of 269 Shopify storefronts, 211 of them (78.4%) failed it. This page covers what the report rows mean, how to fix the code you own, and what to do when the row is a third-party script.
TL;DR
- What: A script contains a polyfill (for example
Array.prototype.includes) or a transpiled form of modern syntax (for example@babel/plugin-transform-classes) for a feature every current browser already supports. - Why it matters: The browser downloads, parses, and runs code it does not need. The report shows the waste as "Est savings" in KiB.
- Fix: Point your build at modern browsers. Set a modern
browserslist, let@babel/preset-envadd only the polyfills those targets need, and raise the TypeScript or bundlertarget. For third-party scripts, update or remove the tag.
What does "Avoid serving legacy JavaScript to modern browsers" mean?
Build tools often compile modern JavaScript down to ES5 so it runs in old browsers like Internet Explorer 11. They also add polyfills: small libraries that recreate newer built-in methods. Modern browsers already have those features, so that extra code is pure cost for them.
Lighthouse scans each script for known signatures of this code and lists what it finds:
| Column in the report | What it tells you |
|---|---|
| URL | The script that contains the legacy code. This tells you who owns the fix. |
| Signal (the code rows under each URL) | The polyfill or transform Lighthouse matched, such as Array.from, Object.assign, @babel/plugin-transform-regenerator, or @babel/plugin-transform-spread |
| Wasted bytes | Lighthouse's estimate of the bytes that legacy code adds to that script |
The insight does not change your Performance score by itself. It feeds the score through the metrics: smaller scripts parse and run faster, which helps Total Blocking Time and main-thread work.
Why did Lighthouse rename it to "Legacy JavaScript"?
Lighthouse 13 replaced many of the older opportunity audits with "insights" that match the Performance panel in Chrome DevTools. The audit id changed from legacy-javascript to legacy-javascript-insight, and the title changed from "Avoid serving legacy JavaScript to modern browsers" to "Legacy JavaScript". The check and the fix are the same. If you search for either wording, this page applies.
How do I find which scripts Lighthouse flagged?
- In the report, open the Performance section and expand Legacy JavaScript (or "Avoid serving legacy JavaScript to modern browsers" in older versions). Note each URL and the signals under it.
- Sort by ownership. URLs on your own domain or your bundle filenames are yours to fix. URLs on
connect.facebook.net,googletagmanager.com, app CDNs, or captcha domains belong to a vendor. - For many pages, read the JSON output:
lighthouse https://example.com --only-categories=performance --output=json --quiet \
| jq -r '.audits["legacy-javascript-insight"].details.items[]
| "\(.wastedBytes) \(.url)"'
How do I fix legacy JavaScript in my own build?
The fix is to tell your toolchain which browsers you support, then let it skip the code those browsers do not need.
1. Set a modern browserslist
Most tools (Babel, Autoprefixer, Next.js, many bundler plugins) read the same browserslist setting. Add it to package.json:
{
"browserslist": [
"defaults and fully supports es6-module",
"not dead"
]
}
This targets browsers that support native ES modules. Every current version of Chrome, Edge, Firefox, and Safari meets it. Run npx browserslist to see the exact list it resolves to.
2. Polyfill only what the targets need
If you use @babel/preset-env, do not import all of core-js at the top of your entry file. Let Babel add only the polyfills your code uses and your targets lack:
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
bugfixes: true,
}],
],
};
Then remove any import 'core-js' or import '@babel/polyfill' line from the entry file. With modern targets, most of the polyfills Lighthouse lists disappear from the bundle.
3. Raise the TypeScript and bundler target
- TypeScript: if
tscemits the final code, an oldtargetsuch as"ES5"rewrites classes, spread, and async functions. Set"target": "ES2020"or later intsconfig.json. - Vite: recent Vite versions target modern browsers by default. If you added
@vitejs/plugin-legacy, check that modern browsers load only thetype="module"build, not the legacy chunks. - webpack: set
target: ['web', 'es2020'](or your browserslist) so webpack's own runtime code is not downgraded to ES5. - esbuild: pass
--target=es2020or a browser list such as--target=chrome107,firefox104,safari16.
4. Serve a legacy bundle only if you must
If you have a real requirement to support old browsers, build two bundles. Load the modern one with <script type="module"> and the legacy one with <script nomodule>. Modern browsers ignore nomodule, so they never download the legacy code.
<script type="module" src="/app.modern.js"></script>
<script nomodule src="/app.legacy.js"></script>
How do I fix it in Next.js, WordPress, or Shopify?
- Next.js: Next.js reads the
browserslistfield frompackage.jsonand already targets modern browsers by default. Some flagged polyfills come from the framework runtime itself, and app code cannot remove them. Fix the rows that point at your own chunks first, and keep Next.js up to date. - WordPress: most legacy code comes from plugins and themes that ship pre-built bundles. You cannot change their build. Update the plugin, replace it with a lighter one, or stop it loading on pages that do not use it.
- Shopify: the theme bundle is yours if you build it with a modern toolchain. App scripts are not. We re-ran three failing stores from our study. The flagged scripts were the Meta Pixel (
connect.facebook.net/.../fbevents.js, about 12 KiB of transforms such as@babel/plugin-transform-classesand@babel/plugin-transform-regenerator) on two stores, Shopify's own hCaptcha script on one, and several app extension scripts. Estimated savings were 14 KiB to 48 KiB per store.
Can I pass this audit if a third-party script causes it?
Usually not, and it is often not worth trying. A vendor builds its script for the widest set of browsers it supports, and you cannot recompile it. Your options:
- Remove the tag if it does not earn its place. This is the only fix you fully control.
- Update to the vendor's current SDK. Some vendors publish a modern build.
- Load it later. Moving an optional tag after
window.loaddoes not remove the bytes, but it takes them off the critical path for TBT. - Accept the row. The savings on a single vendor script are often 10 KiB to 20 KiB. Spend the effort on unused JavaScript first, where the same scripts usually waste far more.
How do I verify the fix?
- Rebuild for production and re-run Lighthouse. The rows for your own bundle should be gone or have smaller savings.
- Search the built output for the signals Lighthouse listed. For example,
grep -l "regeneratorRuntime" dist/*.jsshould return nothing after you drop ES5 async transforms. - Test in the browsers you still support. A target that is too aggressive breaks old browsers, so check your analytics for real browser versions before you raise it.
- Compare the bundle size before and after with your bundler's analyzer or
ls -lhon the output files.
What mistakes should I avoid?
- Importing all of
core-jsin the entry file. This adds every polyfill to every page, no matter what the targets are. - Keeping
"target": "ES5"intsconfig.jsonout of habit. It rewrites modern syntax even when the bundler targets modern browsers. - Dropping old browsers without checking traffic. Read your analytics first. If a meaningful share of buyers uses an old browser, serve them a
nomodulebundle instead of breaking the site. - Chasing vendor rows. A third-party row you cannot rebuild is a keep-or-remove decision, not a build fix.
Related audits
- Reduce unused JavaScript: the same scripts often waste more bytes on unused code than on legacy code
- Total Blocking Time (TBT): less JavaScript to parse and run means shorter long tasks
- Minimize main-thread work: Script Evaluation time includes the polyfills and transforms this audit flags
Audit your URL at https://lighthouse-md.com.
Audit your page now
Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.