向新型浏览器提供新式代码,以提高网页加载速度

在此 Codelab 中,您可以提升这个简单应用的性能,该应用允许用户对随机猫咪进行评分。了解如何通过尽可能减少转译代码量来优化 JavaScript 软件包。

应用屏幕截图

在示例应用中,您可以选择一个单词或表情符号来表达您对每只猫的喜爱。当您点击某个按钮时,应用会在当前猫图片下方显示该按钮的值。

测量

建议您先检查网站,然后再添加任何优化措施:

  1. 如需预览网站,请按查看应用,然后按全屏 全屏
  2. 按 `Ctrl+Shift+J`(在 Mac 上,按 `Command+Option+J`)打开开发者工具。
  3. 点击网络标签页。
  4. 选中停用缓存复选框。
  5. 重新加载应用。

原始 app bundle 大小请求

此应用程序已使用超过 80 KB!检查软件包中是否有部分未被使用的时间:

  1. Control+Shift+P(在 Mac 上,按 Command+Shift+P)打开 Command 菜单。 命令菜单

  2. 输入 Show Coverage 并点击 Enter 以显示覆盖率标签页。

  3. 覆盖率标签页中,点击重新加载以在捕获覆盖率时重新加载应用。

    使用代码覆盖率重新加载应用

  4. 查看使用的代码量与主软件包的加载量:

    软件包的代码覆盖率

超过一半 (44 KB) 的软件包甚至没有得到使用。这是因为,其中包含的许多代码都包含了 polyfill,从而确保应用能够在旧版浏览器中正常运行。

使用 @babel/preset-env

JavaScript 语言的语法遵循 ECMAScript(即 ECMA-262)标准。该规范每年都会发布新版本,其中包含已通过提案流程的新功能。各款主流浏览器始终处于支持这些功能的不同阶段。

该应用中使用了以下 ES2015 功能:

还使用了以下 ES2017 功能:

欢迎随时深入探究 src/index.js 中的源代码,了解如何使用所有相关功能。

最新版本的 Chrome 浏览器支持所有这些功能,但是不支持这些功能的其他浏览器又会怎么样呢?应用中包含的 Babel 是一个最常用的库,用于将包含新语法的代码编译为旧版浏览器和环境可以理解的代码。它通过以下两种方式实现这一功能:

  • 添加了 Polyfill,用于模拟较新的 ES2015+ 函数,因此即使浏览器不支持,也可以使用其 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 与 browserlist 集成,这意味着 browserlist 文档中可找到可在此字段中使用的兼容查询的完整列表。

"last 2 versions" 值会针对每个浏览器的最近两个版本转译应用中的代码。

调试

如需全面了解浏览器的所有 Babel 目标以及其中包含的所有转换和 polyfill,请将 debug 字段添加到 .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • 点击工具
  • 点击日志

重新加载应用,然后查看编辑器底部的 Glitch 状态日志。

定位的浏览器

Babel 会将有关编译过程的许多详细信息记录到控制台,包括编译了代码的所有目标环境。

定位的浏览器

请注意此列表是如何包含 Internet Explorer 等已停用的浏览器。这是一个问题,因为不受支持的浏览器不会添加新功能,而 Babel 会继续为其转译特定语法。如果用户并未使用此浏览器访问您的网站,这会不必要地增加 app bundle 的大小。

Babel 还会记录所使用的转换插件列表:

所用插件的列表

好长的清单呢!这些是 Babel 将任何 ES2015+ 语法转换为旧语法时需要使用的所有插件,才能适用于所有目标浏览器。

但是,Babel 不会显示所使用的任何特定 polyfill:

未添加任何 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:

已导入的 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。

已自动包含 polyfill 列表

应用软件包大小显著减小。

捆绑包大小缩减到 30.1 KB

缩小支持的浏览器列表范围

包含的浏览器目标仍然非常多,而且使用 Internet Explorer 等旧版浏览器的用户并不多。按如下所示更新配置:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

查看提取的 bundle 的详细信息。

捆绑包大小为 30.0 KB

由于应用很小,因此这些更改并没有太大区别。不过,建议的做法是使用浏览器市场份额百分比(例如 ">0.25%"),同时排除您确信用户不会使用的特定浏览器。如需了解详情,请查看 James Kyle 撰写的“最近两个版本”被视为有害的文章。

使用 <script type="module">

但仍有改进的空间。虽然移除了许多未使用的 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
}

请注意,系统使用的是值为 falseesmodules,而不是 "@babel/preset-env"targets 值。这意味着 Babel 包含了所有必要的转换和 polyfill,可以定位到尚不支持 ES 模块的所有浏览器。

entrycssRulecorePlugins 对象添加到 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>

此处需要做的最后一项操作是分别向模块和旧版脚本添加 modulenomodule 属性,并在 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 的插件目前不支持 module 和 nomodule 脚本的输出。尽管有解决方法和单独的插件(例如 BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin)可以解决这个问题,但在本教程中,我们使用了一种更简单的方法来手动添加模块脚本元素。

将以下代码添加到文件末尾的 src/index.js 中:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

现在,在支持模块的浏览器(例如最新版本的 Chrome)中加载应用。

针对新版浏览器通过网络提取的 5.2 KB 模块

仅提取该模块,由于该模块在很大程度上是未转译的,因此软件包大小小得多!浏览器完全忽略另一个脚本元素。

如果您在旧版浏览器上加载应用,系统仅会提取包含所有需要的 polyfill 和转换的较大转译脚本。以下屏幕截图显示了在旧版 Chrome(版本 38)上发出的所有请求。

针对旧版浏览器提取了 30 KB 脚本

总结

现在,您已了解如何使用 @babel/preset-env 仅提供目标浏览器所需的必要 polyfill。您还了解了 JavaScript 模块如何通过交付两个不同转译版本的应用来进一步提升性能。在充分了解这两种方法如何大幅缩减软件包大小后,便可着手优化!