Vite 浏览器兼容与 Legacy 构建:build.target、Polyfill 与兼容插件

Vite 的浏览器兼容策略全解:build.target 与 esbuild 转译目标、现代 vs 遗留浏览器双构建(@vitejs/plugin-legacy)、polyfill 的按需加载(Polyfill.io/core-js)、browserslist 配置、动态 import 与语法降级、以及兼容性测试与决策。

引言

“为什么老 IE/旧浏览器打开是白屏?"——Vite 默认的 build.target 是 现代浏览器(原生 ESM、较新语法),意味着旧浏览器直接打不开。本文讲清 Vite 的兼容策略:先讲 build.target 与 esbuild 转译的关系(Vite 到底"降级"了什么、没降级什么),再讲现代 vs 遗留的双构建方案(@vitejs/plugin-legacy:给新浏览器发新包、给老浏览器发兼容包),接着讲 polyfill 的取舍(core-js、Polyfill.io、按需注入),最后给 browserslist 配置、兼容性测试方法(caniuse/真实设备)与一套决策清单。

前置:/vite-build-optimization/(构建产物)、/vite-config-guide/(构建配置)、/vite-env-production-best-practices/(生产构建)。浏览器平台背景见 [[frontend]]。


目录


1. Vite 的默认目标:现代浏览器

Vite 的两大前提都指向现代浏览器:

- 开发模式:浏览器直接加载原生 ESM 模块
- 生产构建:build.target 默认 'modules'(≈ 基线 2020+)

默认支持什么(esbuild 的 modules 目标):

- 原生 ESM(<script type="module">)
- 较新语法:可选链 ?. 、空值合并 ?? 、BigInt、逻辑赋值
- 不支持的:旧浏览器(无 ESM)、IE 全系、Safari < 10.1 等

后果:老浏览器拿到产物 → SyntaxError 白屏(语法不识别,直接不执行)。

判断你的用户群体:

- 内部系统/桌面工具:用户可控 → 现代目标即可
- 公网 ToC:存在旧设备 → 要 legacy 构建
- 老年/政务/教育场景:旧浏览器占比高 → legacy + polyfill 全套

不要误判:build.target 调成 es5 不代表"全兼容”——语法降级 ≠ polyfill 注入,ESM/API 缺失还是要 polyfill。

记忆:Vite 默认只服务现代浏览器(原生 ESM + 2020+ 语法)——旧浏览器要兼容,先明确’谁是你的用户’再决定降级深度。


2. build.target 与 esbuild 转译

build.target 控制语法转译目标:

// vite.config.js
export default {
  build: {
    target: "es2018",   // 或 'modules'(默认)/ 具体浏览器版本
  },
};

esbuild 转译能做什么:

✓ 语法降级:可选链 → 三元、空值合并 → 逻辑表达式、async/await → ...
✓ 转换 class 字段、BigInt 字面量
✗ 不注入 polyfill:Promise.finally、Array.includes 等 API 缺失不管
✗ 不降级动态 import 到非 ESM 方案(除非 legacy 插件)

target 值的写法:

'build.target': 'es2018'         → ES 版本
'chrome87' / 'safari14' / 'edge88' → 具体浏览器
'esnext'                        → 完全不做语法降级

一个关键限制:target: 'es5' 在 Vite 生产里已不支持——因为 Vite 产物本身依赖 ESM 架构。真要让老浏览器跑,只能走 legacy 双构建(下节)。

权衡:

target 越新 → 产物越小(语法更紧凑)
target 越旧 → 兼容越广但产物越大(转译膨胀)
→ 别盲目 es5:对大多数项目,'modules' + legacy 双构建是最优解

记忆:build.target 管语法降级、不注入 API polyfill;现代浏览器用默认 ‘modules’,老浏览器走 legacy 双构建而非单纯降 target。


3. 遗留浏览器的双构建方案

核心思路:构建两套产物,按浏览器能力加载:

现代构建:现代语法 + 原生 ESM → 新浏览器用(小、快)
遗留构建:降级语法 + 兼容加载 → 老浏览器用(大、稳)

加载判定(<script type="module"> 支持度):

<!-- 现代浏览器:type="module" 生效 -->
<script type="module" src="/assets/modern-index.js"></script>

<!-- 老浏览器:不认识 module 标签 → 跳过上面的,走这里 -->
<script nomodule src="/assets/legacy-index.js"></script>

为什么双构建而不是单 es5:

- 现代浏览器用现代包 → 体积小、性能好
- 老浏览器用兼容包 → 能用就行
- 单 es5 方案 = 所有人吃最差性能(现代用户被拖累)

这正是 @vitejs/plugin-legacy 帮你做的:一次 vite build,产出 index-[hash].js + index-legacy-[hash].js 两套,HTML 里自动排布 module/nomodule。

记忆:双构建 = 现代包 + legacy 包、用 module/nomodule 分流——现代用户不吃亏、老用户能用,是兼容的正确姿势。


4. @vitejs/plugin-legacy 原理与配置

安装与配置:

npm i -D @vitejs/plugin-legacy
// vite.config.js
import legacy from "@vitejs/plugin-legacy";

export default {
  plugins: [
    legacy({
      targets: ["ie >= 11", "chrome >= 49", "safari >= 10"],
      additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
    }),
  ],
};

它做了什么:

- 用 Babel/Terser 生成 legacy 版(语法降级到 targets 允许的范围)
- 按 targets 生成 polyfill 清单(基于 core-js)
- 产出两套资源 + HTML 里 module/nomodule 分流
- 老浏览器先加载 legacy polyfill(Promise 等),再跑 legacy 主包

关键配置项:

配置作用
targets兼容到哪些浏览器(browserslist)
additionalLegacyPolyfills追加 polyfill(如 regenerator)
modernPolyfills现代包也带的 polyfill
renderLegacyChunks是否生成 legacy chunk(默认 true)

注意:

- legacy 构建会让产物变大(两套)、构建变慢(Babel 二次转译)
- 若用户群全是现代浏览器 → 不装这个插件,最省
- Safari 动态 import 的坑:现代包也可能需要 polyfill(下节)

记忆:plugin-legacy = 双构建 + polyfill + module/nomodule 分流,targets 用 browserslist 声明;代价是产物与构建时间变大——用户全现代就别装。


5. Polyfill:要不要带、带多少

polyfill 的本质:给缺失的 API(不是语法)打补丁——Promise、Array.prototype.includes、fetch 等。

三种做法:

① 全量 core-js(大而全)
   import "core-js";        // 全部 polyfill → 体积大但稳
② 按需(babel/preset-env + useBuiltIns)
   只注入 targets 缺的 → 体积适中
③ Polyfill.io 服务(运行时按 UA 下发)
   <script src="https://polyfill.io/v3/polyfill.min.js?features=Promise,fetch">
   缺点:外部依赖、按 UA 缓存策略复杂

Vite/legacy 里怎么用:

// 现代包:一般不带 polyfill(现代浏览器自带)
// legacy 包:plugin-legacy 按 targets 自动注入 core-js polyfill

polyfill 的取舍表:

场景做法
全现代用户不带 polyfill
有老浏览器 + 网络好legacy 插件按需注入
老浏览器 + 网络差内联必要 polyfill(核心 Promise/fetch)
不想管维护Polyfill.io(接受外部依赖)

一个被忽略的点:现代浏览器也可能缺 API(如 Safari < 15 的 replaceAll)——如果你的代码用了较新 API,现代包也要按需 polyfill(modernPolyfills)。

记忆:polyfill 管 API 缺、不是语法;按 targets 按需注入最优、全量最稳但大、Polyfill.io 省维护但依赖外部——现代包也可能要补新 API。


6. browserslist 与目标声明

browserslist 是跨工具的浏览器目标声明标准(Autoprefixer、Babel、eslint 都读它):

// package.json
{
  "browserslist": [
    "defaults",
    "not ie <= 10",
    "> 0.2%",
    "not dead"
  ]
}

常用查询:

defaults          → 全球默认(>0.5%, 最新两版, 非 dead)
> 1% in CN        → 中国使用率 > 1%
not ie <= 10      → 排除 IE10 及以下
last 2 versions   → 最近两个大版本
covered 95% in CN → 覆盖中国 95% 用户

与 Vite 的衔接:

// plugin-legacy 直接读 browserslist 字段
legacy({ targets: "defaults" })
// 或显式数组
legacy({ targets: ["> 0.2%", "not dead", "not ie <= 10"] })

怎么选目标:用真实统计(GA/站点分析)看用户浏览器分布,选 覆盖 95% 以上 的最低集合——别全要、别一刀切现代。

npx browserslist "> 0.2% in CN" --coverage   # 看覆盖度

记忆:browserslist 是跨工具的目标标准——按真实用户分布选’覆盖 95%+ 的最小集合’,plugin-legacy 直接读它;用 –coverage 验证覆盖度。


7. 动态 import 与模块语法的降级

现代包的隐形门槛:动态 import。

Vite 产物用 动态 import() 做代码分割——部分老浏览器(Safari < 11、部分 Android)不支持动态 import,即便支持 ESM 也会卡在这里。

plugin-legacy 的额外处理:

- legacy 包把动态 import 转成 SystemJS 加载(插件内置)
- 现代包若也遇到动态 import 兼容问题 → 用 modernPolyfills 或 SystemJS 回退

SystemJS:老浏览器的模块加载器——plugin-legacy 会在 legacy 包引入它做兜底:

<!-- legacy 分支:先加载 SystemJS,再加载 legacy 入口 -->
<script nomodule> System.import('./legacy-index.js') </script>

什么时候要注意:

- 你的代码大量用 React.lazy / 动态 import → 老浏览器更依赖 SystemJS 兜底
- 只兼容"现代 + 稍旧"(如 Chrome 70+)→ 动态 import 基本没问题
- Safari 10.x 特殊:支持 ESM 但不支持动态 import → 常被坑

记忆:动态 import 是 ESM 的隐形门槛——plugin-legacy 用 SystemJS 给老浏览器兜底;Safari 10 这类’有 ESM 没动态 import’的浏览器最容易翻车。


8. 兼容性测试:从 caniuse 到真实设备

测试金字塔:

① caniuse.com 查 API 支持 → 规划 polyfill(静态)
② 浏览器模拟(Playwright/BrowserStack) → 自动化冒烟
③ 真实设备/降级网速 → 最终验收

自动化测试(Playwright 多浏览器):

// playwright.config.js
const devices = ["Desktop Chrome", "Safari 14", "Safari 11"];
// 或 BrowserStack/SauceLabs 的真机矩阵

关键检查点:

- 首页能渲染(非白屏)
- 动态 import 的懒加载路由能进
- 表单/交互正常(polyfill 是否影响行为)
- 网络节流下(3G)首屏仍可用

降级验证:

# 本地验证 legacy 包
npx vite build && npx serve dist
# 用旧浏览器打开(或 Playwright 模拟),看是否走 nomodule
# DevTools → Application → 检查 <script nomodule> 是否执行

记忆:兼容测试三阶——caniuse 规划、Playwright/真机矩阵自动化、真实设备+节流验收;重点盯’动态 import 路由’与’polyfill 行为’。


9. 决策清单与权衡

一套兼容决策清单:

□ 用户浏览器分布?(GA/站点分析)
□ 目标覆盖:> 95% 的最小 browserslist 集合
□ 是否需要 legacy 双构建?(有 IE/旧 Safari/旧 Android 就装)
□ polyfill 策略:按需注入 or 外部服务?
□ 现代包是否要补新 API polyfill?(用了 replaceAll 等)
□ 动态 import 场景多不多?(决定 SystemJS 依赖程度)
□ 测试矩阵:模拟 + 真机 + 节流
□ 性能代价:legacy 会让产物变大、构建变慢——值不值?

权衡表:

用户群方案代价
全现代默认无 legacy最小
少量旧浏览器legacy 双构建产物+构建成本
大量旧设备legacy + 完整 polyfill + 测试矩阵高成本、保可用
内部可控强制现代浏览器省事、接受白屏

记忆:兼容决策 = 用户分布 + 目标覆盖 95% + 按需 polyfill + 动态 import 兜底 + 测试矩阵;legacy 有代价,‘值不值’按真实用户算,别为想象中的旧浏览器买单。


10. 速查表与一句话记忆

主题结论
默认目标现代浏览器(原生 ESM)
语法降级build.target(esbuild,不注入 polyfill)
老浏览器plugin-legacy 双构建 + module/nomodule
polyfill按 targets 按需注入,现代包也要补新 API
目标声明browserslist(跨工具)
动态 importSystemJS 兜底(legacy)
测试caniuse + Playwright + 真机/节流
决策用户分布 → 覆盖 95% → 最小代价

一句话记忆:Vite 默认只服务现代浏览器(原生 ESM)——build.target 管语法降级但不管 API polyfill;要兼容旧浏览器就上 plugin-legacy 双构建(现代包 + legacy 包 + module/nomodule 分流 + SystemJS 兜动态 import);polyfill 按 targets 按需注入、现代包也要盯新 API;browserslist 按真实用户分布选覆盖 95% 的最小集合;caniuse 规划 + Playwright/真机矩阵验证;legacy 有产物与构建成本,先问’用户里有多少旧浏览器’再决定——兼容不是全要,是算清代价后的精准投放。


延伸阅读

  • /vite-build-optimization/ — 构建产物与体积权衡
  • /vite-config-guide/ — build.target 配置
  • /vite-env-production-best-practices/ — 生产构建最佳实践
  • /vite-framework-integration/ — 框架与浏览器兼容
  • [[frontend]] — 前端兼容与降级策略
  • [[testing]] — Playwright 浏览器测试

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「vite」更多文章

  1. Vite 静态资源与媒体资产处理:图片、字体、SVG 与 Worker
  2. Vite 环境变量与生产构建最佳实践:import.meta.env、构建模式与产物优化
  3. Vite 测试实战:Vitest 单元测试、组件测试与 E2E 测试