前端开发:如何优雅引入polyfill

青铜:全局全量引入

最简单粗暴的方案,在HTML或JS入口直接引入完整的polyfill文件。

html 复制代码
<!-- 在<head>或<body>开头 -->
<script src="./polyfill.js"></script>
js 复制代码
// 或在JS入口文件第一行
import "./polyfill.js";

优点:

  • 引入极其省事,一行代码搞定
  • 覆盖全面,不用担心遗漏任何特性

缺点:

  • 体积巨大,通常200KB+(未压缩)
  • 不管实际用到多少新特性,全部打包进来
  • 首屏加载性能灾难,严重影响用户体验

💡 青铜思维:"能用就行,管他体积多大?"

白银:全局按需引入

进阶方案:编写构建策略,在编译时扫描代码,收集实际用到的polyfill依赖,最后生成一个精简的polyfill.js在入口引入。

这个构建策略可以通过AST分析实现------扫描代码中的新特性用法(如Array.prototype.includes、Promise、async/await等),然后只打包对应的polyfill模块。

优点:

  • 相比青铜方案,总体积大幅减少(可能从200KB降到50KB)

缺点:

  • 首屏仍需加载全部polyfill,只是体积小了
  • 对于大型应用,首屏polyfill体积仍可能成为瓶颈
  • 无法实现代码分割,所有页面都加载相同的polyfill集合

💡 白银思维:"我知道我要什么,但只能一次性给"

钻石:useBuiltIns: 'usage'

目前业界主流方案,通过Babel的@babel/preset-env配置,让构建工具根据引用树智能分包。

js 复制代码
// babel.config.js
module.exports = {
    presets: [
        ['@babel/preset-env', {
            useBuiltIns: 'usage',
            corejs: 3,
            targets: {
            chrome: '60',
                ie: '11'
            }
        }]
    ]
};

Babel会在转译时扫描每个文件,自动注入该文件所需的polyfill。然后再配合rollup等打包工具,可以实现依赖的最优分配方式:公用部分提取成公用chunk、单独使用的,只会和使用它的模块合到同个chunk中。

优点:

  • 智能分包,首屏不会加载全部polyfill
  • 自动化程度高,无需手动管理polyfill列表

缺点:

  1. 只支持core-js:大量core-js中没有的polyfill无法支持(如某些DOM API、特殊的浏览器bug修复等)

  2. 深层依赖不受配置影响:core-js的模块依赖是固定的。很多polyfill底层依赖,比如Object.defineProperty,即使你的目标浏览器原生支持,也会被打包进来

  3. 无法分辨降级场景:有些新特性是无法完美模拟的(sham),代码中可能存在判断和降级逻辑,如:

    javaScript 复制代码
    if (typeof Symbol === 'function') {
        // 使用原生Symbol
    } else {
        // 降级处理
    }

    Babel看到Symbol就注入polyfill,可能导致判断逻辑失效

💡 钻石思维:"让工具帮我做,但工具有自己的局限"

王者:rollup-plugin-polyfill-inject

这是真正的王者方案------构建时精准注入,零运行时开销,极致的代码体积控制。

这个Rollup插件的核心思想是:在打包阶段分析AST,精确识别需要polyfill的位置,然后智能注入import语句。

核心特性

1. Polluting模式(污染型polyfill)

针对会修改全局原型的polyfill(如core-js):

javaScript 复制代码
// rollup.config.js
import polyfill from 'rollup-plugin-polyfill-inject';

export default {
    plugins: [
        polyfill({
            polluting: {
                ".includes": [
                    "core-js/modules/es.array.includes",
                    "core-js/modules/es.string.includes"
                ],
                "Set": "core-js/modules/es.set",
                "Map": "core-js/modules/es.map"
            }
        })
    ]
};

转换效果:

javaScript 复制代码
// 源代码
console.log([].includes(1));
var set = new Set();

// 构建后自动注入
import "core-js/modules/es.array.includes";
import "core-js/modules/es.set";

console.log([].includes(1));
var set = new Set();

2. Pure模式(非污染型polyfill)

不污染全局,排除typeof判断和window判断:

javaScript 复制代码
polyfill({
    pure: {
        ".padStart": "core-js-pure/actual/string/pad-start",
        "Promise": "core-js-pure/actual/promise"
    }
})

转换效果:

javaScript 复制代码
// 源代码
"abc".padStart(5, "0");
Promise.resolve(42);

// 构建后
import padStart from "core-js-pure/actual/string/pad-start";
import Promise from "core-js-pure/actual/promise";

padStart("abc", 5, "0");
Promise.resolve(42);

3. Getter/Setter转换

更强大的能力------可以替换属性访问为函数调用:

javaScript 复制代码
polyfill({
    getter: {
        ".textContent": "@/utils/getNodeText",
        "document.currentScript": ["sky-core/utils/getCurrentScript", 'getCurrentScript']
    },
    setter: {
        ".textContent": "@/utils/setNodeText",
        "document.title": ["@/utils/jsBridge", 'setActivityTitle']
    }
})

转换效果:

javaScript 复制代码
// 转换前
var text = document.getElementById("foo").textContent;
document.title = "New Title";

// 转换后
import getNodeText from "@/utils/getNodeText";
import { setActivityTitle } from "@/utils/jsBridge";

var text = getNodeText(document.getElementById("foo"));
setActivityTitle("New Title");

4. Error特殊处理

针对类继承和Error cause特性的精准注入:

javaScript 复制代码
polyfill({
    polluting: {
        "AggregateError": "sky-core/polyfill/AggregateError"
    },
    error: {
        "Error": "sky-core/pure/Error"  // 只在new Error(..., { cause })时注入
    }
})

转换效果:

javaScript 复制代码
// 转换前
var err = new Error("error");
var err2 = new Error("error", { cause: 1 });
var err3 = new AggregateError([], "msg");
var err4 = new AggregateError([], "msg", { cause: 1 });

// 转换后
import "sky-core/polyfill/AggregateError";
import CauseError from "sky-core/pure/Error";
import CauseAggregateError from "sky-core/pure/AggregateError";

new Error("error");
new CauseError("error", { cause: 1 });
new AggregateError([], "msg");
new CauseAggregateError([], "msg", { cause: 1 });

5. Timer回调参数检测

智能检测setTimeout/setInterval是否传入了参数:

javaScript 复制代码
polyfill({
    timer: {
        "setTimeout": "sky-core/pure/setTimeout",
        "setInterval": "sky-core/pure/setInterval"
    }
})

转换效果:

javaScript 复制代码
// 源代码
let args = [0, 1, 2];
setTimeout(function() {}, 0);           // 不转换
setTimeout(function() {}, ...args);     // 转换!
setTimeout.apply(window, args);         // 转换!

// 构建后
import argsSetTimeout from "sky-core/pure/setTimeout";

let args = [0, 1, 2];
setTimeout(function() {}, 0);
argsSetTimeout(function() {}, ...args);
argsSetTimeout.apply(window, args);

💡 王者选手的思维:"我要在构建阶段就完成一切优化,运行时零负担"

还有高手?typescript-plugin-polyfill

如果说rollup-plugin是"构建时AST分析"的王者,那typescript-plugin就是 "基于类型系统的精准打击" ------真正的宗师级方案。

核心突破:利用TypeScript类型信息

传统方案的最大痛点:同名方法无法区分类型 。比如.at()方法,数组和字符串都有,但Babel看到xxx.at()时,不知道xxx是数组还是字符串,只能两个polyfill都引入。

typescript-plugin-polyfill利用TypeScript的类型系统,在编译时精确判断变量类型:

typeScript 复制代码
// 源代码
const arr: string[] = ['a', 'b'];
const str: string = 'hello';

arr.at(0);  // TypeScript知道这是数组
str.at(0);  // TypeScript知道这是字符串

配置方式:

javaScript 复制代码
// 配合Rollup使用
import typescript from '@rollup/plugin-typescript';
import polyfillPlugin from 'typescript-plugin-polyfill';

export default {
  plugins: [
    typescript({
      transformers: (program) => ({
        before: [
          polyfillPlugin(program, {
            polluting: {
              at: {
                Array: 'sky-core/polyfill/Array/prototype/at',   // 数组用
                String: 'sky-core/polyfill/String/prototype/at'  // 字符串用
              },
              includes: {
                Array: 'sky-core/polyfill/Array/prototype/includes',
                String: 'sky-core/polyfill/String/prototype/includes'
              }
            }
          })
        ]
      })
    })
  ]
};

精准注入效果:

typeScript 复制代码
// 转换前
const arr: string[] = ['a', 'b'];
const str: string = 'hello';

arr.at(0);
str.at(0);

// 转换后 - 基于类型只引入需要的polyfill
import 'sky-core/polyfill/Array/prototype/at';
import 'sky-core/polyfill/String/prototype/at';

const arr: string[] = ['a', 'b'];
const str: string = 'hello';

arr.at(0);
str.at(0);

💡 宗师级思维:"类型即契约,利用类型系统做最精准的决策"


总结

一般的polyfill引入方式只是够用,要真正的优雅,还得靠 rollup-plugin-polyfill-inject 和 typescript-plugin-polyfill。当然一般业务开发者是无需考虑兼容性的,处理构建问题是架构层做的。

本文提到的工具:

相关推荐
cindershade1 天前
首屏不是 dist:用“加载账本”治理 Vite 的大包
性能优化·vite
liangshanbo12152 天前
Webpack vs Vite 底层原理高频追问的问题
前端·webpack·node.js
liangshanbo12152 天前
面试题:AI 对话上下文超限怎么办?
面试·webpack·devops
liangshanbo12153 天前
Webpack 文件指纹面试题整理
前端·webpack·node.js
liangshanbo12153 天前
Webpack 常见 Plugin 面试题
前端·webpack·node.js
liangshanbo12153 天前
Webpack Tree Shaking 面试题整理
前端·webpack·node.js
陪我一起学编程3 天前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式
liangshanbo12153 天前
Webpack的分包策略面试题
前端·webpack·node.js
liangshanbo12155 天前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
晚风醉蝶6 天前
webpack 模块提取
前端·webpack·node.js·ast·逆向分析