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-unknownTrunk:CSR 应用的构建与开发服务器工具:
cargo install trunkcargo-make(可选):仓库 CI 与批量运行示例时使用:
cargo install --force cargo-make
2.2 快速启动
在示例目录下直接运行:
trunk serve --opentrunk会读取 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组件)由两部分组成:
操作面板(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 行 数据表格:一张
table.table-hover.table-striped.test-data,表格行由For组件按 key 渲染。
Button组件是理解 Leptos#[component]与view!宏的最小范例:它以&'static str接收id与text,在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 行结构:RowData与ArcRwSignal
#[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_COUNTER以std::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_lots:set_data.set(build_data(1000))或build_data(10000)整体替换;add:data.append(&mut build_data(1000))原地追加;update:data.with(...)只读遍历,对step_by(10)的行执行label.update(|n| n.push_str(" !!!")),只触碰信号内部字符串、不重建整行结构,是响应式更新粒度的典型示范;clear:set_data.set(Vec::new());swap_rows:当data.len() > 998时data.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 中For的children):
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_to将App挂载到该节点并返回句柄,handle.forget()表示应用常驻、不随句柄销毁(与 js-framework-benchmark 要求的长生命周期一致)。
九、为基准测试量身定制的编译配置
Cargo.toml 中有两处与性能直接相关的配置:
[profile.release] codegen-units = 1 lto = truecodegen-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特性;源码注释明确提示:进行正式基准测试时,还应追加nightly与delegation特性(前者启用 nightly 通道下的额外优化,后者启用事件委托以摊薄事件监听成本); getrandom需要js特性才能在浏览器环境提供随机数;rand使用small_rng轻量随机数生成器,减少随机填充 10,000 行数据时的生成开销。
十、浏览器内自动化验证:wasm-bindgen-test
tests/web.rs 提供了完整的浏览器端集成测试,验证核心交互链路。测试通过wasm_bindgen_test_configure!(run_in_browser)在真实浏览器中运行,将App挂载到一个临时<section>后依次执行:
- 点击
clear清空,断言表格行数为 0; - 点击
runlots,等待tick().await后断言行数为 10,000; - 点击
add追加,断言行数变为 11,000; - 再次
clear,断言归零。
其中tick().await来自leptos::task,用于等待 Leptos 的响应式刷新与 DOM 更新完成。该测试可直接验证For+template!组合在万行规模下的正确性,可配合cargo make test(对应 wasm-test.toml 定义的测试任务)或wasm-pack test体系运行。
十一、接入官方基准的注意事项
本示例是 js-framework-benchmark 中keyed/leptos目录代码的仓库内移植版,用于演示与自测。若要在官方基准环境中获得可比较的数据,需要留意几点:
- 特性组合:按 Cargo.toml 注释为
leptos追加nightly与delegation特性; - 发布构建:基准成绩基于
release构建(codegen-units = 1、lto = true),并在 index.html 中通过data-wasm-opt="z"对 wasm 做体积优化; - 挂载方式:
main.rs中的handle.forget()保证应用生命周期与页面一致,符合基准对长时运行的假设; - 选择器一致性:按钮 ID(
run、runlots、add、update、clear、swaprows)与表格类名(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),仅供参考