在 2017 年,我写了一篇文章,展示了网络开发者如何能够将 ES6+ 代码(即 ES2015)部署到生产环境中,而无需将其转译为 ES5。这种技术对于希望自由编写现代代码而不必担心转译器或 polyfill 膨胀问题的网站开发者来说是一种解放。
不幸的是,虽然许多网站开发者能够使用这种技术,但大多数 JavaScript 库的作者却无法做到。
库作者面临的限制要比网站开发者多得多,因为他们无法控制自己的代码如何部署。此外,由于许多流行的构建工具[推荐](#不要转译 node-modules 建议)开发者从转译中排除他们的 node_modules
目录,库作者被迫非常保守——通常需要全部转译回 ES5,以避免可能破坏站点。
但那是七年前的事了,自那时以来,JavaScript 工具领域取得了巨大的进步。浏览器格局也发生了巨大变化。最值得注意的是,IE 11,最后一个支持 ES5 的浏览器,在 2022 年停止了微软的支持,这意味着许多企业终于可以停止支持它了。
那么,今天网络上 ES5 的现状如何呢?对于准备生产代码的网络开发者来说,最佳实践是什么?
本文查看了我们拥有的数据来回答这些问题。它还提供了一些具体的建议(基于这些数据),供网站开发者和库作者如何在未来处理旧版浏览器支持。
快速声明
在深入研究 ES5 使用情况的实际数据之前,我想澄清,无论是编写还是发布 ES5 代码,本身都没有错。
JavaScript 引擎对 ES5 代码的优化时间比对现代代码要长得多,所以如果你有仍在工作的旧 ES5 代码,没有必要仅仅为了使其“现代化”而更新它。
然而,如果你使用 ES6+ 语法编写代码,然后使用构建工具将其转译为 ES5,通常会引入大量的 polyfill 和转译器膨胀,这可能会显著增加你最终捆绑包的大小。
为了说明这一点,这里有一个例子:
console.log([1, 2, 3].at(-1));
如果你手动将这段代码转译为 ES5,它可能看起来像这样:
var arr = [1, 2, 3];
console.log(arr[arr.length - 1]);
然而,如果你使用 Babel 转译这行代码,并配置它添加 polyfills —— 即使你将其限制在源代码中基于使用所需的 polyfills —— 它包括了 71 个 core-js 依赖项,并且从 31 字节增加到 11,217 字节压缩后!
这个例子的目的不是羞辱 Babel 或 core-js。这些工具需要能够支持所有可能的 ES6+ 代码,这要求它们考虑到各种边缘情况(尽管这个特定的例子没有任何边缘情况)。
相反,要点是强调选择支持旧版浏览器确实有成本,而且这个成本可能是显著的。
不幸的是,问题实际上比代码膨胀更严重。如果你查看下面的数据,了解当今流行网站实际上是如何转译和部署他们的代码到生产的,结果发现大多数网站都在传输转译为 ES5 的代码,但仍然无法在 IE 11 中工作 —— 这意味着转译器和 polyfill 膨胀被 100% 的用户下载,但没有给任何人带来好处。
数据
要了解网络上 ES5 的现状,你必须查看三件事,因为所有这些都在我们作为网络用户接收的最终代码输出中起着关键作用:
- 流行打包器和构建工具的默认配置
- 流行 JavaScript 库中的代码状态
- 网站所有者部署的代码状态
默认打包器和构建工具配置
大多数打包器和构建工具都非常可配置,提供了对最终代码输出几乎无限的控制。然而,在实践中,大多数开发者只是使用默认值,因此默认值非常重要。
这些默认值是什么?具体来说,这些默认值是否导致代码被转译为 ES5?
为了回答这个问题,我查看了一些根据最新的 State of JS 调查(2023 年)最流行的构建工具生成的输出,大致按流行度排序:
工具 | 默认为 ES5? | 注释 |
---|---|---|
Browserlist | 否 | 本身不是构建工具,但许多构建工具内部使用它,并且是最流行的开源工具,用于配置浏览器支持目标。[defaults](https://browsersl.ist/#q=defaults) 设置不再包括任何 ES5 浏览器。最后一个是 IE 11,它在版本 4.21 中被标记为死亡。 |
Babel | 是 | Babel 的文档建议设置一个 [targets](https://babeljs.io/docs/options#targets) 选项(它使用 Browserlist),但如果未指定,它将将所有代码转译为 ES5。 |
webpack | 否 | 默认情况下,webpack 不转译任何代码。大多数 webpack 用户包括 [babel-loader](https://webpack.js.org/loaders/babel-loader/) ,并且 webpack 的使用示例 建议设置 targets: "defaults" 。 |
TypeScript (tsc) | 是 | TypeScript 的默认 [target](https://www.typescriptlang.org/tsconfig/#target) 选项是 ES5。 |
Next.js | 否 | Next.js 使用 Babel 进行转译,并且默认设置一个 Browserlist 配置,目标是 "现代浏览器"(即支持 ES 模块的浏览器)。 |
esbuild | 否 | esbuild 默认不转译。你可以设置自定义目标以启用转译,但 ES5 不作为转译目标支持。 |
Vite | 否 | Vite 使用 esbuild,并且默认为 "现代浏览器" 设置自定义目标(即支持 ES 模块的浏览器)。Vite 允许用户安装 插件 如果他们需要支持旧版浏览器。 |
Rollup | 否 | Rollup 默认不转译。许多 Rollup 用户安装 @rollup/plugin-babel,在这种情况下,使用 Babel 默认值。 |
Parcel | 否 | Parcel 自动应用差异化服务,具有可自定义的目标。 |
Closure Compiler | 否 | 默认 为 ECMASCRIPT_NEXT ,这是最新的一组稳定 ES 功能。 |
如表所示,绝大多数打包器和构建工具不再默认转译为 ES5。同样值得注意的是,新工具根本不支持 ES5,这表明趋势正在朝这个方向发展。
话虽如此,Babel 仍然是转译 JavaScript 最流行的工具,因此在网络上转译为 ES5 仍然相当普遍(见下文 野外 ES5 使用情况 了解更多细节)。
流行的 JavaScript 库
除了查看流行的构建工具外,我还查看了一些今天使用最多的库(同样基于 State of JS 调查,大致按流行度排序):
为了测试这些库中的每一个,我创建了一个只导入特定库的捆绑入口点,使用库文档中的一个代码示例。然后我使用 Rollup 和 Webpack 来测试输出,看看它是否包含了任何 ES6+ 语法(具体来说,任何在 IE 11 中不受支持的 ES6+ 语法)。
这是我发现的:
库 | 包含 ES6+ 语法? | 注释 |
---|---|---|
Lodash | 否 | 仅限 ES5 |
React | 否 | 仅限 ES5 |
date-fns | 是 | 箭头函数 |
three.js | 是 | async/await, 箭头函数, 展开, 解构 |
d3 | 是 | 箭头函数, 展开, 解构 |
Framer-motion | 是 | 箭头函数, 展开, 解构 |
greensock | 否 | 仅限 ES5 |
dayjs | 否 | 仅限 ES5 |
Zod | 是 | async/await, 箭头函数, 展开, 解构 |
RxJS | 是 | 箭头函数 |
immer | 是 | 箭头函数, 展开, 解构 |
luxon | 是 | async/await, 箭头函数, 展开, 解构 |
react-query | 否 | 仅限 ES5(捆绑 Babel 助手) |
正如上面的结果所示,许多流行的 JavaScript 库现在发布 ES6+ 语法。
这一点值得注意,因为正如我之前提到的,大多数使用 Babel 转译源文件并在捆绑时明确配置他们的捆绑器不转译 node_modules
目录中的任何内容的开发人员——这是库作者历史上觉得他们需要继续转译为 ES5 的主要原因。
例如,截至本文发布时(2024 年 9 月):
- Webpack 的
babel-loader
文档推荐一个配置,该配置排除了node_modules
。 - Rollup 的
plugin-babel
文档推荐 排除node_modules
,并且还推荐库作者不发布 ES6 代码。
并且 TypeScript(tsc
),继 Babel 之后第二大流行的转译工具,只会转译项目自己的代码文件。它不会转译 node_modules
中的项目依赖项。
这为任何想要支持 ES5 并使用 Babel 或 tsc
转译代码的网站创造了一个问题。除非他们对构建管道的各个部分如何相互作用有复杂的了解,并且除非他们知道如何正确配置每一个,他们可能会在没有意识到的情况下将 ES6+ 代码捆绑在他们的 ES5 代码中。
那么,这是否真的对真实网站造成了问题,或者他们中的大多数是否正确配置了他们的工具?下一节将查看 HTTP Archive 的数据来回答这个问题。
注意: 上表中的一些库发布了 ES5 和 ES6+ 版本,通常将 ES5 版本设置在
package.main
字段上,将 ES6+ 版本设置在package.module
或package.exports
字段上。在这些情况下,我只查看了捆绑器在使用默认配置时拉入的脚本版本(因为大多数人使用它),并且今天的捆绑器默认使用package.module
或package.exports
而不是package.main
(见:[1], [2], [3])。
野外 ES5 使用情况
开发者用来将 ES6+ 代码转译为 ES5 的三个主要工具是:
- Babel
- TypeScript (tsc)
- Closure Compiler(即 Google 内部的 JSCompiler)
这三种工具都包括某种形式的 polyfills 和所谓的 ES5 “助手”函数,以避免最终输出中的重复。这些工具使用的最常见的 ES5 助手函数库是:babel-helpers, core-js, regenerator-runtime, tslib, 和 $jscomp。
这些助手库中的许多函数足够独特,以至于可以通过查询 HTTP Archive 来检测(即使在压缩代码中)哪些网站正在使用它们。搜索这些助手函数的存在——而不是标准 ES5 语法(例如 var
或非箭头 function
)——有助于区分手工编写的旧 ES5 代码(通常相当优化)和由转译器生成的新 ES5 代码(通常相当膨胀)。
我在 HTTP Archive 上进行了搜索,看看流行的网站(前 10,000 个,基于 CrUX 流行度排名)包含这些助手脚本捆绑包中的频率。我还想知道网站为生产环境服务未转译的 ES6+ 语法有多普遍。
这是我发现的(完整结果):
- 89% 的网站至少提供一个包含未转译 ES6+ 语法的 JavaScript 文件。
- 79% 的网站至少提供一个包含 ES5 助手代码的 JavaScript 文件。
- 68% 的网站至少提供一个包含 两者 的 JavaScript 文件,即同一文件中包含 ES5 助手代码和未转译的 ES6+ 语法。
这最后一个发现真的让我大吃一惊。
重申我之前说过的话——因为它值得重复——如果浏览器不支持 ES6+ 语法(例如 IE 11),那么它在尝试加载包含 ES6+ 语法的脚本文件时会出错。如果浏览器 支持 ES6+ 语法,那么它不需要任何 ES5 助手代码或任何旧版 polyfills。绝对没有理由同时包含两者。
为了再次检查这个查询的结果是否准确,我手动测试了列表上的 20 个随机网站,并确认它们确实在某些脚本捆绑包中包含了 ES5 助手代码以及 ES6+ 语法。我还手动在 IE 11 中访问了这些网站,并确认那些脚本捆绑包确实无法加载。
请记住,这些不仅仅是互联网上的随机网站。这些是世界上最受欢迎 10,000 个网站,占全球所有网络使用量的绝大多数。
所有这些都意味着什么?
对于一个网站来说,提供包含 ES5 助手和未转译 ES6+ 语法的代码,实际上只有两种合理的解释:
- 该网站不需要支持 ES5 浏览器,但它们的一些依赖项转译为 ES5,因此 ES5 代码出现在它们的输出中。
- 该网站 打算 支持 ES5 浏览器,但他们没有意识到他们的一些依赖项发布了未转译的 ES6+ 语法,并且他们没有配置他们的捆绑器来转译
node_modules
中的代码。
不管解释如何,这么多世界上最受欢迎的网站都在提供如此多不必要的代码,这是一个强有力的指标,表明我们的工具当前推荐的默认设置不起作用。
如果这些数据有任何一线希望,那就是对我来说非常清楚,放弃 IE 支持对大多数企业来说不会有明显影响。如果所有这些大公司显然没有受到这些破坏的 IE 体验的影响,你的公司可能也不会。
建议
给库作者
库作者应该将代码转译为 ES5 的最初理由是大多数网站无论如何都需要转译为 ES5。然而,鉴于目前排名前 10,000 的网站中有 89% 目前传输一些未转译的 ES6+ 语法,那个理由不再有效。
鉴于本文中呈现的数据,对于 JavaScript 库作者来说,再将他们的代码转译为 ES5 绝对没有意义。
实际上,库作者没有关于导入他们网站浏览器支持需求的信息,所以他们为所有库的消费者做出这个决定是没有意义的。同时,库作者不应该假设所有库的消费者都能运行一个复杂的构建过程,所以他们发布的代码使用完全标准的 JavaScript 并在当前广泛使用的浏览器集中工作是很重要的。
那么,库作者应该选择什么目标?在我看来,库作者的最佳解决方案是使用 Baseline—具体来说 只包括 Baseline Widely Available 功能在任何发布的代码中。
如果你不熟悉 Baseline,这是 W3C 内 WebDX Community Group 的一项努力,旨在帮助开发者轻松识别所有主要浏览器和跨桌面和移动浏览器渲染引擎稳定且广泛支持的功能。如果一个功能在所有四个主要浏览器的稳定版本中可用至少 30 个月,则被认为是 Baseline Widely Available。
定位像 Baseline Widely Available 这样的东西的主要好处是它是一个移动目标,这意味着它不会像定位 ES5 那样陷入过去(以及目前 Next.js、Vite 和 Parcel 使用的 esmodule
目标正在发生的情况)。
库作者可以配置他们的构建系统,现在就使用以下 Browserlist 查询定位 Baseline Widely Available 功能(对于任何支持 Browserlist 的工具):
targets: [
'chrome >0 and last 2.5 years',
'edge >0 and last 2.5 years',
'safari >0 and last 2.5 years',
'firefox >0 and last 2.5 years',
'and_chr >0 and last 2.5 years',
'and_ff >0 and last 2.5 years',
'ios >0 and last 2.5 years',
]
注意: 有一个开放的 功能请求 将 Baseline 支持添加到 Browserlist,这将简化上述查询为仅为 “baseline widely available”。
如果一个网站需要支持超出 Baseline Widely Available 覆盖范围的更多浏览器,那是 100% 可以的。那个网站总是可以配置他们的构建系统来进一步转译他们正在导入的任何库。关键是这个决定最好由网站开发者来做,而不是库作者。
给网站开发者
这么多受欢迎的网站在同一个脚本捆绑包中传输未转译的 ES6+ 语法和 ES5 助手,这清楚地表明排除 node_modules
目录不进行转译的做法不是一个好的实践。
自 2017 年以来,我一直在争论这不是一个好的做法since 2017,但我交谈过的大多数开发者不想遵循这个建议,因为这样做会减慢他们的构建速度。
然而,这些天来,构建工具已经显著加快。此外,网站可以配置他们的构建在生产构建时只处理 node_modules
中的代码。在开发中,代码应该在开发者使用的任何浏览器上正常运行,特别是如果库作者采纳了我上面给出的建议并定位 Baseline Widely Available。
最后的思考
正如我上面提到的,本文的目的不是基于这些发现来指责或羞辱网站或工具作者。我还想确保清楚,我绝对不是在建议网站仍然应该支持 IE 11。如果有的话,这些发现表明,即使是大型全球客户群的公司,支持 IE 11 也不是大多数企业的必要条件。
我希望读者从本文中得到的要点是:
ES5 不再是构建工具或 JavaScript 库应该默认定位的东西。
如果工具仍然想提供 ES5 支持,它应该是具有特定支持需求的个别网站可以选择加入的东西。
构建工具和库不应该使用固定的浏览器支持政策。
这些政策很快就会过时,正如本文中的数据所突出显示的那样。浏览器支持决策应由网站本身做出,而不是它使用的工具。对于工具和库来说,一个好的浏览器支持政策是 Baseline Widely Available。
导入第三方库的网站开发者应该将这些库作为他们构建过程的一部分进行处理。
不能假设所有库作者都和你有相同的浏览器支持需求。正如本文中的数据所示,在许多情况下,网站开发者可能比他们导入的库有更广泛的浏览器支持需求(因此需要进一步转译它们)。
跨浏览器支持不是你应该完全依赖你的构建工具为你处理的东西。
如果你需要支持一组特定的浏览器,那么你需要测试你的网站以确保它在那些浏览器中工作。