How to Remove Unneeded Polyfills with Browserslist
This guide is part of Polyfills & Differential Serving in JavaScript Bundle Optimization & Code Splitting. Browserslist is the shared configuration that Babel's preset-env, SWC, core-js (through Babel), Autoprefixer, Lightning CSS and many other tools read to decide what to transform. One outdated query — often copied from a project template years ago — quietly instructs all of them to support browsers your visitors abandoned long ago.
The fix is to replace a guessed query with one derived from your own traffic, then verify that each tool actually honours it. On typical single-page apps this removes 20–60KB of compressed JavaScript and a matching amount of CSS prefixes, with no change in behaviour for the browsers people actually use.
Rapid Diagnosis
- Print the current matrix:
npx browserslist. If it lists Internet Explorer, very old Safari, Opera Mini or Android 4.x browsers, the query is outdated. - Check for multiple sources of truth. A
browserslistkey inpackage.jsonand a.browserslistrcfile, or per-tooltargetsoptions in Babel config, can disagree. - Check environment sections.
[production]and[development]sections in.browserslistrcapply depending onBROWSERSLIST_ENVorNODE_ENV; production may still use an old list. - Look for polyfills in the treemap (
core-js/modules/...) and helpers (regenerator-runtime).
Root Cause Analysis
1. Template defaults. Starters shipped years ago with queries like > 0.25%, not dead, ie 11 that persist unexamined.
2. Global usage instead of your usage. Queries based on global percentages include browsers that are popular somewhere but absent from your audience.
3. Tool-specific overrides. A Babel targets option overrides Browserslist for Babel only, so CSS and JS disagree; or SWC is configured with an explicit env.targets that nobody updates.
4. Stale caniuse data. Browserslist resolves queries using the caniuse-lite database installed in your project. Old data makes "last 2 versions" mean versions from the past.
Step-by-Step Resolution
1. Build a usage-based query
Export browser versions from your analytics, convert them to a Browserslist stats file, and query against it.
# browserslist-ga or similar tools convert analytics exports into browserslist-stats.json.
npx browserslist-ga-export --reportPath report.csv # produces browserslist-stats.json
npx browserslist "> 0.2% in my stats, last 2 Chrome versions, last 2 Safari versions, Firefox ESR, not dead"
# trade-off: "in my stats" reflects past traffic. Browsers you do not support
# today will not appear in your analytics because their users already left —
# decide policy deliberately rather than following the data in a loop.
Expected outcome: a matrix covering your chosen share of traffic, typically far more modern than the template.
2. Make Browserslist the single source of truth
Remove per-tool targets so every tool reads the same matrix.
# .browserslistrc
[production]
> 0.2% in my stats
last 2 Chrome versions
last 2 Safari versions
last 2 iOS versions
Firefox ESR
not dead
[development]
last 1 Chrome version
last 1 Firefox version
// babel.config.js — no "targets" here; preset-env reads .browserslistrc.
module.exports = {
presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: '3.40', bugfixes: true }]],
};
// trade-off: useBuiltIns 'usage' injects polyfills per file based on what that
// file uses, which is precise but can miss APIs used by un-transpiled
// dependencies. If a dependency needs a polyfill, add it explicitly in the entry.
Expected outcome: Babel, PostCSS and other tools produce output for one consistent matrix.
3. Update caniuse-lite and pin corejs
npx update-browserslist-db@latest
# trade-off: updating the database can change what "last 2 versions" means and
# therefore your output. Commit the lockfile change in its own PR so any output
# difference is reviewable.
Set corejs to the installed minor version (for example '3.40') so core-js knows which polyfills exist and which browsers need them; a bare 3 assumes an old feature set.
Expected outcome: polyfill decisions use current browser data.
4. Align esbuild-based tools manually
esbuild (and therefore Vite's build.target) does not read Browserslist. Convert your query with browserslist-to-esbuild so syntax lowering matches.
// vite.config.js
import browserslistToEsbuild from 'browserslist-to-esbuild';
export default { build: { target: browserslistToEsbuild() } };
// trade-off: esbuild lowers syntax but never adds polyfills. If you need
// runtime polyfills with Vite, add them explicitly or use @vitejs/plugin-legacy.
Expected outcome: syntax lowering and polyfilling are both driven by the same matrix.
Verification
Rebuild and compare: the treemap should show far fewer core-js modules, and regeneratorRuntime should be absent if your matrix supports native async functions. Run your end-to-end tests in the oldest browser the new matrix includes (via BrowserStack, Sauce Labs or Playwright's WebKit/Firefox builds) to confirm nothing broke. Measure TBT on a throttled run; expect a modest but real improvement proportional to the bytes removed.
Common Mistakes
- Deriving the query from desktop analytics only. Mobile Safari versions lag desktop ones; make sure iOS versions are represented.
- Excluding embedded browsers. In-app browsers (social apps' webviews) report as Chrome or Safari variants with their own version lag; check them in your stats.
- Forgetting
not dead. Without it, a percentage-based query can include discontinued browsers that still have residual traffic. - Changing the matrix without telling support. Users of newly unsupported browsers may see broken features; give support teams the list and a recommended upgrade path.
Worked Example: One Query Change, Three Tools
A documentation site used > 0.25%, not dead as its query. Running npx browserslist showed it included Opera Mini, UC Browser and several old Android WebViews because of their global share — none of which appeared in the site's own analytics, where 99.6% of sessions came from Chromium 110+, Safari 16+ and Firefox 115+. Replacing the query with one based on the site's stats changed three outputs at once: Babel stopped injecting 31 polyfills, Autoprefixer removed about 4KB of prefixed CSS, and Lightning CSS stopped emitting fallbacks for color-mix() and nesting. The pull request was a two-line configuration change; the review consisted mostly of running the end-to-end tests in Safari 16, which passed.
FAQ
How much traffic coverage is reasonable?
Most consumer sites settle between 98% and 99.5% of sessions. The remaining sessions are often bots, very old devices, or browsers that would struggle with the site regardless. For B2B products with known customer environments, use customer requirements rather than percentages.
Does a tighter matrix break old browsers completely?
It can, if modern syntax reaches a browser that cannot parse it — the whole script fails. Decide how unsupported browsers should behave: a server-rendered page that works without JavaScript, a feature-detected notice, or a legacy build served via nomodule. Silently broken pages are the one outcome to avoid.
Should Browserslist differ between client and server bundles?
Yes. Server code should target your Node version (node 20 or similar), which every server-side tool understands. Keep the browser matrix for client builds only.
Can I check a query's effect before changing the build?
Yes. Run npx browserslist "<new query>" to see the resolved browsers, then build once with BROWSERSLIST="<new query>" set in the environment, which overrides the config file for that run. Compare the output treemap with the current build before committing to the change.
What about browsers on smart TVs and consoles?
Some audiences — streaming and media sites in particular — have meaningful traffic from TV and console browsers based on older engine versions. They appear in analytics with distinctive user-agent strings; include them in the policy decision explicitly, since a global query will usually exclude them and a tightened one certainly will.
Related
- Auditing core-js polyfill weight — checking what remains after tightening.
- Targeting baseline browsers with esbuild and SWC — syntax targets without Babel.
- esbuild vs Terser for production minification — the next step in shrinking output.