使用 gzip 缩小和压缩网络负载

此代码实验室探讨了缩小和压缩以下应用程序的 JavaScript 捆绑包如何通过减小应用程序的请求大小来提高页面性能。

App screenshot

测量

在深入添加优化之前,最好先分析应用程序的当前状态。

  • 要预览网站,请按查看应用。然后按全屏 全屏

此应用程序在 “删除未使用的代码” 代码实验室中也有介绍,它允许您为您最喜欢的小猫投票。🐈

现在看看这个应用程序有多大

  1. 按 `Control+Shift+J`(在 Mac 上按 `Command+Option+J`)打开 DevTools。
  2. 点击 Network 选项卡。
  3. 选中 Disable cache 复选框。
  4. 重新加载应用程序。

Original bundle size in Network panel

尽管在 “删除未使用的代码” 代码实验室中在减小此捆绑包大小方面取得了很大进展,但 225 KB 仍然很大。

缩小

考虑以下代码块。

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

如果此函数保存在其自己的文件中,则文件大小约为 112 字节(bytes)。

如果删除所有空格,则生成的代码如下所示

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

现在文件大小约为 83 字节。如果通过缩短变量名长度和修改某些表达式进一步进行处理,则最终代码可能如下所示

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

现在文件大小达到 62 字节

每一步,代码都变得越来越难以阅读。但是,浏览器的 JavaScript 引擎以完全相同的方式解释这些代码。以这种方式混淆代码的好处是可以帮助实现更小的文件大小。112 字节一开始确实不多,但大小仍然减少了 50%!

在此应用程序中,webpack 版本 4 用作模块捆绑器。可以在 package.json 中查看特定版本。

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

版本 4 在生产模式下默认已缩小捆绑包。它使用 TerserWebpackPlugin,这是 Terser 的一个插件。Terser 是一种流行的工具,用于压缩 JavaScript 代码。

要了解缩小后的代码是什么样子,请继续点击 DevTools Network 面板中的 main.bundle.js。现在点击 Response 选项卡。

Minified response

最终形式的代码,经过缩小和混淆,显示在响应正文中。要了解如果未缩小捆绑包,它可能有多大,请打开 webpack.config.js 并更新 mode 配置。

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

重新加载应用程序,并通过 DevTools Network 面板再次查看捆绑包大小

Bundle size of 767 KB

这是一个非常大的差异!😅

在继续之前,请务必恢复此处的更改。

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

在您的应用程序中包含缩小代码的过程取决于您使用的工具

  • 如果使用 webpack v4 或更高版本,则无需进行额外的工作,因为代码在生产模式下默认已缩小。👍
  • 如果使用旧版本的 webpack,请安装并将 TerserWebpackPlugin 包含到 webpack 构建过程中。文档详细解释了这一点。
  • 还存在其他缩小插件,可以替代使用,例如 BabelMinifyWebpackPluginClosureCompilerPlugin
  • 如果根本没有使用模块捆绑器,请使用 Terser 作为 CLI 工具,或直接将其作为依赖项包含在内。

压缩

尽管术语“压缩”有时被松散地用来解释代码在缩小过程中是如何减少的,但它实际上并不是字面意义上的压缩。

压缩通常指使用数据压缩算法修改过的代码。与最终提供完全有效代码的缩小不同,压缩代码在使用前需要解压缩

对于每个 HTTP 请求和响应,浏览器和 Web 服务器都可以添加 标头,以包含有关正在获取或接收的资产的附加信息。这可以在 DevTools Network 面板中的 Headers 选项卡中看到,其中显示了三种类型

  • General 表示与整个请求-响应交互相关的通用标头。
  • Response Headers 显示服务器实际响应的特定标头列表。
  • Request Headers 显示客户端附加到请求的标头列表。

查看 Request Headers 中的 accept-encoding 标头。

Accept encoding header

accept-encoding 供浏览器用于指定它支持的内容编码格式或压缩算法。有很多文本压缩算法,但此处仅支持三种用于 HTTP 网络请求的压缩(和解压缩)

  • Gzip (gzip):用于服务器和客户端交互的最广泛使用的压缩格式。它建立在 Deflate 算法之上,并受所有当前浏览器的支持。
  • Deflate (deflate):不常用。
  • Brotli (br):一种较新的压缩算法,旨在进一步提高压缩率,从而可以实现更快的页面加载速度。它在大多数浏览器的最新版本中受支持。

本教程中的示例应用程序与 “删除未使用的代码” 代码实验室中完成的应用程序相同,不同之处在于现在 Express 用作服务器框架。在接下来的几个部分中,将探讨静态和动态压缩。

动态压缩

动态压缩涉及在浏览器请求资产时即时压缩资产。

优点

  • 无需创建和更新已保存的资产压缩版本。
  • 即时压缩对于动态生成的网页尤其有效。

缺点

  • 以更高的级别压缩文件以实现更好的压缩率需要更长的时间。这可能会导致性能下降,因为用户需要等待资产压缩后才能由服务器发送。

使用 Node/Express 进行动态压缩

server.js 文件负责设置托管应用程序的 Node 服务器。

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

目前,它所做的只是导入 express 并使用 express.static 中间件来加载 public/ 目录中的所有静态 HTML、JS 和 CSS 文件(这些文件由 webpack 在每次构建时创建)。

为了确保每次请求资产时都对其进行压缩,可以使用 compression 中间件库。首先将其作为 devDependency 添加到 package.json

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

并将其导入到服务器文件 server.js

const express = require('express');
const compression = require('compression');

并在挂载 express.static 之前将其添加为中间件

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

现在重新加载应用程序,并在 Network 面板中查看捆绑包大小。

Bundle size with dynamic compression

从 225 KB 降至 61.6 KB!现在在 Response Headers 中,content-encoding 标头显示服务器正在发送使用 gzip 编码的此文件。

Content encoding header

静态压缩

静态压缩背后的想法是预先压缩和保存资产。

优点

  • 由于高压缩级别而导致的延迟不再是问题。无需即时发生任何事情来压缩文件,因为现在可以直接获取它们。

缺点

  • 每次构建都需要压缩资产。如果使用高压缩级别,构建时间可能会显着增加。

使用 Node/Express 和 webpack 进行静态压缩

由于静态压缩涉及预先压缩文件,因此可以修改 webpack 设置以在构建步骤中压缩资产。 CompressionPlugin 可以用于此目的。

首先将其作为 devDependency 添加到 package.json

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

像任何其他 webpack 插件一样,将其导入到配置文件 webpack.config.js:

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

并将其包含在 plugins 数组中

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

默认情况下,该插件使用 gzip 压缩构建文件。查看 文档,了解如何添加选项以使用不同的算法或包含/排除某些文件。

当应用程序重新加载并重建时,现在会创建主捆绑包的压缩版本。打开 Glitch 控制台以查看 Node 服务器提供的最终 public/ 目录中的内容。

  • 点击 Tools 按钮。
  • 点击 Console 按钮。
  • 在控制台中,运行以下命令以更改为 public 目录并查看其所有文件
cd public
ls

Final outputted files in public directory

捆绑包的 gzipped 版本 main.bundle.js.gz 现在也保存在此处。 CompressionPlugin 默认情况下还会压缩 index.html

接下来需要做的是告诉服务器,每当请求其原始 JS 版本时,都发送这些 gzipped 文件。这可以通过在 server.js 中的 express.static 提供文件之前定义新路由来完成。

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

app.get 用于告诉服务器如何响应对特定端点的 GET 请求。然后使用回调函数来定义如何处理此请求。路由的工作方式如下

  • '*.js' 指定为第一个参数意味着这适用于为获取 JS 文件而触发的每个端点。
  • 在回调中,.gz 附加到请求的 URL,并且 Content-Encoding 响应标头设置为 gzip
  • 最后,next() 确保序列继续到下一个可能的任何回调。

应用程序重新加载后,再次查看 Network 面板。

Bundle size reduction with static compression

与之前一样,捆绑包大小显着减小!

结论

本代码实验室介绍了缩小和压缩源代码的过程。这两种技术都正在成为当今许多可用工具中的默认设置,因此务必了解您的工具链是否已支持它们,或者您是否应该开始自己应用这两个过程。