引言
“为什么老 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 的默认目标:现代浏览器
- 2. build.target 与 esbuild 转译
- 3. 遗留浏览器的双构建方案
- 4. @vitejs/plugin-legacy 原理与配置
- 5. Polyfill:要不要带、带多少
- 6. browserslist 与目标声明
- 7. 动态 import 与模块语法的降级
- 8. 兼容性测试:从 caniuse 到真实设备
- 9. 决策清单与权衡
- 10. 速查表与一句话记忆
- 延伸阅读
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(跨工具) |
| 动态 import | SystemJS 兜底(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 浏览器测试
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。