The most surprising thing about Vite’s legacy browser support is that it doesn’t actually bundle polyfills for older browsers directly. Instead, it generates two distinct bundles: one for modern browsers and another for older ones, with the latter including the necessary polyfills.
Let’s see this in action. Imagine a simple Vite project with a dependency that uses modern JavaScript features, like async/await.
// src/main.js
async function greet() {
console.log('Hello from Vite!');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Async done!');
}
greet();
Without any legacy configuration, a build would produce a single index.js file, optimized for modern browsers. However, if we want to support, say, Internet Explorer 11, we need the @vitejs/plugin-legacy.
First, install the plugin:
npm install @vitejs/plugin-legacy --save-dev
# or
yarn add @vitejs/plugin-legacy --dev
Then, configure it in your vite.config.js:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
react(),
legacy({
targets: ['ie 11'], // Specify the browsers to support
additionalLegacyPolyfills: ['regenerator-runtime/runtime'] // For async/await and generators
})
]
});
Now, when you run vite build, Vite will analyze your code and dependencies. It detects that async/await requires features not present in IE 11. It then proceeds to create two sets of output files in the dist directory:
- Modern Bundle:
assets/index.<hash>.js(and associated CSS/other assets). This bundle is for browsers that support modern ES modules and syntax. It’s smaller and faster. - Legacy Bundle:
legacy/index.<hash>.js(and associated assets). This bundle is for older browsers. It includes your original code, transpiled down to ES5, and critically, the polyfills necessary to make those modern JavaScript features work.
Your index.html will be automatically updated by the plugin to include <script type="module"> for modern browsers and a fallback <script nomodule> for legacy browsers that points to the transpiled, polyfilled bundle.
<!-- dist/index.html (simplified) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vite App</title>
<link rel="modulepreload" href="/assets/index.a1b2c3d4.js">
<link rel="stylesheet" href="/assets/index.e5f6g7h8.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/index.a1b2c3d4.js"></script>
<script nomodule src="/legacy/index.f9e8d7c6.js"></script>
</body>
</html>
The targets option in the plugin configuration is crucial. It accepts a glob string or an array of glob strings compatible with browserslist. This tells Vite which browsers need the transpilation and polyfills. Common targets include 'defaults', 'not IE 11', 'last 2 versions', or specific browser versions like 'ie 11', 'firefox 60'.
The additionalLegacyPolyfills option is for specific features that Vite’s default Babel presets might not cover or that you explicitly want to ensure are present. regenerator-runtime/runtime is a common one for making async/await and generator functions work in older environments.
Under the hood, @vitejs/plugin-legacy uses Rollup’s plugin-legacy as its base. It leverages Babel to transpile your code and inject polyfills. The key difference is Vite’s optimized build process and its intelligent handling of module types. It doesn’t just create one giant polyfilled bundle; it strategically separates the modern and legacy outputs. This means modern browsers download only the lean, optimized code they need, while older browsers get the slightly larger, but functional, legacy bundle.
A common pitfall is forgetting to include regenerator-runtime/runtime when using async/await or generators. Without it, even with transpilation, these features will crash in older browsers because the underlying runtime support for them is missing. The targets option is also key; if you set it too narrowly (e.g., only supporting the latest Chrome), your nomodule script might not actually be necessary, leading to an unnecessary download for some users. Conversely, if you include very old browsers, your legacy bundle can become quite large.
Once you’ve got legacy support configured, the next challenge is often managing the performance implications of those larger legacy bundles, and how to progressively enhance the user experience for those on modern browsers.