Configuration Analysis of Low version Browser Compatibility with Vite
Modern Browsers vs Legacy Browsers differentiate the baseline:
Whether to support:
- Chrome >= 87
- Firefox >= 78
- Safari >= 13
- Edge >= 88
https://vitejs.dev/guide/#browser-support
The following analysis is based on vite v2.9.4
vite uses esbuild to transform JS, TS, CSS and other syntaxes, which can be passed to esbuild by configuring this option, works in both development and production environments.
The relevant source code is as follows:
// packages/vite/src/node/plugins.ts
export async function resolvePlugins(
config: ResolvedConfig,
prePlugins: Plugin[],
normalPlugins: Plugin[],
postPlugins: Plugin[]
): Promise<Plugin[]> {
...
return [
...
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
...
].filter(Boolean) as Plugin[]
}
// packages/vite/src/node/esbuild.ts
import { transform } from 'esbuild'
...
export async function transformWithEsbuild(
code: string,
filename: string,
options?: TransformOptions,
inMap?: object
): Promise<ESBuildTransformResult> {
...
// transform by esbuild's transform
const result = await transform(code, resolvedOptions)
...
}
...
For example:
The ??
operator only supports Chrome >= 85 and above, so you need to configure this option to transform it into a conditional expression equivalent to a lower version, which is used in a lower version of Chrome. The configuration example is as follows:
// vite.config.js
export default defineConfig({
esbuild: {
target: 'chrome70',
}
})
Set the browser target for the final build, the compilation process will be performed by esbuild.
Note: Due to the limitation of esbuild, the minimum version is supported to es2015
The relevant source code is as follows:
// packages/vite/src/node/build.ts
...
// default value of build.target
if (resolved.target === 'modules') {
resolved.target = [
'es2019',
'edge88',
'firefox78',
'chrome87',
'safari13.1'
]
} else if (resolved.target === 'esnext' && resolved.minify === 'terser') {
resolved.target = 'es2019'
}
...
// packages/vite/src/node/build.ts
...
export function resolveBuildPlugins(config: ResolvedConfig): {
pre: Plugin[]
post: Plugin[]
} {
const options = config.build
return {
// user plugin with enforce: 'pre'
pre: [
...
],
// user plugin with enforce: 'post'
post: [
// follow here: the default configuration (config.esbuild !== false), will transform the code through esbuild
...(config.esbuild !== false ? [buildEsbuildPlugin(config)] : []),
...
]
}
}
...
// packages/vite/src/node/plugins/esbuild.ts
import { transform } from 'esbuild'
export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => {
return {
name: 'vite:esbuild-transpile',
...
async renderChunk(code, chunk, opts) {
if (opts.__vite_skip_esbuild__) {
return null
}
// transform target
const target = config.build.target
const minify =
config.build.minify === 'esbuild' &&
!(config.build.lib && opts.format === 'es')
if ((!target || target === 'esnext') && !minify) {
return null
}
// transform code by esbuild
const res = await transformWithEsbuild(code, chunk.fileName, {
...config.esbuild, // incoming esbuild configuration
target: target || undefined, // in the production environment, if config.build.target is configured, it will override config.esbuild.target
...(minify
? {
minify,
treeShaking: true,
format: rollupToEsbuildFormatMap[opts.format]
}
: undefined)
})
return res
}
}
}
export async function transformWithEsbuild(
code: string,
filename: string,
options?: TransformOptions,
inMap?: object
): Promise<ESBuildTransformResult> {
...
try {
// esbuild transform
const result = await transform(code, resolvedOptions)
...
} catch (e: any) {
...
}
}
Set a different browser target for CSS compression, in the cssPostPlugin will use esbuild to compress the CSS code.
Should only be used when targeting non-mainstream browsers, such as: webview in Android WeChat, supports most modern JavaScript features, but does not support the #RGBA
hex color symbol in CSS. So set build.cssTarget
to chrome61
to prevent vite from converting rgba()
colors to #RGBA
hex notation.
The relevant source code is as follows:
// packages/vite/src/node/build.ts
...
// default will be the same as build.target
if (!resolved.cssTarget) {
resolved.cssTarget = resolved.target
}
...
// packages/vite/src/node/plugins/css.ts
import { transform, formatMessages } from 'esbuild'
export function cssPostPlugin(config: ResolvedConfig): Plugin {
return {
name: 'vite:css-post',
...
async transform(css, id, options) {
...
// defaults to 'esbuild', compressed by esbuild
if (minify && config.build.minify) {
css = await minifyCSS(css, config)
}
...
},
...
}
}
async function minifyCSS(css: string, config: ResolvedConfig) {
// esbuild transform
const { code, warnings } = await transform(css, {
loader: 'css',
minify: true,
target: config.build.cssTarget || undefined
})
return code
}
@vitejs/plugin-legacy: compatible plugins for low-version browsers in production environment.
Note: Does not work on development environments!
- Depending on the target browser version, use babel to transform the new syntax to generate the corresponding transformed chunk for each chunk in the final package
- Generate a polyfill chunk
- If the transform target includes legacy browsers, a js file with the name
polyfill-legacy
will be generated by default, containing:- SystemJS runtime
- Any necessary polyfills determined by specified browser targets and actual usage in the bundle
- If the transform target includes a modern browser, a js file with the name
polyfill-modern
is generated by default, containing:- The target browser and the actual usage polyfill in the bundle
- If the transform target includes legacy browsers, a js file with the name
- If the transform target includes legacy browsers
- Generate files with
legacy
names for each js file - Inject
<script nomodule>
tags into generated HTML to load polyfills and legacy bundle in legacy browsers
- Generate files with
- Inject the
import.meta.env.LEGACY
env variable, which will only betrue
in the legacy production build, andfalse
in all other cases.
Note: The following analysis is based on vite v2.9.4, @vitejs/plugin-legacy v1.8.1, and the logic of the new version will be changed! ! !
There are two main logics:
- legacy browser related
- modern browser related
The target browser version range, passed to @babel/preset-env.
This option value is also compatible with Browserslist, the default value of defaults
is the value recommended by Browserslist.
The relevant source code is as follows:
// packages/plugin-legacy/index.js
let babel
const loadBabel = () => babel || (babel = require('@babel/standalone'))
function viteLegacyPlugin(options = {}) {
// default target browser range
const targets = options.targets || 'defaults'
...
const legacyPostPlugin = {
name: 'vite:legacy-post-process',
enforce: 'post',
apply: 'build',
...
renderChunk(raw, chunk, opts) {
// babel transform
const { code, map } = loadBabel().transform(raw, {
...
presets: [
...
// @babel/preset-env
[
'env',
{
targets, // pass the target browser to @babel/preset-env
modules: false,
bugfixes: true,
loose: false,
useBuiltIns: needPolyfills ? 'usage' : false,
corejs: needPolyfills
? {
version: require('core-js/package.json').version,
proposals: false
}
: undefined,
shippedProposals: true,
ignoreBrowserslistConfig: options.ignoreBrowserslistConfig
}
]
]
})
return { code, map }
}
}
}
Whether to generate legacy browser chunks
- Default:
true
- Don't generate when set to false, i.e. only compatible with modern browsers
Generate a separate polyfill chunk for modern browsers
- Default:
false
- When set to
true
, the polyfills will be automatically detected based on the target browser version range - When set to a string array, it means to explicitly control which polyfills to include, in this case it will no longer be automatically detected
The relevant source code is as follows:
// packages/plugin-legacy/index.js
function viteLegacyPlugin(options = {}) {
...
// whether to generate legacy browser chunks
const genLegacy = options.renderLegacyChunks !== false
// modern browser's polyfills
const modernPolyfills = new Set()
// if an array of modernPolyfills is passed in, format it and concatenate the full path
if (Array.isArray(options.modernPolyfills)) {
options.modernPolyfills.forEach((i) => {
modernPolyfills.add(
i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js`
)
})
}
...
const legacyPostPlugin = {
name: 'vite:legacy-post-process',
enforce: 'post',
apply: 'build',
...
renderChunk(raw, chunk, opts) {
// logic of modern browser
if (!isLegacyChunk(chunk, opts)) {
// if options.modernPolyfills is true, auto detect polyfills
if (
options.modernPolyfills &&
!Array.isArray(options.modernPolyfills)
) {
// detects needed polyfills, appends to the modernPolyfills collection
detectPolyfills(raw, { esmodules: true }, modernPolyfills)
}
}
// when legacy browser compatibility is not required, subsequent logic is no longer executed
if (!genLegacy) {
return
}
...
// the logic after that is mainly to pass the babel transform syntax and generate chunks corresponding to legacy browsers
},
...
}
// plugins that generate polyfill chunks
const legacyGenerateBundlePlugin = {
name: 'vite:legacy-generate-polyfill-chunk',
apply: 'build',
async generateBundle(opts, bundle) {
// logic of modern browser
if (!isLegacyBundle(bundle, opts)) {
// if there are no polyfills to build, just return
if (!modernPolyfills.size) {
return
}
// build polyfills for modern browsers as polyfill chunks
await buildPolyfillChunk(
'polyfills-modern',
modernPolyfills, // pass in polyfills required by modern browsers
bundle,
facadeToModernPolyfillMap,
config.build,
options.externalSystemJS
)
return
}
// when legacy browser compatibility is not required, subsequent logic is no longer executed
if (!genLegacy) {
return
}
// the logic after that is to generate polyfill chunks for legacy browsers
...
}
}
}
Add polyfills for legacy browsers
- Default:
true
, a chunk of polyfills will be generated based on the target browser range and actual usage in the final bundle - Set to a list of strings to explicitly control which polyfills to include, this will no longer be auto-detected
The relevant source code is as follows:
// packages/plugin-legacy/index.js
let babel
const loadBabel = () => babel || (babel = require('@babel/standalone'))
function viteLegacyPlugin(options = {}) {
...
const DEFAULT_LEGACY_POLYFILL = [
'core-js/modules/es.promise',
'core-js/modules/es.array.iterator'
]
// polyfills required by legacy browsers
const legacyPolyfills = new Set(DEFAULT_LEGACY_POLYFILL)
// if an array of polyfills is passed in, format it and add it to the polyfills collection required by legacy browsers
if (Array.isArray(options.polyfills)) {
options.polyfills.forEach((i) => {
if (i.startsWith(`regenerator`)) {
legacyPolyfills.add(`regenerator-runtime/runtime.js`)
} else {
legacyPolyfills.add(
i.includes('/') ? `core-js/${i}` : `core-js/modules/${i}.js`
)
}
})
}
...
const legacyPostPlugin = {
name: 'vite:legacy-post-process',
enforce: 'post',
apply: 'build',
...
renderChunk(raw, chunk, opts) {
...
// the above is the logic of modern browsers
...
// if options.polyfills is set, and the value is not an array, it is assumed that polyfills need to be auto-detected
// default true
const needPolyfills = options.polyfills !== false && !Array.isArray(options.polyfills)
...
// babel transform
const { code, map } = loadBabel().transform(raw, {
...
presets: [
...
// @babel/preset-env
[
'env',
{
targets,
modules: false,
bugfixes: true,
loose: false,
/**
* The essential:
* useBuiltIns
* - 'usage':@babel/preset-env will load the actual polyfills needed
* - false:don't load polyfills
*
* https://babeljs.io/docs/en/babel-preset-env#usebuiltins
*/
useBuiltIns: needPolyfills ? 'usage' : false,
// version of corejs(https://babeljs.io/docs/en/babel-preset-env#corejs)
corejs: needPolyfills
? {
version: require('core-js/package.json').version,
proposals: false
}
: undefined,
shippedProposals: true,
ignoreBrowserslistConfig: options.ignoreBrowserslistConfig
}
]
]
})
return { code, map }
}
}
// plugins that generate polyfill chunks
const legacyGenerateBundlePlugin = {
name: 'vite:legacy-generate-polyfill-chunk',
apply: 'build',
async generateBundle(opts, bundle) {
...
// the above is the logic of modern browsers
...
// generate legacy browser polyfill chunk
if (legacyPolyfills.size || genDynamicFallback) {
...
// generate polyfill chunk
await buildPolyfillChunk(
'polyfills-legacy',
legacyPolyfills, // pass in the polyfills required by legacy browsers
bundle,
facadeToLegacyPolyfillMap,
config.build,
options.externalSystemJS
)
}
}
}
}
Add custom polyfills additionally to legacy browser polyfills.
The relevant source code is as follows:
// packages/plugin-legacy/index.js
function viteLegacyPlugin(options = {}) {
...
const DEFAULT_LEGACY_POLYFILL = [
'core-js/modules/es.promise',
'core-js/modules/es.array.iterator'
]
// polyfills required by legacy browsers
const legacyPolyfills = new Set(DEFAULT_LEGACY_POLYFILL)
...
// add custom polyfills additionally to polyfills
if (Array.isArray(options.additionalLegacyPolyfills)) {
options.additionalLegacyPolyfills.forEach((i) => {
legacyPolyfills.add(i)
})
}
...
}
- Determine whether it is a legacy browser chunk
function isLegacyChunk(chunk, options) {
// mainly based on the format value and whether the filename includes -legacy
return options.format === 'system' && chunk.fileName.includes('-legacy')
}
- Determine whether it is a legacy browser bundle
function isLegacyBundle(bundle, options) {
// mainly based on the format value and whether the filename of the entry block includes -legacy
if (options.format === 'system') {
const entryChunk = Object.values(bundle).find(
(output) => output.type === 'chunk' && output.isEntry
)
return !!entryChunk && entryChunk.fileName.includes('-legacy')
}
return false
}
- Detect required polyfills
function detectPolyfills(code, targets, list) {
// generate ast corresponding to polyfill by babel
const { ast } = loadBabel().transform(code, {
ast: true,
babelrc: false,
configFile: false,
presets: [
[
'env',
{
targets, // target browser range
modules: false,
useBuiltIns: 'usage', // load the polyfills that actually need to be used
corejs: { version: 3, proposals: false }, // version of corejs
shippedProposals: true,
ignoreBrowserslistConfig: true
}
]
]
})
// parse ast
for (const node of ast.program.body) {
if (node.type === 'ImportDeclaration') {
const source = node.source.value
if (
source.startsWith('core-js/') ||
source.startsWith('regenerator-runtime/')
) {
// append to the incoming collection
list.add(source)
}
}
}
}
- Build the polyfill chunk
async function buildPolyfillChunk(
name,
imports,
bundle,
facadeToChunkMap,
buildOptions,
externalSystemJS
) {
...
// vite build
const res = await build({
...
// load polyfills
plugins: [polyfillsPlugin(imports, externalSystemJS)],
build: {
write: false,
target: false,
minify,
assetsDir,
rollupOptions: {
input: {
[name]: polyfillId
},
output: {
format: name.includes('legacy') ? 'iife' : 'es',
manualChunks: undefined
}
}
}
})
// get the built polyfill chunk
const _polyfillChunk = Array.isArray(res) ? res[0] : res
const polyfillChunk = _polyfillChunk.output[0]
...
// add the built polyfill chunk to the bundle
bundle[polyfillChunk.name] = polyfillChunk
}
const polyfillId = '\0vite/legacy-polyfills'
function polyfillsPlugin(imports, externalSystemJS) {
return {
name: 'vite:legacy-polyfills',
...
load(id) {
if (id === polyfillId) {
return (
[...imports].map((i) => `import "${i}";`).join('') +
(externalSystemJS ? '' : `import "systemjs/dist/s.min.js";`)
)
}
}
}
}