Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

优化 WASM 二进制大小

Optimizing WASM Binary Size

WebAssembly 二进制文件通常比同等应用程序的 JavaScript 包要大得多。因为 WASM 格式是为流式编译设计的,所以每千字节 WASM 文件的编译速度比 JavaScript 文件快得多。(欲了解更多信息,你可以阅读 Mozilla 团队关于流式 WASM 编译的这篇精彩文章。)尽管如此,向用户发送尽可能小的 WASM 二进制文件仍然很重要,因为这会减少他们的网络使用,并使你的应用尽快达到可交互状态。

WebAssembly binaries are significantly larger than the JavaScript bundles you’d expect for the equivalent application. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can read this great article from the Mozilla team on streaming WASM compilation.) Still, it’s important to ship the smallest WASM binary to users that you can, as it will reduce their network usage and make your app interactive as quickly as possible.

那么有哪些实际的步骤呢?

So what are some practical steps?

该做的事情

Things to Do

  1. 确保你查看的是发布(release)构建。(调试构建要大得多。)

  2. Make sure you’re looking at a release build. (Debug builds are much, much larger.)

  3. 为 WASM 添加一个针对大小(而不是速度)进行优化的发布配置(profile)。

  4. Add a release profile for WASM that optimizes for size, not speed.

例如,对于一个 cargo-leptos 项目,你可以将以下内容添加到 Cargo.toml 中:

For a cargo-leptos project, for example, you can add this to your Cargo.toml:

[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1

# ....

[package.metadata.leptos]
# ....
lib-profile-release = "wasm-release"

这将为你的发布构建极度优化 WASM 的大小,同时保持你的服务器构建针对速度进行优化。(对于没有服务器考量的纯客户端渲染应用,只需将 [profile.wasm-release] 块作为你的 [profile.release] 即可。)

This will hyper-optimize the WASM for your release build for size, while keeping your server build optimized for speed. (For a pure client-rendered app without server considerations, just use the [profile.wasm-release] block as your [profile.release].)

  1. 在生产环境中始终提供压缩后的 WASM。WASM 的压缩效果通常非常好,压缩后的大小通常不到未压缩大小的 50%,而且在 Actix 或 Axum 中为静态文件启用压缩非常简单。

  2. Always serve compressed WASM in production. WASM tends to compress very well, typically shrinking to less than 50% its uncompressed size, and it’s trivial to enable compression for static files being served from Actix or Axum.

  3. 如果你使用的是 nightly Rust,你可以使用相同的配置重新构建标准库,而不是使用随 wasm32-unknown-unknown 目标分发的预编译标准库。

  4. If you’re using nightly Rust, you can rebuild the standard library with this same profile rather than the prebuilt standard library that’s distributed with the wasm32-unknown-unknown target.

为此,请在项目的 .cargo/config.toml 中创建一个文件:

To do this, create a file in your project at .cargo/config.toml

[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]

请注意,如果你也将其用于 SSR,则会应用相同的 Cargo 配置。你需要明确指定你的目标:

Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:

[build]
target = "x86_64-unknown-linux-gnu" # 或其他目标 (or whatever)

另外请注意,在某些情况下,cfg 特性 has_std 将不会被设置,这可能会导致一些检查 has_std 的依赖项出现构建错误。你可以通过添加以下内容来修复由此产生的任何构建错误:

Also note that in some cases, the cfg feature has_std will not be set, which may cause build errors with some dependencies which check for has_std. You may fix any build errors due to this by adding:

[build]
rustflags = ["--cfg=has_std"]

并且你需要在 Cargo.toml[profile.release] 中添加 panic = "abort"。请注意,这会将相同的 build-std 和 panic 设置应用于你的服务器二进制文件,这可能不是你想要的。这里可能需要进一步探索。

And you'll need to add panic = "abort" to [profile.release] in Cargo.toml. Note that this applies the same build-std and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.

  1. WASM 二进制文件大小的来源之一可能是 serde 序列化/反序列化代码。Leptos 默认使用 serde 来序列化和反序列化使用 Resource::new() 创建的资源。leptos_server 包含额外的特性,通过添加额外的 new_ 方法来激活替代编码。例如,激活 leptos_server crate 上的 miniserde 特性会添加 Resource::new_miniserde() 方法,而 serde-lite 特性会添加 new_serde_liteminiserdeserde-lite 仅实现了 serde 功能的一个子集,但通常在优化时更看重二进制大小而非速度。

  2. One of the sources of binary size in WASM binaries can be serde serialization/deserialization code. Leptos uses serde by default to serialize and deserialize resources created with Resource::new(). leptos_server includes additional features to activate alternative encodings by adding additional new_ methods. For example, activating the miniserde feature on the leptos_server crate adds a Resource::new_miniserde() method, and the serde-lite feature adds new_serde_lite. miniserde and serde-lite only implement subsets of serde’s functionality, but typically optimize for binary size over speed.

该避免的事情

Things to Avoid

某些 crate 往往会膨胀二进制文件的大小。例如,带有默认特性的 regex crate 会增加大约 500kb 的 WASM 二进制大小(主要是因为它必须拉入 Unicode 表数据!)。在对大小敏感的环境中,你可能会考虑通常避免使用正则表达式,甚至降级调用浏览器 API 来使用内置的正则引擎。(这就是 leptos_router 在少数需要正则表达式的情况下所做的。)

There are certain crates that tend to inflate binary sizes. For example, the regex crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!). In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what leptos_router does on the few occasions it needs a regular expression.)

一般来说,Rust 对运行时性能的承诺有时会与对小体积二进制文件的承诺相冲突。例如,Rust 会对泛型函数进行单态化(monomorphizes),这意味着它会为调用的每个泛型类型创建一个该函数的独特副本。这比动态分发快得多,但增加了二进制大小。Leptos 尝试非常仔细地平衡运行时性能与二进制大小的考量;但你可能会发现,编写使用许多泛型的代码往往会增加二进制大小。例如,如果你有一个主体代码很多的泛型组件,并使用四种不同的类型调用它,请记住编译器可能会包含该代码的四个副本。重构为使用具体的内部函数或辅助函数,通常可以在保持性能和人体工程学的同时减小二进制大小。

In general, Rust’s commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type it’s called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size.

代码分割

Code Splitting

cargo-leptos 以及 Leptos 框架和路由支持 WASM 二进制分割。(请注意,此支持于 2025 年夏季发布;取决于你阅读本文的时间,我们可能仍在修复 bug。)

cargo-leptos and the Leptos framework and router have support for WASM binary splitting. (Note that this support was released during the summer of 2025; depending on when you’re reading this, we may still be ironing out bugs.)

这可以通过组合使用三种工具来实现:cargo leptos (serve|watch|build) --split#[lazy] 宏以及 #[lazy_route] 宏(配合 LazyRoute trait)。

This can be used through the combination of three tools: cargo leptos (serve|watch|build) --split, the #[lazy] macro, and the #[lazy_route] macro (paired with the LazyRoute trait).

#[lazy]

#[lazy] 宏表示一个函数可以从单独的 WebAssembly (WASM) 二进制文件中延迟加载。它可以用来标注同步或异步函数;在任何一种情况下,它都会生成一个异步函数。你第一次调用该延迟加载函数时,该独立的代码块将从服务器加载并被调用。随后,它将在没有额外加载步骤的情况下被调用。

The #[lazy] macro indicates that a function can be lazy-loaded from a separate WebAssembly (WASM) binary. It can be used to annotate a synchronous or async function; in either case, it will produce an async function. The first time you call the lazy-loaded function, that separate chunk of code will be loaded from the server and called. Subsequently, it will be called without an additional loading step.

#[lazy]
fn lazy_synchronous_function() -> String {
    "Hello, lazy world!".to_string()
}

#[lazy]
async fn lazy_async_function() -> String {
    /* 执行一些需要异步工作的任务 */
    /* do something that requires async work */
    "Hello, lazy async world!".to_string()
}

async fn use_lazy_functions() {
    // 同步函数已被转换为异步
    // synchronous function has been converted to async
    let value1 = lazy_synchronous_function().await;

    // 异步函数仍然是异步的
    // async function is still async
    let value1 = lazy_async_function().await;
}

这对于一次性的延迟函数很有用。但当延迟加载与路由配合使用时,它的功能最为强大。

This can be useful for one-off lazy functions. But lazy-loading is most powerful when it’s paired with the router.

#[lazy_route]

延迟路由(Lazy routes)允许你将路由视图的代码拆分出来,并在导航时与该路由的数据并发地延迟加载它。通过使用嵌套路由,可以嵌套多个延迟加载路由:每个路由都将并发加载其自身的数据和自身的延迟视图。

Lazy routes allow you to split out the code for a route’s view, and to lazily load it concurrently with data for that route while navigating. Through the use of nested routing, multiple lazy-loaded routes can be nested: each will load its own data and its own lazy view concurrently.

将数据加载与(延迟加载的)视图分离,可以防止“瀑布流”现象,即你等待延迟视图加载,然后才开始加载数据。

Splitting the data loading from the (lazy-loaded) view allows you to prevent a “waterfall,” in which you wait for the lazy view to load, then begin loading data.

use leptos::prelude::*;
use leptos_router::{lazy_route, LazyRoute};

// 路由定义
// the route definition
#[derive(Debug)]
struct BlogListingRoute {
    titles: Resource<Vec<String>>
}

#[lazy_route]
impl LazyRoute for BlogListingRoute {
    fn data() -> Self {
        Self {
            titles: Resource::new(|| (), |_| async {
                vec![/* 待办:加载博客文章 (todo: load blog posts) */]
            })
        }
    }

    // 此函数将被延迟加载,与 data() 并发执行
    // this function will be lazy-loaded, concurrently with data()
    fn view(this: Self) -> AnyView {
        let BlogListingRoute { titles } = this;

        // ... 现在你可以将 `posts` 资源与 Suspense 等一起使用,
        // 并通过在视图上调用 .into_any() 返回 AnyView
        // ... now you can use the `posts` resource with Suspense, etc.,
        // and return AnyView by calling .into_any() on a view
    }
}

示例与更多信息

Examples and More Information

你可以在这个 YouTube 视频中找到更深入的讨论,并在仓库中找到完整的 lazy_routes 示例。

You can find more in-depth discussion in this YouTube video, and a full lazy_routes example in the repo.