在此 Codelab 中,您将改进这个简单应用的性能,让用户可以对随机猫咪进行评分。了解如何通过最大限度地减少转译的代码量来优化 JavaScript 软件包。
在示例应用中,您可以选择一个字词或表情符号来表达您对每只猫的喜爱程度。当您点击某个按钮时,应用会在当前猫图片下方显示该按钮的值。
测量
在添加任何优化措施之前,最好先检查网站:
- 如需预览网站,请按 View App(查看应用)。然后按 Fullscreen(全屏)。
- 按 `Control+Shift+J`(在 Mac 上为 `Command+Option+J`)打开 DevTools。
- 点击网络标签页。
- 选中 Disable cache(停用缓存)复选框。
- 重新加载应用。
此应用使用的空间超过 80 KB!现在,我们来看看软件包的哪些部分未被使用:
按
Control+Shift+P
(在 Mac 上,按Command+Shift+P
)打开命令菜单。输入
Show Coverage
并按Enter
以显示覆盖率标签页。在覆盖率标签页中,点击重新加载以重新加载应用,同时捕获覆盖率。
查看主软件包使用了多少代码与加载了多少代码:
甚至超过一半的 bundle(44 KB)都未被使用。这是因为其中的许多代码都包含 polyfill,以确保应用在旧版浏览器中正常运行。
使用 @babel/preset-env
JavaScript 语言的语法符合 ECMAScript(也称为 ECMA-262)标准。该规范每年都会发布新版本,其中包含已通过提案流程的新功能。每款主要浏览器在支持这些功能方面总是处于不同的阶段。
该应用中使用了以下 ES2015 功能:
还使用了以下 ES2017 功能:
您可以随意深入研究 src/index.js
中的源代码,了解所有这些内容的使用方式。
最新版 Chrome 支持所有这些功能,但不支持这些功能的其他浏览器又该如何?应用中包含的 Babel 是最常用的库,用于将包含较新语法的代码编译为旧版浏览器和环境可以理解的代码。它通过以下两种方式来实现这一目标:
- Polyfill 插件用于模拟较新的 ES2015 及更高版本的函数,以便即使浏览器不支持其 API,也能使用这些 API。下面是
Array.includes
方法的 polyfill 示例。 - 插件用于将 ES2015 代码(或更高版本)转换为较旧的 ES5 语法。由于这些更改与语法相关(例如箭头函数),因此无法使用 polyfill 来模拟。
查看 package.json
,了解其中包含哪些 Babel 库:
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
@babel/core
是核心 Babel 编译器。这样,所有 Babel 配置都将在项目根目录下的.babelrc
中定义。babel-loader
在 webpack 构建流程中添加了 Babel。
现在,查看 webpack.config.js
,了解如何将 babel-loader
作为规则包含在内:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
@babel/polyfill
为所有较新的 ECMAScript 功能提供了所有必要的 polyfill,以便这些功能可以在不支持它们的环境中运行。已导入到src/index.js.
的顶部
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
用于确定为目标选择的任何浏览器或环境需要哪些转换和 polyfill。
查看 Babel 配置文件 .babelrc
,了解其包含方式:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
这是 Babel 和 webpack 设置。如果您使用的是与 webpack 不同的模块捆绑器,请了解如何在应用中添加 Babel。
.babelrc
中的 targets
属性用于标识要定位到的浏览器。@babel/preset-env
与 browserslist 集成,这意味着您可以在 browserlist 文档中找到可在此字段中使用的兼容查询的完整列表。
"last 2 versions"
值会针对每个浏览器的最后两个版本转译应用中的代码。
调试
如需全面了解浏览器的所有 Babel 目标以及包含的所有转换和 Polyfill,请将 debug
字段添加到 .babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- 点击工具。
- 点击日志。
重新加载应用,然后查看编辑器底部的 Glitch 状态日志。
定位到的浏览器
Babel 会将有关编译过程的大量详细信息记录到控制台,包括已为代码编译的所有目标环境。
请注意,已弃用的浏览器(例如 Internet Explorer)是如何包含在此列表中的。这会带来问题,因为不受支持的浏览器不会添加较新的功能,而 Babel 会继续为它们转译特定语法。如果用户未使用此浏览器访问您的网站,这会不必要地增加软件包的大小。
Babel 还会记录所使用的转换插件列表:
这个列表很长!这些是 Babel 需要使用的所有插件,用于将所有 ES2015 及更高版本的语法转换为适用于所有目标浏览器的旧版语法。
不过,Babel 不会显示使用的任何具体 polyfill:
这是因为系统会直接导入整个 @babel/polyfill
。
单独加载 polyfill
默认情况下,将 @babel/polyfill
导入文件时,Babel 会添加完整 ES2015+ 环境所需的所有 polyfill。如需导入目标浏览器所需的特定 polyfill,请将 useBuiltIns: 'entry'
添加到配置中。
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
重新加载应用。现在,您可以看到包含的所有具体 polyfill:
虽然目前仅包含 "last 2 versions"
所需的 polyfill,但该列表仍然非常长!这是因为,每个较新功能仍包含目标浏览器所需的 polyfill。将该属性的值更改为 usage
,以仅包含代码中使用的功能所需的属性。
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
这样,系统会在需要时自动添加 polyfill。这意味着,您可以移除 src/index.js.
中的 @babel/polyfill
导入项
import "./style.css";
import "@babel/polyfill";
现在,仅包含应用所需的 Polyfill。
应用软件包大小显著缩减。
缩小受支持的浏览器列表
包含的浏览器目标数量仍然相当多,而且使用已弃用的浏览器(例如 Internet Explorer)的用户并不多。将配置更新为以下内容:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
查看提取的软件包的详细信息。
由于应用非常小,因此这些更改实际上没有太大差异。不过,建议您使用浏览器市场份额百分比(例如 ">0.25%"
),并排除您确信用户不会使用的特定浏览器。如需了解详情,请参阅 James Kyle 撰写的“最后 2 个版本”被视为有害一文。
使用 <script type="module">
但仍有改进的空间。虽然移除了许多未使用的 polyfill,但仍有许多 polyfill 在发货时并未被一些浏览器使用。通过使用模块,您可以编写较新的语法并直接将其发送到浏览器,而无需使用任何不必要的 polyfill。
JavaScript 模块是所有主流浏览器支持的一种相对较新的功能。您可以使用 type="module"
属性创建模块,以定义从其他模块导入和导出的脚本。例如:
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
支持 JavaScript 模块(而无需 Babel)的环境中已经支持许多较新的 ECMAScript 功能。这意味着,您可以修改 Babel 配置,以向浏览器发送应用的两个不同版本:
- 此版本适用于支持模块的较新浏览器,其中包含的模块在很大程度上未转译,但文件大小较小
- 包含较大的转译脚本的版本,可在任何旧版浏览器中运行
将 ES 模块与 Babel 搭配使用
如需为两个版本的应用设置不同的 @babel/preset-env
,请移除 .babelrc
文件。您可以为应用的每个版本指定两种不同的编译格式,以将 Babel 设置添加到 webpack 配置中。
首先,将旧版脚本的配置添加到 webpack.config.js
:
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
请注意,此处使用的是值为 false
的 esmodules
,而不是 "@babel/preset-env"
的 targets
值。这意味着 Babel 包含所有必要的转换和 polyfill,可定位到尚不支持 ES 模块的每款浏览器。
将 entry
、cssRule
和 corePlugins
对象添加到 webpack.config.js
文件的开头。这些数据在模块和向浏览器提供的旧版脚本之间共享。
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
现在,同样地,为下面定义了 legacyConfig
的模块脚本创建一个配置对象:
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
这里的主要区别在于,输出文件名使用 .mjs
文件扩展名。此处将 esmodules
值设为了 true,这意味着输出到此模块中的代码是一个更小、更少编译的脚本,在本例中不会经过任何转换,因为所使用的所有功能都已在支持模块的浏览器中受支持。
在文件的最后,将这两种配置导出到单个数组中。
module.exports = [
legacyConfig, moduleConfig
];
现在,它会为支持它的浏览器构建较小的模块,并为旧版浏览器构建较大的转译脚本。
支持模块的浏览器会忽略具有 nomodule
属性的脚本。反之,不支持模块的浏览器会忽略带有 type="module"
的脚本元素。这意味着,您可以添加模块以及编译后的回退。理想情况下,应用的两个版本应位于 index.html
中,如下所示:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
支持模块的浏览器会提取并执行 main.mjs
并忽略 main.bundle.js.
。不支持模块的浏览器则会执行相反操作。
请务必注意,与常规脚本不同,模块脚本默认始终是延迟执行的。如果您希望等效的 nomodule
脚本也被推迟,并且仅在解析后执行,则需要添加 defer
属性:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
此处需要完成的最后一件事是,分别向模块和旧版脚本添加 module
和 nomodule
属性,并在 webpack.config.js
的顶部导入 ScriptExtHtmlWebpackPlugin:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
现在,更新配置中的 plugins
数组以添加此插件:
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
这些插件设置会为所有 .mjs
脚本元素添加 type="module"
属性,并为所有 .js
脚本模块添加 nomodule
属性。
在 HTML 文档中提供模块
最后需要做的是将旧版和新版脚本元素输出到 HTML 文件。很遗憾,用于创建最终 HTML 文件的插件 HTMLWebpackPlugin
目前不支持模块脚本和 nomodule 脚本的输出。虽然有专门用于解决此问题的权宜解决方法和单独的插件(例如 BabelMultiTargetPlugin 和 HTMLWebpackMultiBuildPlugin),但本教程将采用更简单的方法手动添加模块脚本元素。
将以下代码添加到文件末尾的 src/index.js
中:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
现在,在支持模块的浏览器(例如最新版 Chrome)中加载该应用。
系统只会提取模块,由于该模块大部分未转译,因此软件包大小要小得多!浏览器会完全忽略其他脚本元素。
如果您在旧版浏览器上加载应用,则系统只会提取包含所有所需 polyfill 和转换的较大的转译脚本。以下是针对旧版 Chrome(版本 38)发出的所有请求的屏幕截图。
总结
现在,您已经了解如何使用 @babel/preset-env
仅提供目标浏览器所需的必要 polyfill。您还了解了 JavaScript 模块如何通过发布两个不同的转译版本的应用来进一步提升性能。现在,您已经充分了解这两种方法如何显著缩减软件包大小,接下来就开始优化吧!