压缩 JavaScript

压缩 JavaScript 并密切关注块大小以获得最佳性能。

过高的 JavaScript 包粒度有助于重复数据删除和缓存,但可能会在 50-100 个块范围内受到较差的压缩和影响加载(由于浏览器进程、缓存检查等)。

最终,选择最适合您的压缩策略。

JavaScript 是页面大小的第二大贡献者,也是互联网上仅次于图像的第二大请求量

减少 JavaScript 传输、加载和执行时间的模式来提高网站性能。
压缩有助于减少通过网络传输脚本所需的时间。

将压缩与其他技术(例如缩小、代码拆分、捆绑、缓存和延迟加载)结合使用,以减少大量 JavaScript 对性能的影响。

然而,这些技术的目标有时会相互矛盾。

本节探讨 JavaScript 压缩技术,并讨论在决定代码拆分和压缩策略时应考虑的细微差别。

  1. Gzip 和 Brotli 是压缩 JavaScript 的最常用方法,并且被现代浏览器广泛支持。

  2. Brotli 在类似的压缩级别下提供更好的压缩率。

  3. Next.js 默认提供 Gzip 压缩。 但建议在像 Nginx 这样的 HTTP 代理上启用它。

  4. 如果使用 Webpack 来捆绑代码,可以使用 CompressionPlugin 进行 Gzip 压缩或使用 BrotliWebpackPlugin 进行 Brotli 压缩。

  5. 在切换到 Brotli 压缩而不是 Gzip 后,Oyo 的文件大小减少了 15-20%,Wix 的文件大小减少了 21-25%。

  6. 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 默认为这个库包含一个插件,用于创建缩小的构建文件。

还可以将 TerserWebpackPlugin 与旧版本的 Webpack 一起使用,或者将 Terser 用作没有模块捆绑器的 CLI 工具。


静态与动态压缩

缩小有助于显着减小文件大小,但 JS 的压缩可以提供更显着的收益。

可以通过两种方式实现服务器端压缩。

静态压缩:可以使用静态压缩来预压缩资源并在构建过程中提前保存它们。
在这种情况下,可以使用更高的压缩级别来缩短代码的下载时间。
高构建时间不会影响网站性能。如果对不经常更改的文件使用静态压缩,那将是最好的

动态压缩:通过这个过程,当浏览器请求资源时,压缩会即时进行。
动态压缩更容易实现,但只能使用较低的压缩级别。
更高的压缩级别将需要更多的时间,并且将失去从较小的内容大小中获得的优势。
如果对经常更改或由应用程序生成的内容使用动态压缩,这将有所帮助

可以根据应用程序内容的类型使用静态或动态压缩。

可以使用流行的压缩算法启用静态和动态压缩,但每种情况下推荐的压缩级别是不同的。

让我们看看压缩算法以更好地理解这一点。


压缩算法

GzipBrotli 是当今用于压缩 HTTP 数据的两种最常用的算法。

Gzip

Gzip 压缩格式已经存在近 30 年,是一种基于 Deflate 算法的无损算法。
deflate 算法本身对输入数据流中的数据块使用 LZ77 算法和霍夫曼编码的组合。

LZ77 算法识别重复的字符串并用反向引用替换它们,反向引用是指向它先前出现的位置的指针,后跟字符串的长度。
随后,霍夫曼编码识别出常用的引用并将它们替换为具有较短位序列的引用。 较长的位序列用于表示不经常使用的引用。

所有主流浏览器都支持 Gzip。

Zopfli 压缩算法是 Deflate/Gzip 的较慢但改进的版本,生成更小的 GZip 兼容文件。
它最适合静态压缩,可以提供更显着的收益。

Brotli

2015年,谷歌推出了Brotli算法和Brotli压缩数据格式。

与 GZip 一样,Brotli 也是一种基于 LZ77 算法和霍夫曼编码的无损算法。

此外,它使用二阶上下文建模以类似的速度产生更密集的压缩。

上下文建模是一项功能,它允许在同一块中为同一字母表使用多个哈夫曼树。

Brotli 还支持用于反向引用的更大窗口大小,并具有静态字典。
这些功能有助于提高其作为压缩算法的效率。

Brotli 受到当今所有主要服务器和浏览器的支持,并且正变得越来越流行。

它还受托管提供商和中间件(包括 Netlify、AWS 和 Vercel)的支持并且可以轻松启用。

拥有庞大用户群的网站,例如 OYO 和 Wix,在将 Gzip 替换为 Brotli 后,其性能有了很大的提升

Gzip vs Brotli

下表显示了不同压缩级别下 Brotli 和 Gzip 压缩率和速度的基准比较。

此外,以下是 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 网络框架,并提供了一个压缩中间件库。

使用它来在请求时压缩任何资产。

Brotli 比其他压缩算法更受推荐,因为它生成的文件更小

对于不支持 Brotli 的浏览器,可以启用 Gzip 作为后备。
如果配置成功,服务器将返回 Content-Encoding HTTP 响应头以指示响应中使用的压缩算法。 例如,

Content-Encoding: br

审计压缩

可以在 Chrome -> DevTools -> network -> Headers 中检查服务器是否压缩了下载的脚本或文本。

DevTools 显示响应中使用的内容编码,如下所示。

Lighthouse 报告包括对“启用文本压缩”的性能审计,该审计检查接收到的基于文本的资源类型,而内容编码标头未设置为“br”、“gzip”或“deflate”。

Lighthouse 使用 Gzip 来计算资源的潜在节省。


JavaScript 压缩和加载粒度

要全面掌握 JavaScript 压缩的效果,还必须考虑 JavaScript 优化的其他方面,例如基于路由的拆分、代码拆分和捆绑。

具有大量 JavaScript 代码的现代 Web 应用程序通常使用不同的代码拆分和捆绑技术来有效地加载代码。

应用程序使用逻辑边界来拆分代码,例如单页应用程序的路由级别拆分或在交互或视口可见性上增量地提供 JavaScript。 可以配置捆绑器以识别这些边界。

在继续讨论这如何影响压缩之前,先介绍一些与代码拆分和捆绑相关的基本定义。


捆绑术语

以下是与我们的讨论相关的一些关键术语。

  1. Module:模块是离散的功能块,旨在提供可靠的抽象和封装。 有关更多详细信息,请参阅模块模式。

  2. Bundle:一组不同的模块,包含源文件的最终版本,并且已经在打包器中进行了加载和编译过程。

  3. Bundle splitting:捆绑器使用的过程将应用程序拆分为多个捆绑包,以便每个捆绑包都可以独立、发布、下载或缓存。

  4. chunk:从 Webpack 术语中采用,块是捆绑和代码拆分过程的最终输出。 Webpack 可以根据入口配置、SplitChunksPlugin 或动态导入将包拆分为块。

如果模块包含在源文件中,则在代码或包拆分后构建过程的最终输出称为 chunk。 请注意,源文件和 chunk 可能相互依赖。

JavaScript 的输出大小是指经过 JavaScript 打包器或编译器优化后的块大小或原始大小。
大型 JS 应用程序可以解构为可独立加载的 JavaScript 文件块。
加载粒度是指输出块的数量——块的数量越多,每个块的大小越小,粒度越高。

一些块比其他块更重要,因为它们加载更频繁或者是更有影响力的代码路径的一部分(例如,加载“结帐”小部件)。
知道哪些块最重要需要应用知识,尽管可以安全地假设“基本”块总是必不可少的。

页面所需的块的每个字节都需要由用户设备下载和解析/执行。
这是直接影响应用程序性能的代码。由于块是最终将被下载的代码,因此压缩块可以提高下载速度。

在此背景下,让我们讨论加载粒度和压缩之间的相互作用。


粒度权衡

在理想的世界中,粒度和分块策略应旨在实现以下相互矛盾的目标。

  1. 提高下载速度:

如前几节所示,可以使用压缩来提高下载速度。
但是,与使用相同的代码压缩多个小块相比,压缩一个大块会产生更好的结果或更小的文件大小。

compress(a + b) <= compress(a) + compress(b)

mited 本地数据表明较小块的损失为 5% 到 10%。 非捆绑块的极端情况显示大小增加了 20%。 额外的 IPC、I/O 和处理成本附加到在较大块的情况下共享的每个块。 v8 引擎具有 30K 流/解析阈值。 这意味着所有小于 30K 的块都会在关键加载路径上解析,即使它是非关键的。

由于上述原因,对于优化下载和浏览器性能的相同代码,较大的块可能比较小的块更有效。

  1. 提高缓存命中率和缓存效率:较小的块导致更好的缓存效率,特别是对于增量加载 JS 的应用程序。
  • 更改被隔离到具有较小块的较少块中。 如果有代码变化,只需要重新下载受影响的chunk,而这些对应的代码大小很可能很小。 剩余的块可以在缓存中找到,从而增加缓存命中的次数。

  • 对于较大的块,很可能会影响较大的代码,并需要在代码更改后重新下载。
    因此,希望使用更小的块来利用缓存机制。

  1. 执行快 - 对于快速执行的代码,它应该满足以下条件。
  • 所有必需的依赖项都很容易获得 - 它们已经一起下载或在缓存中可用。 这意味着您应该将所有相关代码捆绑在一起作为一个更大的块。

  • 只有页面/路由所需的代码才能执行。 这要求不下载或执行额外的代码。 包含公共依赖项的公共块可能具有大多数但不是所有页面所需的依赖项。 重复代码删除需要较小的独立块。

  • 主线程上的长任务会阻塞很长时间。 因此,这些需要分解成更小的块。

如上面的三角形所示,尝试优化上述目标之一的加载粒度可能会使您远离其他目标。 这就是粒度权衡的问题

重复数据删除和缓存与浏览器性能和压缩不一致。

由于这种权衡,当今大多数生产应用程序使用的最大块数约为 10。
需要增加此限制以支持对具有大量 JavaScript 的应用程序更好的缓存和重复数据删除。


SplitChunksPlugin 和 Granular chunking

粒度权衡的潜在解决方案将满足以下要求。

  1. 允许使用更小块大小的更多块(40 到 100),以便在不影响性能的情况下实现更好的缓存和重复数据删除。

  2. 由于 IPC、I/O 和许多脚本标签的处理成本,解决了多个较小块的性能开销。

  3. 在多个较小块的情况下解决压缩损失。

满足这些要求的潜在解决方案仍在开发中。 但是,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 性能问题,

但了解浏览器和打包器在幕后的工作方式有助于创建更好的打包策略,以支持更好的压缩。

加载粒度问题需要跨生态系统中的不同平台解决。

粒度分块可能是朝着这个方向迈出的一步,但我们还有很长的路要走。


知识点