# "Avoid serving legacy JavaScript to modern browsers": how to fix it

**Audit ID:** `legacy-javascript-insight` · **Category:** Performance

<!--QA-->
> **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.
<!--/QA-->

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-env` add only the polyfills those targets need, and raise the TypeScript or bundler `target`. 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](/audits/total-blocking-time) and [main-thread work](/audits/mainthread-work-breakdown).

## 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?

1. **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.
2. **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.
3. **For many pages, read the JSON output:**

   ```bash
   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`:

```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:

```js
// 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 `tsc` emits the final code, an old `target` such as `"ES5"` rewrites classes, spread, and async functions. Set `"target": "ES2020"` or later in `tsconfig.json`.
- **Vite:** recent Vite versions target modern browsers by default. If you added `@vitejs/plugin-legacy`, check that modern browsers load only the `type="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=es2020` or 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.

```html
<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 `browserslist` field from `package.json` and 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-classes` and `@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:

1. **Remove the tag** if it does not earn its place. This is the only fix you fully control.
2. **Update to the vendor's current SDK.** Some vendors publish a modern build.
3. **Load it later.** Moving an optional tag after `window.load` does not remove the bytes, but it takes them off the critical path for [TBT](/audits/total-blocking-time).
4. **Accept the row.** The savings on a single vendor script are often 10 KiB to 20 KiB. Spend the effort on [unused JavaScript](/audits/reduce-unused-javascript) first, where the same scripts usually waste far more.

## How do I verify the fix?

1. Rebuild for production and re-run Lighthouse. The rows for your own bundle should be gone or have smaller savings.
2. Search the built output for the signals Lighthouse listed. For example, `grep -l "regeneratorRuntime" dist/*.js` should return nothing after you drop ES5 async transforms.
3. 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.
4. Compare the bundle size before and after with your bundler's analyzer or `ls -lh` on the output files.

## What mistakes should I avoid?

- **Importing all of `core-js` in the entry file.** This adds every polyfill to every page, no matter what the targets are.
- **Keeping `"target": "ES5"` in `tsconfig.json` out 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 `nomodule` bundle 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](/audits/reduce-unused-javascript): the same scripts often waste more bytes on unused code than on legacy code
- [Total Blocking Time (TBT)](/audits/total-blocking-time): less JavaScript to parse and run means shorter long tasks
- [Minimize main-thread work](/audits/mainthread-work-breakdown): Script Evaluation time includes the polyfills and transforms this audit flags

---

Audit your URL at https://lighthouse-md.com.
