用 Brotli 与 Gzip 为 Leptos WASM 提速:构建阶段静态压缩的最佳实践

现代Leptos应用依赖WebAssembly (WASM) 来实现丰富的客户端功能。然而,编译后的WASM二进制文件体积可能相当庞大,从而影响加载速度。幸运的是,像Brotli和gzip这样的压缩算法能够显著减小WASM文件体积,实现更快的交付。本文将对比Brotli与gzip在压缩率、解压速度和浏览器支持方面的差异。我将探讨为何压缩Leptos应用中通常较大的WASM资源如此重要——例如,当前网站的944KB的 .wasm​ 文件在使用gzip压缩后减小至约282KB,而使用Brotli压缩后更是降至219KB(体积减小超过75%),极大地提升了下载速度并节省了带宽。我还将阐释为何静态预压缩(在构建时而非实时压缩资源)是处理WASM文件的理想方法:它能减轻服务器CPU负担,生成确定性的文件哈希,并确保一致的性能。最后,Leptos可以通过使用哈希文件名、预压缩文件到配置Axum服务器和Nginx代理,以实现性能调优。

Brotli压缩后的WASM文件大小
Brotli压缩后的WASM文件大小

Brotli vs. Gzip:压缩率、速度与浏览器支持

压缩效果: Gzip和Brotli都采用了相似的底层原理(如LZ77算法和霍夫曼编码),但Brotli通常能实现更高的压缩率。事实上,在最高压缩设置下,对于相同输入,Brotli生成的文件通常比gzip小15-25%。这意味着Brotli能比gzip削减更多文件大小。例如,一个12.4MB的WASM包,用gzip压缩后为4.8MB,而用Brotli压缩后仅为3.5MB——差异显著。实践中,Brotli压缩的内容通常明显更小,尤其对于像WASM这样的文本或二进制格式。文件越小,加载时间就越短,数据消耗也越少(Choosing Between gzip, Brotli and zStandard Compression | Paul Calvano)。

压缩与解压速度: Gzip的deflate算法压缩速度快,而Brotli的高级压缩(尤其在最高级别下)则更消耗CPU,生成最小文件所需时间更长。然而,如果提前进行压缩(正如我稍后讨论的静态预压缩),较慢的压缩速度通常不成问题。另一方面,Brotli和gzip在客户端的解压速度相当——Brotli专为Web设计,其解压速度与gzip几乎一样快。这意味着浏览器可以迅速解压Brotli压缩的内容,因此使用Brotli不会损害客户端性能。总结来说,gzip压缩更快(适用于实时压缩),而Brotli以较慢的压缩速度换取更小的文件体积(这在离线完成时是可接受的)。两种算法在浏览器中的解压速度都非常快(Text Compression in R: brotli, gzip, xz and bz2)。

浏览器支持: Gzip已存在数十年,并得到所有Web客户端的普遍支持。Brotli相对较新(约2015年推出),但现在已被所有主流现代浏览器支持。截至2025年,超过96%的浏览器会发送 Accept-Encoding: br​ 请求头来表明支持Brotli(Caniuse)。换言之,几乎所有使用Chrome、Firefox、Edge、Safari及其他现代浏览器的用户都能接收Brotli压缩的响应。(缺乏Brotli支持的老旧浏览器则会回退到它们都支持的gzip。)重要的是,Brotli广泛的支持和卓越的压缩性能使其极具吸引力——业界专家称其“性能卓越,并且在WebAssembly代码上表现惊人”。总而言之,同时启用Brotli和gzip可确保用户获得其浏览器所能处理的最佳压缩效果。

超过96%的浏览器支持Brotli
超过96%的浏览器支持Brotli

为何要压缩Leptos应用中的WASM资源?

像Leptos这样的Rust框架生成的WASM文件可能非常大(几百KB甚至几MB),因为它们包含了应用的编译代码和数据。通常,整个WASM包必须在应用初始化之前下载完成,因此减小其体积对于快速启动至关重要。好消息是WASM的压缩效果非常好。Leptos官方文档指出,WebAssembly二进制文件“通常压缩效果很好,典型情况下能缩减到未压缩大小的50%以下”。(Optimizing WASM Binary Size)实践中,节省的体积可能更为可观。

例如,当前网站现目前的Leptos应用的发布版本WASM文件未压缩时为944KB(压缩的crate不小心也编译进了WASM)。使用gzip压缩(级别9)后,文件缩小到约282KB;而使用Brotli压缩(质量11)后,进一步缩小到219KB。这意味着体积减少了超过75%——从近1MB降至约0.22MB。这种大幅度的体积缩减显著减少了应用的下载时间和所需带宽。类似地,一个12.4MB的游戏WASM文件,用gzip压缩后降至4.8MB,用Brotli则降至3.5MB。在这两个例子中,压缩都消除了数MB的数据传输。

WASM之所以对压缩响应如此良好,是因为WebAssembly二进制文件包含许多重复的模式和序列,压缩器可以高效地打包它们。通过压缩这些资源,你基本上是“免费”获得了巨大的性能提升(就网络成本而言)——浏览器唯一额外的工作就是解压,而这个过程非常迅速。网速较慢或设备性能较低的用户尤其受益,因为较小的负载能够更快到达。对于交付一个响应迅速、节省带宽的Leptos应用而言,压缩大型WASM文件是必不可少的

静态预压缩:在构建时压缩WASM,而非实时压缩

在提供压缩内容时,你有两种选择:为每个请求实时压缩(动态压缩),或者提前预压缩文件(静态压缩)然后直接提供。对于Leptos中的WASM文件,静态预压缩是理想选择。这意味着你在构建或部署过程中压缩 .wasm​ 文件(及其他资源),并在服务器上存储压缩版本(例如 .wasm.gz​ 和 .wasm.br​ 文件)。然后,Web服务器直接向支持这些格式的客户端提供预压缩文件,而无需实时压缩。

为何对WASM采用预压缩?主要有以下几个好处:

  • 降低服务器CPU负载: 压缩像WASM模块这样的大文件非常消耗CPU——尤其是在Brotli的最高设置下。如果为每个用户请求都执行此操作,会给服务器带来不必要的压力(或迫使你为了速度而使用较低的压缩级别)。预压缩将这项工作转移到构建时,这样服务器在运行时只需从磁盘读取已压缩的文件。这消除了实时压缩的开销,也正是Nginx等服务器提供 brotli_static​ 选项以提供预压缩文件并避免运行时压缩成本的原因。特别是Brotli压缩“可能非常耗费资源”,因此最好提前一次性完成。通过提供静态的 .br​ / .gz​ 文件,可以节省CPU周期,并用相同的硬件处理更多用户。
  • 最大化压缩,无延迟惩罚: 与上一点相关,静态压缩允许你使用最高的压缩设置(gzip级别9,Brotli级别11)来获得最小的文件体积,而无需担心压缩在请求期间耗时过长。你可能永远不会实时使用Brotli-11(它可能需要几秒钟),但离线操作则可以从容应对。其结果是为用户提供尽可能小的WASM文件,并且在请求时没有任何压缩延迟——服务器只是即时传输压缩后的字节流。这实现了两全其美:最小的文件体积和最小的响应延迟。
  • 确定性的资源哈希与缓存: 在构建期间进行预压缩可确保压缩文件以受控、可重复的方式生成。原始的 .wasm​ 文件内容保持不变,同时生成相应的 .wasm.gz​ / .wasm.br​ 文件。这意味着构建过程也可以为未压缩文件生成有效的内容哈希(指纹)。如果尝试实时压缩,你仍会基于原始文件的哈希进行缓存,但如果服务器或CDN正在修改内容,协调起来可能会很棘手。相比之下,静态压缩与哈希文件名的缓存清除策略完美契合:你可以确切知道为应用的特定版本提供了哪些字节。在Leptos中,它为JS/WASM输出引入了可选的哈希文件名功能,目的正是为了实现长期缓存而不会出现内容陈旧问题。其原理是哈希(例如 app.abcd1234.wasm​)会随着内容的变化而改变,因此你可以为其设置一个超长期的缓存过期时间。预压缩文件与此策略一致,因为它们是在构建时根据内容生成的;文件名中的哈希对于未压缩内容仍然是正确的,而压缩后的变体也对应于同一版本的文件。
  • 一致的性能表现: 使用预压缩资源,应用在提供WASM(及其他静态文件)时的响应时间变得非常可预测。服务器无需为每个请求执行繁重的计算——本质上只是文件查找和发送。这种一致性在高负载下非常重要;即使许多用户同时请求WASM,服务器也不会因压缩每个响应而陷入困境。相比之下,实时压缩可能通过不保留.gz/.br文件来节省存储空间,但它可能引入可变的延迟(例如,前几个请求会产生压缩成本,或者服务器在繁忙时可能不得不限制压缩)。通过提前准备好一切,可以确保每个用户都能快速、持续低延迟地下载WASM资源。额外的好处是,这种方法简化了基础设施——即使是简单的文件服务器或CDN也可以提供压缩文件,无需特殊的动态行为。

静态预压缩利用了这样一个事实:Brotli的优势在于压缩率,而非速度。我利用构建时间(或CI部署时间)来处理缓慢的压缩步骤,生成极其紧凑的WASM文件,然后让服务器快速交付这些文件。这符合Brotli的预期用例: “Brotli在提供静态内容时最为有效” 。Gzip也可以提前处理以最大化其效益。最终结果是:用户下载的WASM更小,服务器也无需承担运行时成本。

Leptos WASM交付的最佳实践

以下是在Leptos应用中交付WASM(及其他静态资源)的一些最佳实践

  • 为WASM输出使用哈希文件名。 在构建中启用基于哈希的文件名,以便每个发布版本的WASM文件都有一个唯一的名称(例如 app.abc123.wasm​)。在Leptos的工具链(例如 cargo-leptos​)中,可以通过在 Cargo.toml​ 中设置 hash-files = true​ 或使用 LEPTOS_HASH_FILES=true​ 环境变量来启用此功能。哈希文件名有助于长期缓存——你可以设置超长期的缓存头,因为当你更新应用时,会生成新的文件名,从而确保客户端获取新版本。这种缓存清除模式(每个版本使用唯一URL)是一种经过验证的最佳实践。

  • 使用gzip和Brotli预压缩WASM文件。 如前所述,在构建/部署时使用最高压缩级别压缩你的 .wasm​ 文件。例如,你可以运行:gzip -kf -9 app.wasm​ (这将生成 app.wasm.gz​)brotli -f -q 11 app.wasm​ (这将生成 app.wasm.br​) 这些命令分别使用gzip级别9和Brotli质量11。-k​ 标志保留原始文件,-f​ 覆盖任何已存在的压缩文件,-q 11​ 将Brotli设置为最高质量。结果将是与原始WASM文件并存的两个压缩文件。由于Brotli压缩可能非常消耗CPU,因此首选提前完成。其回报是显著的大小减少,正如我所见(通常比原始WASM小70%以上)。

  • 配置服务器以提供预压缩文件。 在Rust Axum服务器(使用Tower HTTP库)中,你可以轻松启用此功能。例如,如果你使用 tower_http::services::ServeDir​ 来提供静态文件,请确保在构建服务时调用其 precompressed_br() precompressed_gzip() ​ 方法。这会通知服务器查找文件的 .br​ 或 .gz​ 版本,并在客户端的 Accept-Encoding 表明支持时提供它们。代码可能如下所示:

    use tower_http::services::ServeDir;
    use tower::ServiceBuilder;
    
    let static_service = ServeDir::new("static")
        .precompressed_br()
        .precompressed_gzip();
    

    通过链式调用 .precompressed_br().precompressed_gzip()​,如果浏览器接受Brotli,服务器将自动选择Brotli压缩的文件;如果接受gzip,则选择gzip压缩的文件(此内容协商基于浏览器发送的 Accept-Encoding​ 头)。如果没有声明支持的压缩方式,则会回退到未压缩的文件。这种设置使你的应用免于实时压缩——它只是提供预先制作的文件,并在响应中设置适当的 Content-Encoding: br​ 或 Content-Encoding: gzip​ 头。

  • 为静态资源发送长期缓存头。 鉴于你使用的是每次构建都会更改的哈希文件名,你可以指示浏览器长时间缓存这些文件。建议发送如下的HTTP头:Cache-Control: public, max-age=31536000, immutable​这告诉浏览器(和代理)该资源可以缓存长达一年(31,536,000秒),并且是不可变的(永远不会改变)。“Immutable”指令会阻止浏览器在max-age期间检查更新——因为我知道当内容有更新时,文件名本身会改变。使用这些头部意味着重复访问者(或导航)不会不必要地重新下载WASM;文件将从缓存加载,使你的应用启动瞬间完成(Cache-Control header - HTTP | MDN)。注意: 在开发期间不应使用如此激进的缓存策略,但在生产环境中,这是一个巨大的性能提升。

  • 如果使用反向代理,请确保转发浏览器的 Accept-Encoding头。 如果你的Leptos服务器位于代理或CDN(例如Nginx或Cloudflare)之后,请确保来自客户端的 Accept-Encoding​ 头在到达你的应用程序之前沒有被剥离或更改。服务器需要看到原始的 Accept-Encoding​ (例如 “gzip, br​”)才能提供正确的预压缩文件。某些代理配置默认情况下可能会删除或重置此标头(以便自行处理压缩)。你可能需要显式配置代理以将其传递。例如,在Nginx中,当代理到你的Axum应用时,应设置:proxy_set_header Accept-Encoding $http_accept_encoding;​。这确保了如果浏览器表示它接受Brotli,该信息能到达你的应用服务器。否则,你最终可能总是提供gzip或未压缩版本,因为后端不知道客户端可以处理Brotli。仔细检查你的代理设置,以确保内容协商不受阻碍——当客户端支持时,应优先选择Brotli。目标是确保预压缩资源的好处能够一直传递给最终用户。

哈希化文件名、预压缩资源、为预压缩文件配置服务器、使用正确的缓存头以及正确设置代理——你将确保Leptos应用的WASM负载尽可能高效地交付。用户将因下载文件大幅减小(得益于gzip/Brotli压缩)而享受到更快的加载时间,你的服务器也将因工作负载和带宽使用量的减少而受益。为WASM利用Brotli和gzip是一个简单的步骤,却能带来显著的性能提升,使其成为任何生产Leptos应用不可或缺的优化手段。

你可能也感兴趣