在本代码实验室中,改进这个简单应用程序的性能,该应用程序允许用户评价随机的猫。了解如何通过最小化转译的代码量来优化 JavaScript 包。
在示例应用中,您可以选择一个词或表情符号来表达您对每只猫的喜爱程度。当您单击按钮时,应用会在当前猫图像下方显示按钮的值。
测量
在添加任何优化之前,最好先检查网站
- 要预览网站,请按查看应用。然后按全屏
。
- 按 `Control+Shift+J`(在 Mac 上按 `Command+Option+J`)打开 DevTools。
- 点击 Network 选项卡。
- 选中 Disable cache 复选框。
- 重新加载应用。
此应用程序使用了超过 80 KB 的空间!现在找出是否包的某些部分没有被使用
按
Control+Shift+P
(在 Mac 上按Command+Shift+P
)打开 Command 菜单。输入
Show Coverage
并按Enter
以显示 Coverage 选项卡。在 Coverage 选项卡中,点击 Reload 以在捕获覆盖率的同时重新加载应用程序。
查看主包的已用代码量与加载代码量
超过一半的包(44 KB)甚至没有被使用。这是因为其中很多代码都包含 polyfill,以确保应用程序在旧版浏览器中也能正常运行。
使用 @babel/preset-env
JavaScript 语言的语法符合称为 ECMAScript 的标准,或 ECMA-262。规范的较新版本每年发布,并包含已通过提案流程的新功能。每个主要浏览器始终处于支持这些功能的不同阶段。
应用程序中使用了以下 ES2015 功能
还使用了以下 ES2017 功能
随意深入研究 src/index.js
中的源代码,了解所有这些功能是如何使用的。
所有这些功能在最新版本的 Chrome 中都受支持,但其他不支持它们的浏览器呢?Babel(应用程序中包含)是最流行的库,用于将包含较新语法的代码编译为旧版浏览器和环境可以理解的代码。它通过两种方式实现这一点
- 包含 Polyfill 以模拟较新的 ES2015+ 函数,以便即使浏览器不支持,也可以使用它们的 API。这是一个 polyfill 的
Array.includes
方法的示例。 - 插件 用于将 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 设置。了解如何在您的应用程序中包含 Babel,如果您碰巧使用与 webpack 不同的模块打包器。
.babelrc
中的 targets
属性标识了要定位的浏览器。@babel/preset-env
与 browserslist 集成,这意味着您可以在 browserlist 文档中找到可在此字段中使用的兼容查询的完整列表。
"last 2 versions"
值将应用程序中的代码转译为每个浏览器的 最后两个版本。
调试
要完整查看所有浏览器的 Babel 目标以及包含的所有转换和 polyfill,请将 debug
字段添加到 .babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- 点击 Tools。
- 点击 Logs。
重新加载应用程序,并查看编辑器底部的 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
,以便仅包含代码中正在使用的功能所需的 polyfill。
{
"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 的 “Last 2 versions” considered harmful 文章,以了解更多信息。
使用 <script type="module">
仍有更多改进空间。虽然已删除许多未使用的 polyfill,但仍有很多 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>
许多较新的 ECMAScript 功能在支持 JavaScript 模块的环境中已经受支持(而不是需要 Babel。)这意味着可以修改 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
}
请注意,不是使用 "@babel/preset-env"
的 targets
值,而是使用了值为 false
的 esmodules
。这意味着 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
的位置下方创建一个模块脚本的 config 对象
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"
的 script 元素。这意味着您可以同时包含模块和编译后的回退。理想情况下,应用程序的两个版本应像这样在 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
script 元素添加 type="module"
属性,并为所有 .js
script 模块添加 nomodule
属性。
在 HTML 文档中提供模块
最后需要做的是将旧版和现代脚本元素都输出到 HTML 文件。遗憾的是,创建最终 HTML 文件的插件 HTMLWebpackPlugin
目前不支持 同时输出 module 和 nomodule 脚本。虽然有一些解决方法和单独的插件被创建来解决这个问题,例如 BabelMultiTargetPlugin 和 HTMLWebpackMultiBuildPlugin,但为了本教程的目的,使用了手动添加模块脚本元素的更简单方法。
将以下内容添加到 src/index.js
文件的末尾
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
现在在支持模块的浏览器(例如最新版本的 Chrome)中加载应用程序。
仅获取模块,由于它在很大程度上是未转译的,因此包大小要小得多!另一个 script 元素完全被浏览器忽略。
如果您在旧版浏览器上加载应用程序,则只会获取较大、已转译的脚本,其中包含所有必需的 polyfill 和转换。这是在旧版本 Chrome(版本 38)上发出的所有请求的屏幕截图。
结论
您现在了解了如何使用 @babel/preset-env
仅提供目标浏览器所需的必要 polyfill。您还了解了 JavaScript 模块如何通过交付应用程序的两个不同转译版本来进一步提高性能。充分理解这两种技术如何显著减小您的包大小后,继续前进并进行优化吧!