A Practical Guide to Modern Browserslist Configuration: Baseline Targets, Eliminating Polyfill Bloat, and Optimizing Bundle Size in Vite and SWC

Published: September 20, 2026

A Practical Guide to Modern Browserslist Configuration: Baseline Targets, Eliminating Polyfill Bloat, and Optimizing Bundle Size in Vite and SWC

Ship less JavaScript by aligning your build toolchain with the modern web platform. Outdated configuration presets often force bundlers to downlevel syntax and inject heavy core-js polyfills for features already supported across all modern mobile and desktop engines. This guide provides an actionable, step-by-step approach to defining clean Browserslist queries, harmonizing them with Vite and SWC, and stripping unnecessary runtime shims from production releases.


Why Legacy Browser Targets Are Bloating Your Bundles

Many frontend projects inadvertently inherit legacy defaults that target obsolete browsers like Internet Explorer or versions of Safari and Chrome from over six years ago. When compilers see targets lacking support for features like native async/await, optional chaining, or private class fields, they inject bulky runtime helper functions and polyfill shims. By realigning build configurations to modern targets aligned with Web Platform Baseline, you allow the compiler to emit native syntax, eliminating hundreds of lines of transpilation scaffolding.

When build pipelines downlevel modern JavaScript, the cost is two-fold:

  1. Syntax Downleveling Overhead: An async/await statement transpiled to ES5 requires helper functions such as _asyncToGenerator and a generator state machine (regeneratorRuntime), significantly increasing output code size and execution overhead.
  2. Polyfill Injection: When using tools with auto-polyfilling configured (such as Babel or SWC with core-js), ubiquitous APIs like Promise.allSettled, Array.prototype.flat, and structuredClone trigger large dependency injections into your production bundle, even though modern browsers run them natively.

Aligning build configurations around modern targets ensures your application ships lean code to the vast majority of your users while avoiding unnecessary maintenance overhead.


Step 1: Configuring Modern Browserslist and Baseline Targets

A clean Browserslist setup defines the browser support matrix across PostCSS, Autoprefixer, ESLint, and modern compilers using a single .browserslistrc file. Configuring explicit, modern targets prevents tools from falling back to conservative defaults that target deprecated platforms. This setup ensures tooling consistently targets active evergreen engines.

Create a .browserslistrc file in the root of your project:

# Modern baseline: evergreen browsers and active engines
last 2 Chrome versions
last 2 Firefox versions
last 2 Safari versions
last 2 Edge versions
not dead
not IE 11
not op_mini all

Alternatively, if your release target focuses on modern standards-compliant mobile and desktop web runtimes supporting ES2022+, you can define a strict modern query:

defaults and fully supports es6-module
last 2 versions
not dead
not op_mini all

Inspecting Your Target Coverage

Always verify the resolved browser matrix directly in your terminal using the official CLI from the Browserslist Documentation:

# Print all matching browsers and versions
npx browserslist

# Check global audience coverage for your query
npx browserslist --coverage

Running these commands allows you to confirm that legacy browsers are excluded from your compilation matrix before configuring your downstream compilers.


Step 2: Integrating Browserslist with Vite and Bridging the ESBuild Gap

Vite uses esbuild for fast production syntax transpilation, but esbuild does not read .browserslistrc files by default. To make Vite respect your Browserslist configuration, you must bridge the two using the browserslist-to-esbuild utility. This bridge translates your browser queries directly into a valid build.target array for esbuild.

First, install the converter utility:

npm install -D browserslist-to-esbuild

Next, import the utility into your vite.config.ts and assign the result to build.target as detailed in the Vite Build Target Documentation:

import { defineConfig } from 'vite';
import browserslistToEsbuild from 'browserslist-to-esbuild';

export default defineConfig({
  build: {
    // Converts your .browserslistrc into an esbuild-compatible target array
    // e.g., ['chrome120', 'edge120', 'firefox120', 'safari17']
    target: browserslistToEsbuild(),
    cssTarget: browserslistToEsbuild(),
    minify: 'esbuild',
  },
});

Handling Legacy Fallbacks (Only When Explicitly Required)

If your project must support older corporate devices alongside modern clients, avoid degrading your main application bundle for everyone. Use @vitejs/plugin-legacy, which keeps your primary bundle modern and generates a separate legacy chunk loaded conditionally via <script nomodule>:

import { defineConfig } from 'vite';
import legacy from '@vitejs/plugin-legacy';
import browserslistToEsbuild from 'browserslist-to-esbuild';

export default defineConfig({
  plugins: [
    legacy({
      // Separate target matrix used ONLY for legacy polyfill chunks
      targets: ['defaults', 'not IE 11'],
      renderModernChunks: false,
    }),
  ],
  build: {
    // Primary modern bundle remains completely unencumbered by polyfills
    target: browserslistToEsbuild(),
  },
});

Step 3: Configuring SWC for Zero-Bloat Compilation

SWC is an extensible Rust-based compiler that supports reading Browserslist targets natively via its env configuration block. By default, misconfigured SWC setups can inject core-js imports across every module; setting env.mode appropriately stops SWC from injecting runtime shims. This configuration retains native syntax structures while outputting standard ECMAScript code.

Here is a lean, production-ready .swcrc configuration configured in accordance with the SWC Compilation Documentation:

{
  "$schema": "https://swc.rs/schema.json",
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true,
      "dynamicImport": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    },
    "target": "es2022"
  },
  "env": {
    "targets": {
      "browsers": [
        "last 2 Chrome versions",
        "last 2 Firefox versions",
        "last 2 Safari versions",
        "last 2 Edge versions",
        "not dead"
      ]
    },
    // Set to false or omit "mode" to disable automatic core-js injection
    "mode": false,
    "loose": false
  },
  "minify": true
}

Why Disabling env.mode: "usage" Eliminates Bloat

Setting "mode": "usage" or "mode": "entry" instructs SWC to parse your source code for standard library methods and insert imports from core-js whenever an API is referenced.

When your minimum baseline targets already natively implement Promise, Object.assign, Array.prototype.includes, and WeakMap, runtime shimming is redundant. Setting "mode": false disables polyfill injection completely, ensuring that your compiled output consists solely of clean, standard JavaScript.


Step 4: Eliminating Hidden Polyfills in Dependencies and PostCSS

Optimizing your application's direct source files is only half the battle, as third-party packages and CSS processors frequently smuggle outdated vendor prefixes and polyfills into your build. Auditing bundle outputs and keeping CSS autoprefixing tightly coupled to your Browserslist definition prevents unnecessary CSS rules from leaking into production. A brief audit ensures your build pipeline strips unused vendor artifacts systematically.

1. Optimize Autoprefixer in PostCSS

If your build includes PostCSS and Autoprefixer, ensure your postcss.config.js does not hardcode vendor lists. Autoprefixer automatically picks up your .browserslistrc:

// postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {
      // Uses the root .browserslistrc
      flexbox: 'no-2009',
      grid: false,
    },
  },
};

When targeting modern browsers, vendor prefixes like -webkit-border-radius or -moz-box-shadow are omitted, keeping style assets compact.

2. Auditing Production Bundles for Leaked Polyfills

To verify that third-party dependencies or misconfigured plugins are not injecting core-js or regeneratorRuntime, inspect your production output using rollup-plugin-visualizer:

npm install -D rollup-plugin-visualizer

Mount the visualizer in vite.config.ts:

import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'dist/stats.html',
      open: false,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
});

Run your production build (npm run build) and open dist/stats.html. Search the visual treemap for references to core-js, regenerator-runtime, or polyfill helper modules. If they appear inside your application chunks, trace them to the originating npm package and evaluate whether a modern native alternative is available.


Step 5: Common Pitfalls and Best Practices

A successful build optimization strategy balances aggressive modernization with real-world user accessibility. Avoiding common pitfalls like mismatched configuration files and outdated dependency queries ensures consistent deployment behavior. Adhering to structured best practices prevents syntax regressions across continuous integration workflows.

Pitfall 1: Mismatched Tool Configurations

  • The Problem: Specifying modern targets in Vite's build.target while leaving an old .browserslistrc for PostCSS or Babel creates divergence where CSS is prefixed for obsolete browsers while JS uses modern syntax.
  • The Solution: Use .browserslistrc as the single source of truth across all tools, and use browserslist-to-esbuild to forward the exact same targets to Vite.

Pitfall 2: Neglecting the caniuse-lite Database

  • The Problem: Browserslist relies on the caniuse-lite database. If this package is outdated, your build tool may assume a browser is older or less capable than it currently is.
  • The Solution: Run the automated database update command routinely in your project:
    npx update-browserslist-db@latest

Pitfall 3: Inadvertent Polyfills via Transpiled Dependencies

  • The Problem: Compiling node_modules with default Babel loaders can reinject core-js shims even if your own application source code has disabled them.
  • The Solution: When using bundlers like Vite or SWC, configure external dependency compilation to preserve ES modules and avoid running generic polyfill loaders on pre-bundled vendor files.

Frequently Asked Questions

Why does Vite ignore my .browserslistrc file during JS builds?

Vite delegates JavaScript parsing and transpilation to esbuild, which focuses on raw build speed and accepts engine targets (e.g., es2022, chrome120) rather than complex Browserslist queries. While Vite uses Browserslist for CSS via PostCSS, it ignores .browserslistrc for JavaScript downleveling unless you pass converted targets to build.target using the browserslist-to-esbuild package.

How do I check which specific browsers my query matches?

You can inspect the exact browser engines and versions resolved by your query by running npx browserslist in your terminal. To verify the demographic percentage of global or regional users covered by your current configuration, run npx browserslist --coverage.

Should I use Web Baseline queries or explicit version rules in production?

For standard consumer applications, using modern relative queries like last 2 versions, not dead, not IE 11 or baseline queries provides an optimal balance between compatibility and bundle efficiency. For enterprise applications with fixed device deployments, specify explicit minimum browser versions to ensure deterministic, reproducible compiler outputs across all builds.


About the Author

Furkan Çetinkaya is a Mobile-focused Software Developer specializing in React Native, native bridge integrations (Kotlin & Swift), and supporting backend services. Experienced in maintaining high-impact mobile applications and developer SDKs.