关于 compression
这是一项介绍 JavaScript 与 HTTP 压缩的开发技能,涵盖 Brotli、Gzip、代码压缩及静态或动态压缩。面向优化传输体积和加载性能的开发者,并讨论压缩与分包粒度、缓存之间的取舍。
主要功能
- 01比较 Brotli 与 Gzip
- 02选择静态或动态压缩
- 03压缩 JavaScript 后再传输
- 04权衡分块大小与缓存
- 05在 DevTools 中检查压缩
适用场景
- 01减少 JavaScript 传输体积
- 02改善慢速网络下的加载
- 03为静态或动态内容配置压缩
- 04在 DevTools 中检查压缩响应
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill compression安装后在 Claude Code 中输入/compression确认已加载。
详细介绍
压缩 JavaScript
目录
在互联网中,JavaScript 是仅次于图片的第二大页面大小组成部分,也是仅次于图片的第二大请求资源。我们采用能够减少 JavaScript 传输、加载和执行时间的模式,以提升网站性能。压缩有助于缩短通过网络传输脚本所需的时间。
适用场景
- 需要减小 JavaScript 负载大小以加快页面加载速度时,可使用此方法
- 优化网络传输时间时,此方法很有帮助,尤其适用于网络连接较慢的用户
- 可配合缩小代码、代码拆分和缓存策略使用
说明
- 优先使用 Brotli 压缩,而非 Gzip,以在速度相近的情况下获得更高的压缩率
- 对不常变化的资源使用静态压缩,对频繁变化的内容使用动态压缩
- 在服务端或 CDN 层启用压缩(例如 Nginx、Vercel、Netlify)
- 应用压缩前,先缩小 JavaScript 代码
- 注意粒度方面的权衡:较大的包压缩效果更好,但较小的代码块缓存效果更好
详情
你可以将压缩与缩小代码、代码拆分、打包、缓存和延迟加载等其他技术结合使用,以降低大量 JavaScript 对性能的影响。不过,这些技术的目标有时可能相互冲突。本节将介绍 JavaScript 压缩技术,并探讨在确定代码拆分和压缩策略时应考虑的细节。
- Gzip 和 Brotli 是最常见的 JavaScript 压缩方式,并得到现代浏览器的广泛支持。
- 在压缩级别相近的情况下,Brotli 的压缩率更高。
- Next.js 默认提供 Gzip 压缩,但建议在 Nginx 等 HTTP 代理上启用压缩。
- 如果使用 Webpack 打包代码,可以使用 CompressionPlugin 进行 Gzip 压缩,或使用 BrotliWebpackPlugin 进行 Brotli 压缩。
- Oyo 在改用 Brotli 压缩而非 Gzip 后,文件大小减少了 15-20%;Wix 则减少了 21-25%。
- compress(a + b) <= compress(a) + compress(b) - 单个大包的压缩效果优于多个较小的包。这带来了粒度方面的权衡:去重和缓存与浏览器性能及压缩效果之间存在冲突。采用细粒度的代码块拆分有助于应对这种权衡。
HTTP 压缩
压缩可以减小文档和文件的体积,使其占用的磁盘空间比原始文件更少。较小的文档所需带宽更低,也能更快地通过网络传输。HTTP 压缩利用这一简单原理压缩网站内容,减小页面体积,降低带宽需求并提升性能。
HTTP 数据压缩可以按不同方式分类,其中一种是有损压缩与无损压缩。
有损压缩意味着压缩和解压缩后,文档会略有变化,但仍可正常使用。这种变化对最终用户大多难以察觉。最常见的有损压缩示例是图像的 JPEG 压缩。
无损压缩则意味着压缩后再解压缩得到的数据与原始数据完全一致。PNG 图像就是无损压缩的示例。无损压缩适用于文本传输,应应用于 HTML、CSS 和 JavaScript 等基于文本的格式。
由于你希望浏览器中包含的所有 JS 代码都有效,因此应对 JavaScript 代码使用无损压缩算法。在压缩 JS 之前,代码缩减可以帮助去除不必要的语法,只保留执行所需的代码。
代码缩减
为了减小负载体积,你可以在压缩 JavaScript 之前先对其进行代码缩减。代码缩减通过删除空白字符和不必要的代码来生成体积更小、但完全有效的代码文件,从而与压缩相辅相成。编写代码时,我们会使用换行、缩进、空格、易读的变量名和注释,以提高代码的可读性和可维护性。然而,这些元素会增加 JavaScript 的整体体积,并非浏览器执行代码所必需。代码缩减会将 JavaScript 代码精简到成功执行所需的最小程度。
代码缩减是 JS 和 CSS 优化中的标准做法。JavaScript 库开发者通常会提供文件的缩减版本,用于生产环境部署,文件名扩展名通常为 min.js。(例如,jquery.js 和 jquery.min.js)
有多种工具可用于缩减 HTML、CSS 和 JS 资源。Terser 是一款适用于 ES6+ 的热门 JavaScript 压缩工具;Webpack v4 默认包含该库的插件,可生成缩减后的构建文件。你也可以在较旧版本的 Webpack 中使用 TerserWebpackPlugin,或者将 Terser 作为 CLI 工具使用,而无需模块打包器。
静态压缩与动态压缩
缩小文件体积有助于大幅减小文件大小,而压缩 JS 则能带来更显著的收益。你可以通过两种方式实现服务器端压缩。
静态压缩: 你可以使用静态压缩预先压缩资源,并在构建过程中将压缩后的文件保存起来。在这种情况下,你可以采用较高的压缩级别,以缩短代码下载时间。较长的构建时间不会影响网站性能。对于不经常变更的文件,最好使用静态压缩。
动态压缩: 在此过程中,浏览器请求资源时会即时进行压缩。动态压缩更容易实现,但你只能使用较低的压缩级别。较高的压缩级别需要更多时间,会抵消内容体积缩小带来的优势。对于频繁变更或由应用生成的内容,最好使用动态压缩。
你可以根据应用内容的类型使用静态压缩或动态压缩。你可以使用常见的压缩算法同时启用静态和动态压缩,但两种情况下建议采用的压缩级别有所不同。
压缩算法
Gzip 和 Brotli 是当今用于压缩 HTTP 数据的两种最常见算法。
Gzip
Gzip 压缩格式已有近 30 年历史,是一种基于 Deflate 算法的无损算法。Deflate 算法本身会对输入数据流中的数据块结合使用 LZ77 算法和 Huffman 编码。
LZ77 算法会识别重复字符串,并将其替换为回溯引用,即指向该字符串之前出现位置的指针,后面再附上字符串长度。随后,Huffman 编码会识别常用引用,并将其替换为位序列更短的引用。位序列较长的引用则用于表示不常用的引用。
所有主流浏览器都支持 Gzip。Zopfli 压缩算法是速度较慢但有所改进的 Deflate/Gzip 版本,可生成体积更小且兼容 Gzip 的文件。它最适合用于静态压缩,可带来更显著的收益。
Brotli
2015 年,Google 推出了 Brotli 算法和 Brotli 压缩数据格式。与 GZip 一样,Brotli也是一种基于 LZ77 算法和 Huffman 编码的无损算法。此外,它还采用二阶上下文建模,以相近的速度实现更高密度的压缩。上下文建模是一项功能,允许在同一个数据块中为同一个字母表使用多个 Huffman 树。Brotli 还支持用于反向引用的更大窗口,并带有静态字典。这些特性有助于提高其作为压缩算法的效率。
如今,所有主流服务器和浏览器都支持Brotli,而且它正变得越来越受欢迎。包括 Netlify、AWS 和 Vercel 在内的托管服务提供商和中间件也支持 Brotli,且可轻松启用。
OYO 和 Wix 等拥有大量用户的网站,在用 Brotli 替换 Gzip 后,性能得到了显著提升。
Gzip 与 Brotli 的比较
以下是 Chrome 关于使用 Gzip 和 Brotli 压缩 JS 的研究得出的一些结论:
- Gzip 9 的压缩率最佳,且压缩速度良好,因此建议优先考虑使用它,而非其他 Gzip 级别。
- 使用 Brotli 时,可以考虑 6-11 级。否则,使用 Gzip 可以更快地达到相近的压缩率。
- 对于所有大小范围,Brotli 9-11 的表现都远胜 Gzip,但速度相当慢。
- 压缩包越大,压缩率和速度就越好。
- 不同算法之间的关系在所有压缩包大小下都相似(例如,对于任何压缩包大小,Brotli 7 都优于 Gzip 9;而在所有大小范围内,Gzip 9 都比 Brotli 5 快)。
启用压缩
你可以在构建过程中启用静态压缩。如果使用 Webpack 打包代码,可以使用 CompressionPlugin 进行 Gzip 压缩,或使用 BrotliWebpackPlugin 进行 Brotli 压缩。可以按如下方式将插件添加到 Webpack 配置文件中。
module.exports = {
//...
plugins: [
//...
new CompressionPlugin(),
],
};
Next.js 默认提供 Gzip 压缩,但建议在 Nginx 等 HTTP 代理上启用压缩。Vercel 平台支持在代理层进行 Gzip 和 Brotli 压缩。
你可以在支持不同压缩算法的服务端(包括 Node.js)上启用动态无损压缩。浏览器会通过请求中的 Accept-Encoding HTTP 标头告知支持的压缩算法。例如,Accept-Encoding: gzip, br。
这表示浏览器支持 Gzip 和 Brotli。你可以按照对应服务端类型的说明,在服务端启用不同类型的压缩。例如,你可以在此处找到在 Apache 服务端启用 Brotli 的说明。Express 是一个常用的 Node Web 框架,并提供了一个 compression 中间件库。使用它可以在请求资源时对其进行压缩。
与其他压缩算法相比,推荐使用 Brotli,因为它生成的文件更小。你可以为不支持 Brotli 的浏览器启用 Gzip 作为回退。如果配置成功,服务端会在 HTTP 响应中返回 Content-Encoding 标头,以指明响应使用的压缩算法。例如,Content-Encoding: br。
审核压缩
你可以在 Chrome DevTools → Network → Headers 中检查服务端是否压缩了下载的脚本或文本。DevTools 会显示响应使用的内容编码。
Lighthouse 报告包含一项名为 “Enable Text Compression” 的性能审核,用于检查文本类资源是否未设置值为 'br'、'gzip' 或 'deflate' 的 content-encoding 标头。Lighthouse 使用 Gzip 计算资源可能节省的大小。
JavaScript 压缩和加载粒度
要全面理解 JavaScript 压缩的影响,还必须考虑 JavaScript 优化的其他方面,例如基于路由的拆分、代码拆分和打包。
包含大量 JavaScript 代码的现代 Web 应用通常会使用不同的代码拆分和打包技术,以高效加载代码。应用会根据逻辑边界拆分代码,例如,针对单页应用进行路由级拆分,或在用户交互或视口可见时逐步提供 JavaScript。你可以配置打包器来识别这些边界。
打包术语
以下是与我们的讨论相关的一些关键术语。
- 模块:模块是独立的功能单元,旨在提供清晰的抽象和封装。详情请参阅 模块模式。
- 包:由不同模块组成的集合,其中包含源文件的最终版本,并已在打包器中完成加载和编译。
- 包拆分:打包器用于将应用拆分为多个包的过程,这样每个包都可以独立隔离、发布、下载或缓存。
- 代码块:该术语源自 Webpack。代码块是打包和代码拆分过程的最终输出。Webpack 可以根据 入口 配置、SplitChunksPlugin 或 动态导入 将包拆分为代码块。
如果源文件中包含模块,那么代码或包拆分后构建过程的最终输出称为代码块。请注意,源文件和代码块可能相互依赖。
JavaScript 输出大小指经 JavaScript 打包器或编译器优化后的代码块大小或原始大小。大型 JS 应用可以拆分为多个可独立加载的 JavaScript 文件代码块。加载粒度指输出代码块的数量——代码块数量越多,每个代码块就越小,粒度也越高。
有些代码块比其他代码块更关键,因为它们加载得更频繁,或属于影响更大的代码路径(例如加载“结账”小组件)。要了解哪些代码块最重要,需要了解应用;不过,可以认为“基础”代码块始终必不可少。
页面所需代码块中的每个字节都需要由用户设备下载并解析/执行。这些代码会直接影响应用性能。由于代码块最终都会被下载,压缩代码块有助于提高下载速度。
粒度权衡
理想情况下,粒度和分块策略应力求实现以下目标,但这些目标彼此矛盾。
- 提升下载速度: 如前文所述,可以通过压缩来提升下载速度。不过,使用相同代码压缩一个大块,比压缩多个小块的效果更好,生成的文件也更小。
compress(a + b) <= compress(a) + compress(b)
- 提升缓存命中率和缓存效率: 较小的块能带来更高的缓存效率,尤其适用于增量加载 JS 的应用。
-
块越小,变更影响的块就越少。如果代码发生变更,只需重新下载受影响的块,而且这些块对应的代码量通常较小。其余块可以从缓存中获取,从而提高缓存命中率。
-
块越大,代码变更后受影响的代码量就可能越大,需要重新下载的内容也越多。
因此,为了利用缓存机制,较小的块更理想。
- 快速执行 - 要让代码快速执行,需要满足以下条件。
- 所需依赖都能立即获取——它们已一同下载,或已存在于缓存中。这意味着应将所有相关代码打包成一个较大的块。
- 只有页面/路由所需的代码才应执行。这要求不下载或执行额外代码。包含常见依赖的
commons块可能带有大多数页面需要、但并非所有页面都需要的依赖。代码去重则需要更小且相互独立的块。 - 主线程上的长任务可能会长时间阻塞线程,因此需要将其拆分成更小的块。
试图优化上述某一目标的加载粒度,可能会让你偏离其他目标。这就是粒度权衡问题。
代码去重和缓存与浏览器性能及压缩效果相互矛盾。
由于这种权衡,如今大多数生产应用使用的块数上限约为 10。为了让 JavaScript 代码量较大的应用获得更好的缓存和代码去重效果,需要提高这一上限。
SplitChunksPlugin 与细粒度分块
针对粒度权衡问题的潜在解决方案需要满足以下要求。
- 允许创建数量更多(40 到 100 个)、大小更小的分块,以便更好地缓存和去重,同时不影响性能。
- 解决多个较小分块带来的性能开销,包括 IPC、I/O 以及处理大量脚本标签的成本。
- 解决多个较小分块导致的压缩损失。
能够满足这些要求的潜在解决方案仍在开发中。不过,Webpack v4 的 SplitChunksPlugin 和细粒度分块策略可以在一定程度上提高加载粒度。
较早版本的 Webpack 使用 CommonsChunkPlugin 将公共依赖项或共享模块打包到单个分块中。对于未使用这些公共模块的页面,这可能导致下载和执行时间不必要地增加。为了更好地优化这类页面,Webpack 在 v4 中引入了 SplitChunksPlugin。它会根据默认值或配置创建多个分块,以避免在不同路由间获取重复代码。
Next.js 采用了 SplitChunksPlugin,并实现了以下细粒度分块策略,以生成能够解决粒度权衡问题的 Webpack 分块。
- 任何体积足够大的第三方模块(大于 160 KB)都会被拆分到单独的分块中。
- 为框架依赖项创建单独的框架分块。(react、react-dom 等)
- 根据需要创建尽可能多的共享分块。(最多 25 个)
- 将生成分块的最小大小调整为 20 KB。
与单个共享分块相比,生成多个共享分块可最大限度地减少不同页面下载或执行的不必要代码(或重复代码)。为大型第三方库生成独立分块有助于提高缓存效果,因为它们不太可能频繁变更。将分块的最小大小设为 20 kB,可确保压缩损失相对较低。
细粒度分块策略帮助多个 Next JS 应用减少了网站使用的 JavaScript 总量。该策略也已在 Gatsby 中实现,并取得了类似的效果。
结论
仅靠压缩无法解决所有 JavaScript 性能问题,但了解浏览器和打包工具在幕后如何工作,有助于制定更好的打包策略,从而支持更好的压缩。整个生态系统中的不同平台都需要解决加载粒度问题。细粒度分块或许是朝这个方向迈出的一步,但我们还有很长的路要走。