Leptos js-framework-benchmark 示例解析:用 template! 宏与 For 组件构建万行级高性能表格
2026/9/13 4:09:20 网站建设 项目流程

Leptos js-framework-benchmark 示例解析:用 template! 宏与 For 组件构建万行级高性能表格

【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos

导读

本文围绕 Leptos 仓库中的 js-framework-benchmark 示例 展开,它改编自著名的 js-framework-benchmark(keyed 实现),用于在统一的基准测试框架下验证 Leptos 渲染 1,000~10,000 行随机数据表格时的真实性能。读完本文,你将掌握该示例的启动方式、template!宏与For组件在大型列表渲染中的正确用法、响应式数据结构的组织方式,以及为基准测试优化的编译配置与 WebAssembly 浏览器测试方案。

一、示例定位:Leptos 在 js-framework-benchmark 中的 keyed 实现

js-framework-benchmark 是前端社区通用的性能对比基准,其 keyed 测试要求框架在每次数据更新时按唯一 key 复用 DOM 节点。本示例正是该基准中 Leptos 一方的客户端渲染(CSR)实现,位于 examples/js-framework-benchmark,具备以下两个典型特征:

  • 生成一张包含随机条目的大表格:通过随机词库拼装 "pretty red table" 这类标签,配合原子自增 ID 构建海量行数据,模拟真实列表页的高频增删改场景;
  • 集中演示 Leptos 两大高性能渲染原语template!宏(编译期模板预渲染)与For组件(keyed 列表差分更新)。

示例入口由 src/lib.rs(组件与业务逻辑)和 src/main.rs(浏览器挂载)组成,配套 index.html 提供页面骨架,tests/web.rs 提供浏览器内自动化测试。

二、环境准备与快速启动

2.1 依赖工具

运行本示例需要以下工具链(完整说明见 Examples README):

  • Rust 工具链,并添加wasm32-unknown-unknown编译目标:

    rustup toolchain install nightly rustup target add wasm32-unknown-unknown
  • Trunk:CSR 应用的构建与开发服务器工具:

    cargo install trunk
  • cargo-make(可选):仓库 CI 与批量运行示例时使用:

    cargo install --force cargo-make

2.2 快速启动

在示例目录下直接运行:

trunk serve --open

trunk会读取 index.html,其中通过<link>cargo make ci # 安装依赖并测试 cargo make start # 启动开发服务器(默认 127.0.0.1:8080 或 127.0.0.1:3000) cargo make stop # 结束相关进程

示例的 Makefile.toml 扩展了 cargo-make/main.toml(通用任务)、wasm-test.toml(WebAssembly 测试)与 trunk_server.toml(Trunk 服务器),并额外定义了面向wasm32-unknown-unknown的全特性 clippy 检查任务clippy-each-feature

三、页面骨架与交互面板

index.html 定义了页面基础结构:<div id='main'>是 Leptos 的挂载点,<base href="bundled-dist/">保证静态资源路径正确,并通过 CDN 引入 Bootstrap 5.2.3 样式。页面内置一段行内样式.danger { background: gold },对应选中行的金色高亮效果。

应用界面(见 src/lib.rs 中的App组件)由两部分组成:

  1. 操作面板(jumbotron 区):通过可复用组件Button渲染 6 个按钮,分别对应 js-framework-benchmark 的标准测试动作:

    按钮 ID文案触发的动作
    runCreate 1,000 rows生成并渲染 1,000 行随机数据
    runlotsCreate 10,000 rows生成并渲染 10,000 行随机数据
    addAppend 1,000 rows在现有数据后追加 1,000 行
    updateUpdate every 10th row每隔 10 行在标签末尾追加 " !!!"
    clearClear清空全部数据
    swaprowsSwap Rows交换第 1 行与第 998 行
  2. 数据表格:一张table.table-hover.table-striped.test-data,表格行由For组件按 key 渲染。

Button组件是理解 Leptos#[component]view!宏的最小范例:它以&'static str接收idtext,在view!中直接内插{text},并通过on:click=run事件绑定将按钮与信号更新闭包相连。

四、数据模型:随机数据生成与响应式行结构

4.1 随机标签词库

build_data从三组静态词库中随机抽取形容词(ADJECTIVES)、颜色(COLOURS)和名词(NOUNS),拼装出形如 "pretty red table" 的标签。值得注意的是,代码在构造字符串前先计算adjective.len() + colour.len() + noun.len() + 2作为String::with_capacity的容量,避免拼接过程中的重复扩容——这一细节对基准测试场景下的分配开销有明显影响。

4.2 行结构:RowDataArcRwSignal

#[derive(Debug, Clone, PartialEq, Eq, Hash)] struct RowData { id: usize, label: ArcRwSignal<String>, }

每行的label不是普通String,而是 Leptos 的ArcRwSignal<String>。这样设计有两个目的:

  • label本身是响应式信号,update按钮只需要原地push_str(" !!!")For内的文本节点即可精确刷新;
  • 由于 For 组件 的children闭包需要Send + Clone,使用Arc封装的信号可以让行数据在多个闭包之间廉价克隆共享。

4.3 原子 ID 计数器

static ID_COUNTER: AtomicUsize = AtomicUsize::new(1);

ID_COUNTERstd::sync::atomic原子自增方式为每行分配全局唯一 ID(从 1 开始),配合Relaxed内存序,保证并发/重入场景下 ID 不重复。这个 ID 正是For组件做 keyed 差分所依赖的稳定键。

五、响应式状态组织:signal、update 与 Selector

App组件使用两个顶层信号驱动整个页面:

let (data, set_data) = signal(Vec::<RowData>::new()); let (selected, set_selected) = signal(None::<usize>);
  • data保存全部行数据,所有按钮动作都通过set_data.set(...)整体替换或set_data.update(...)原地修改:
    • run/run_lotsset_data.set(build_data(1000))build_data(10000)整体替换;
    • adddata.append(&mut build_data(1000))原地追加;
    • updatedata.with(...)只读遍历,对step_by(10)的行执行label.update(|n| n.push_str(" !!!"))只触碰信号内部字符串、不重建整行结构,是响应式更新粒度的典型示范;
    • clearset_data.set(Vec::new())
    • swap_rows:当data.len() > 998data.swap(1, 998),直接交换两个元素位置。
  • selected记录当前选中行 ID,点击行内链接时set_selected.set(Some(row_id))

选中态的高效判断依赖Selector

let is_selected = Selector::new(move || selected.get());

Selector会为每个观察到的键值维护独立的响应式订阅,相比在每行闭包中反复selected.get()比较,能显著降低高选中切换频率场景下的无效计算量。

六、For组件:keyed 列表渲染的原理

表格主体完全由 For 组件 渲染:

<For each=move || data.get() key=|row| row.id children=move |row: RowData| { /* template! { ... } */ } />

从 源码 可以看到For的核心实现逻辑:

  • 三个参数each是返回迭代器的函数(这里直接读取信号data);key为每行提取唯一键(row.id,满足K: Eq + Hash + SerializableKey);children把每个条目映射为视图;
  • 调用keyed(each(), key.clone(), children.clone())keyed会对新旧列表按键做差分,仅创建新增节点的 DOM、复用未变化的节点,这正是“keyed 基准”要求的行为,也解释了 文档注释 所述“比逐节点.iter().map(...)更高效,因为它避免重建未变化的 DOM 节点”;
  • 每个子行独立 reactive owner:源码中For先取得当前Owner,再为每个子项创建新的Owner,保证行级 effect 与 context 互不干扰,行更新时其他行不受影响(见 for_loop.rs)。

七、template!宏:编译期模板预渲染优化

每个表格行的视图使用template!宏生成(见 lib.rs 中Forchildren):

template! { < tr class : danger = { move || is_selected.selected(&Some(row_id)) } > < td class = "col-md-1" > { row_id.to_string() } </ td > < td class = "col-md-4" > < a on : click = move | _ | set_selected.set(Some(row_id)) > { move || label.get() } </ a > </ td > < td class = "col-md-1" > < a on : click = move | _ | remove(row_id) > < span class = "glyphicon glyphicon-remove" aria - hidden = "true" ></ span > </ a > </ td > < td class = "col-md-6" /> </ tr > }

template!是 Leptos 在view!之外的第二种视图宏,二者的关系可以从 leptos_macro 源码 的文档注释得到权威解释:template!view!行为等价,但会把整棵视图树包装进ViewTemplate在编译期将大部分 HTML 渲染为<template>标签内容,运行时直接克隆模板再水合动态部分,从而优化创建速度,代价是少量二进制体积开销。

在该示例中,行结构(表格行、单元格、删除图标等静态部分)被模板化,仅class:danger高亮、ID 文本与标签文本是动态节点;当update只修改第 10、20、30… 行的标签时,模板机制与For的 keyed 差分叠加,使每行的 DOM 创建与更新开销被压到最低。动态部分同样展示了 Leptos 的典型写法:

  • class: danger = { move || ... }根据响应式表达式条件切换类名;
  • { move || label.get() }将信号读取封装进闭包,建立精确的文本节点订阅。

八、浏览器挂载:mount_to 与 DOM 查询

main.rs 展示了无框架包装下的手动挂载流程:

pub fn main() { console_error_panic_hook::set_once(); let root = document().query_selector("#main").unwrap().unwrap(); let handle = mount_to(root.unchecked_into(), App); handle.forget(); }
  • console_error_panic_hook::set_once()让 WebAssembly 中的 panic 输出到浏览器控制台,便于调试;
  • 通过leptos_dom::helpers::document查询#main节点作为挂载根;
  • mount_toApp挂载到该节点并返回句柄,handle.forget()表示应用常驻、不随句柄销毁(与 js-framework-benchmark 要求的长生命周期一致)。

九、为基准测试量身定制的编译配置

Cargo.toml 中有两处与性能直接相关的配置:

[profile.release] codegen-units = 1 lto = true
  • codegen-units = 1:关闭并行代码生成单元,让优化器能看到整个 crate 进行跨函数优化;
  • lto = true:开启全程序链接时优化,进一步内联与消除冗余代码。

两者都牺牲编译时间换取运行时性能,是发布基准构建的标准做法。依赖部分同样值得注意:

leptos = { path = "../../leptos", features = ["csr"] } # for actual benchmarking, add `nightly` and `delegation` features getrandom = { version = "0.2.15", features = ["js"] } rand = { version = "0.8.5", features = ["small_rng"] }
  • 示例通过path直接依赖仓库内的 leptos crate,并启用csr特性;源码注释明确提示:进行正式基准测试时,还应追加nightlydelegation特性(前者启用 nightly 通道下的额外优化,后者启用事件委托以摊薄事件监听成本);
  • getrandom需要js特性才能在浏览器环境提供随机数;
  • rand使用small_rng轻量随机数生成器,减少随机填充 10,000 行数据时的生成开销。

十、浏览器内自动化验证:wasm-bindgen-test

tests/web.rs 提供了完整的浏览器端集成测试,验证核心交互链路。测试通过wasm_bindgen_test_configure!(run_in_browser)在真实浏览器中运行,将App挂载到一个临时<section>后依次执行:

  1. 点击clear清空,断言表格行数为 0;
  2. 点击runlots,等待tick().await后断言行数为 10,000;
  3. 点击add追加,断言行数变为 11,000;
  4. 再次clear,断言归零。

其中tick().await来自leptos::task,用于等待 Leptos 的响应式刷新与 DOM 更新完成。该测试可直接验证For+template!组合在万行规模下的正确性,可配合cargo make test(对应 wasm-test.toml 定义的测试任务)或wasm-pack test体系运行。

十一、接入官方基准的注意事项

本示例是 js-framework-benchmark 中keyed/leptos目录代码的仓库内移植版,用于演示与自测。若要在官方基准环境中获得可比较的数据,需要留意几点:

  1. 特性组合:按 Cargo.toml 注释为leptos追加nightlydelegation特性;
  2. 发布构建:基准成绩基于release构建(codegen-units = 1lto = true),并在 index.html 中通过data-wasm-opt="z"对 wasm 做体积优化;
  3. 挂载方式main.rs中的handle.forget()保证应用生命周期与页面一致,符合基准对长时运行的假设;
  4. 选择器一致性:按钮 ID(runrunlotsaddupdateclearswaprows)与表格类名(test-data)严格遵循 js-framework-benchmark 的约定,驱动脚本正是依据这些选择器触发动作并采样的。

十二、总结

js-framework-benchmark 示例是理解 Leptos 高性能渲染路径的最佳入口之一:ArcRwSignal提供了细粒度的行级响应式更新,For组件通过 keyed 差分复用 DOM 节点,template!宏把静态骨架下沉到编译期,三者叠加正是该示例能在万行表格上流畅完成增、删、改、换操作的技术基石。对照 leptos/src/for_loop.rs 与 leptos_macro/src/lib.rs 的源码,你可以把这里的模式直接迁移到自己的大型表格、长列表或虚拟滚动场景中。

【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询