- Published on
性能优化篇(二) : 压缩Javascript
此系列文章:
Compressing JavaScript
压缩 JavaScript 并留意chunk大小以获得最佳性能。过高的 JavaScript Bundle粒度有助于重复数据删除和缓存,但可能会遭受较差的压缩和影响到 (50-100) chunk 范围内的加载(由于浏览器进程、缓存检查等)。您需要选择最适合您的压缩策略。
JavaScript 是占应用源码大小第二大的"贡献者",也是互联网仅次于图像的第二大请求量的 Web 资源。我们通过减少 JavaScript 传输、加载和执行时间的方式来提高网站性能。而压缩有助于减少通过网络传输脚本所需的时间。
您可以将压缩与其他技术(例如压缩、代码分割、Bundle、缓存和懒加载)结合起来,以减少大量的 JavaScript 对性能的影响。
然而,这些技术的目标有时可能会相互矛盾。本节探讨 JavaScript 压缩技术,并讨论在选择代码分割和压缩这两种策略时应考虑的细微差别。
Gzip 和 Brotli 是最常见的 JavaScript 压缩方式,并受到现代浏览器的广泛支持。
Brotli 在相似的压缩级别下提供了更好的压缩比。
Next.js 默认提供 Gzip 压缩,但建议在 Nginx 等 HTTP 代理上启用它。
如果您使用 Webpack 打包代码,则可以使用 CompressionPlugin 进行 Gzip 压缩,或使用 BrotliWebpackPlugin 进行 Brotli 压缩。
在改用 Brotli 压缩而不是 Gzip 后,Oyo文件大小减少了 15-20%,Wix文件大小减少了 21-25%。
对于
compress(a + b) <= compress(a) + compress(b)
- 单个大包将比多个小包获得更好的压缩比。这导致了粒度权衡,在这里 去重和缓存与浏览器性能和压缩相矛盾。粒度块 可以帮助应对这种权衡。
HTTP 压缩
压缩可减少文档和文件的大小,因此它们比原始文件占用更少的磁盘空间。较小的文档可以使使用带宽降低,能够在网络上快速传输。 HTTP 压缩利用这个简单的概念来压缩网站内容、减少页面大小、降低带宽从而提高性能。
HTTP 数据压缩可以以不同的方式进行分类。其中一种分类是:有损(Lossy compression) 与 无损(Lossless compression)。
有损压缩
有损压缩意味着压缩-解压缩流程会导致文档有微小改变,但最终用户察觉不到这种变化。有损压缩最常见的示例是图像的 JPEG 压缩。
无损压缩
压缩与解压缩后的数据将与原始数据精确匹配。 PNG 图像是无损压缩的一个示例。无损压缩适用于文本传输,并应用于基于文本的格式,如 HTML、CSS 和 JavaScript。
由于您不希望JS压缩解压缩前后不一致,所以您应该为 JavaScript 代码使用无损压缩算法。
Minification 缩小化
要减少有效负载大小,您可以在压缩之前缩小 JavaScript。缩小通过删除空格和任何不必要的代码来进行,以创建更小但完全有效的代码文件。
在编写代码时,我们使用换行符、缩进、空格、命名良好的变量和注释来提高代码的可读性和可维护性。但是,这些元素会影响 JavaScript 的整体大小,并且对于在浏览器上执行来说并不是必需的。缩小将 JavaScript 代码减少到成功执行所需的最少数量。
缩小是 JS 和 CSS 优化的标准做法。 JavaScript库开发人员通常会为生产环境提供文件的缩小版本,通常用 *.min.js 扩展名表示。 (例如, jquery.js 和 jquery.min.js )
有多种工具可用于缩小 HTML、CSS 和 JS 资源。 Terser 是 ES6+ 的流行 JavaScript 压缩工具,Webpack v4 默认包含该库的一个插件,用于创建缩小的构建文件。
您还可以将 TerserWebpackPlugin 与旧版本的 Webpack 一起使用,或者使用 Terser 作为 CLI 工具,而无需使用打包工具。
压缩算法
Gzip 和 Brotli 是当今用于压缩 HTTP 数据的两种最常用算法。
Gzip
Gzip 压缩格式已经存在了近 30 年,是一种基于 Deflate 算法的无损算法。 deflate 算法本身对输入数据流中的数据块使用 LZ77 算法和霍夫曼编码的组合。
LZ77 算法识别重复字符串,并用后向引用替换它们,后向引用是指向它之前出现的位置的指针,后跟字符串的长度。随后,Huffman 编码识别常用引用,并用较短的位序列替换它们。较长的位序列用于表示不经常使用的引用。
图片提供:https://www.youtube.com/watch?v=whGwm0Lky2s&t=851s
所有主流浏览器都支持 Gzip。 Zopfli 压缩算法是 Deflate/Gzip 的较慢但改进的版本,可生成较小的 GZip 兼容文件。它最适合静态压缩,可以提供更显着的增益。
Brotli
2015年,Google推出了Brotli算法和Brotli压缩数据格式。与 GZip 一样,Brotli 也是基于 LZ77 算法和 Huffman 编码的无损算法。此外,它使用二阶上下文建模以相似的速度产生更高的压缩比。
上下文建模是一项功能,允许同一块中的同一字母表使用多个霍夫曼树。 Brotli 还支持更大的反向引用窗口大小,并具有静态字典。这些功能有助于提高其作为压缩算法的效率。
Brotli 受到当今所有主要服务器和浏览器的支持,并且变得越来越流行。它还受到托管提供商和中间件(包括 Netlify、AWS 和 Vercel)的支持并可以轻松启用。
OYO、Wix等用户基数较大的网站,在用Brotli替换Gzip后,性能得到了显着提升。
比较 Gzip 和 Brotli
下表显示了不同压缩级别下 Brotli 和 Gzip 压缩率和速度的基准比较。
此外,以下是 Chrome 研究中关于使用 Gzip 和 Brotli 压缩 JS 的一些见解
Gzip 9 具有最佳的压缩率和良好的压缩速度,您应该在使用其他级别的 Gzip 之前考虑使用它。
对于 Brotli,请考虑 6-11 级。否则,我们可以使用 Gzip 更快地实现类似的压缩率。
在所有大小范围内,Brotli 9-11 的性能都比 Gzip 好得多,但速度相当慢。
Bundle包越大,您获得的压缩率和速度就越好。
对于所有包而言,算法之间的关系都是相似的(例如,对于每个包大小,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 的说明。
与其他压缩算法相比,建议使用 Brotli,因为它生成的文件大小更小。您可以启用 Gzip 作为不支持 Brotli 的浏览器的后备方案。如果配置成功,服务器将返回 Content-Encoding
HTTP 响应头,以表示响应中使用的压缩算法。例如: Content-Encoding: br
。
检查
您可以在 Chrome -> DevTools -> network -> Headers 中检查服务器是否压缩了下载的脚本或文本。 DevTools 显示响应中使用的内容编码,如下所示。
Lighthouse 报告包括“启用文本压缩”的性能审核,该审核检查接收到的基于文本的资源类型。
JavaScript 压缩和加载粒度
要充分掌握 JavaScript 压缩的效果,还必须考虑 JavaScript 优化的其他方面,例如基于路由的拆分、代码拆分和Bundle。
在我们继续讨论代码分割和打包如何影响压缩之前,让我们先介绍一些与代码分割和打包相关的基本定义。
以下是与我们讨论相关的一些关键术语。
模块:模块是离散的功能块,旨在提供可靠的抽象和封装。有关更多详细信息,请参阅模块模式。
Bundle:一组不同的模块,包含源文件的最终版本,并且已经在Bundle完成了加载和编译过程。
Bundle包拆分:打包程序将应用程序拆分为多个包的过程,以便每个包都可以独立隔离、发布、下载或缓存。
块:Webpack 术语,块是打包和代码分割过程的最终输出。 Webpack 可以根据入口配置、SplitChunksPlugin 或动态导入将包拆分为块。
如果模块包含在源文件中,则代码或Bundle包拆分后构建过程的最终输出称为块。请注意,源文件和块可能相互依赖。
JavaScript 的输出大小是指 JavaScript 打包工具或编译器优化后的chunk大小或原始大小。大型 JS 应用程序可以解构为可独立加载的 JavaScript 文件块。加载粒度是指输出块的数量——块的数量越多,每个块的大小越小,粒度越高。
页面所需的块的每个字节都需要由用户设备下载并解析/执行。这是直接影响应用程序性能的代码。由于chunk是最终将被下载的代码,因此压缩chunk可以带来更好的下载速度。
在此背景下,让我们讨论加载粒度和压缩之间的相互作用。
粒度权衡
在理想的世界中,粒度和分块策略应该旨在实现以下目标,这些目标是相互矛盾的。
- 提高下载速度:如前面部分所示,可以使用压缩来提高下载速度。然而,与使用相同代码压缩多个小块相比,压缩一个大块将产生更好的结果或更小的文件大小。
由于上述原因,对于优化下载和浏览器性能的相同代码,较大的块可能比较小的块更有效。
- 提高缓存命中率和缓存效率:较小的块会带来更好的缓存效率,特别是对于增量加载 JS 的应用程序。
隔离到较小的块中。如果代码发生变化,只需要重新下载受影响的块,并且这些对应的代码大小可能很小。剩余的块可以在缓存中找到,从而增加缓存命中的数量。
对于较大的块,很可能会影响大量代码,并且需要在代码更改后重新下载。
因此,需要使用较小的块来利用缓存机制。
如上面的三角形所示,尝试优化上述目标之一的加载粒度可能会让您远离其他目标。这就是粒度权衡的问题
由于这种权衡,目前大多数生产应用程序使用的最大块数约为 10 个。需要增加此限制,以支持具有大量 JavaScript 的应用程序更好的缓存和重复数据删除。
SplitChunksPlugin 和粒度分块
粒度权衡的潜在解决方案将满足以下要求。
允许使用较大数量的块(40 到 100)和较小的块大小,以实现更好的缓存和重复数据删除,而不影响性能。
解决由于 IPC、I/O 和许多脚本标签的处理成本而导致的多个较小块的性能开销。
解决多个较小块的情况下的压缩损失。
满足这些要求的潜在解决方案仍在研究中。然而,Webpack v4 的 SplitChunksPlugin 和粒度分块策略可以在一定程度上帮助提高加载粒度。
Webpack 的早期版本使用 CommonsChunkPlugin 将公共依赖项或共享模块打包到单个块中。这可能会导致不使用这些通用模块的页面的下载和执行时间不必要地增加。为了更好地优化此类页面,Webpack 在 v4 中引入了 SplitChunksPlugin 。根据默认值或配置创建多个分割块,以防止跨不同路由获取重复的代码。
Next.js 采用了 SplitChunksPlugin 并实现了以下粒度分块策略来生成解决粒度权衡问题的 Webpack 块。
任何足够大的第三方模块(大于 160 KB)都会被分割成单独的块。
为框架依赖项创建一个单独的框架块。 (反应、反应-dom 等)
根据需要创建尽可能多的共享块。 (最多 25 个)
生成的块的最小大小更改为 20 KB。
发出多个共享块而不是单个共享块可以最大限度地减少在不同页面上下载或执行的不必要(或重复)代码的数量。为大型第三方库生成独立块可以改善缓存,因为它们不太可能频繁更改。 20 kB 的最小块大小可确保压缩损失相当低。
精细的分块策略帮助多个 Next JS 应用减少了网站使用的 JavaScript 总量。
结论
仅靠压缩并不能解决所有 JavaScript 性能问题,但了解浏览器和打包工具在幕后的工作方式有助于创建更好的打包策略,以支持更好的压缩。加载粒度问题需要在生态系统中的不同平台上得到解决。粒度分块可能是朝这个方向迈出的一步,但我们还有很长的路要走。