本文来自知乎:https://zhuanlan.zhihu.com/p/112223727
作者:温柏甫
因为 WebAssembly 的出现,很多的编程语言被带到了 Web,进入了更多前端er的视野,Rust 就为其中之一。本文将使用 Dodrio 来 渲染十万个待办事项并随机消灭一半(?灭霸本霸),抱着学习使用的心态顺带测试一下它的速度。
Dodrio 是一个用 Rust 和 WebAssembly 编写的虚拟 DOM 库。它利用了 Wasm 的线性内存和 Rust 的低层次控制 api ,围绕指针碰撞(bump allocation)的方式来设计虚拟 DOM 渲染机制。初步的基准测试结果表明它比现有的虚拟 DOM 库性能都高。
link:alloyteam.com/2020/01/d
开始前
白话简介一下相关名词,建议跳过
WebAssembly
是一种编译目标,将 C/C++/Rust/Go 等语言的编译为二进制格式后可供 Javascript 使用。因其跳过了 JavaScript 运作的 Parser 阶段,能带来性能上的提升。
WebAssembly 是被设计成 JavaScript 的完善与补充,而不是一个替代品。
虚拟 DOM (Virtual DOM)
将 DOM 状态生成一份虚拟的树结构,在需要更新的时候,使用差异(diff)算法来尽可能减少调用 DOM 的相关方法(因为性能不好),通常缓存没有变更的组件来避免重新渲染。Virtual DOM 在重复渲染大量数据的时候你能明显感觉到提升,但并不意味着任何场景用了就会带来性能的飞跃,这一点在后文有做简单的测试。
手动斜眼( ﹁ ﹁ ) ,WebAssembly 和 Virtual DOM 都能提升性能,那用 Rust 的 vdom 库来渲染咱的待办列表岂不是快(♂)上加快(♂)。
React vs 原生
在使用 Rust 编写之前,为了打消咱的好奇,决定先测试一下咱们常用的 React(没错,它也是使用了 Virtual DOM 并还带?了一把)和原生的差距。
测试目的
原生 JS 和 采用了 vdom 的框架渲染大量数据的时间上的差距。
测试方式
测试方式为渲染十万个待办列表,然后统计点击第一次随机消灭到完成渲染所需要的时间。
Round 1
使用 create-react-app 创建一个模板项目,修改 App.js:
function App() {
const size = 100 * 100 * 10;
const [todoList, setTodoList] = useState(
Array(size)
.fill(1)
.map((_, index) => `待办事项${index}`)
);
function onDelete() {
setTodoList(todoList.filter(() => Math.random() > 0.5));
}
return (
<div className="App">
<button onClick={onDelete}>随机消灭 Todo</button>
<ul>
{todoList.map((todo, index) => (
<li key={index}> {todo} </li>
))}
</ul>
</div>
);
}
原生则使用拼接 DOM 字符串然后使用 innerHTML
的插入方式。
渲染结果(都挺慢的,转半天?):
我们打开 Chrome 的 Performance 工具,对两个页面进行性能分析,经过多次记录随机消灭(一响指的事儿)的时长,得到以下结果:
实际上在简单渲染文本的情况下,两者都是 4000ms 左右 (Loading + Scripting + Painting),没有太大差距。也验证了并不是使用了虚拟 DOM 就起飞了~。
Round 2
我们尝试给两者都加点料,给文本前面加个小图片(不带颜色的)。
<li key={index}>
<img
src="https://avatars0.githubusercontent.com/u/33797740?s=48&v=4"
alt=""
/>
{todo}
</li>
渲染结果(这次加载的更慢了?):
测试结果:
多次记录后发现,在同一环境下,React 稳定在 6000ms 上下,而原生的时长绝大多数时候都超过了 8000ms。
以上是我触手可测的两种方式,接下来使用 Rust + Dodrio 画上页面来测试。
用 Rust 来画页面
本文的重点从这里开始 ?,且假装大家已经有了 Rust 的相关环境以及了解过 Rust 的基本概念。
直接导入 .rs 文件
Rust 可以编译成 WebAssembly 让 Javascript 调用这我们知道了,可有没有更方便一点的呢,最好是直接导入 .rs(Rust的后缀) 文件。
你别说,在前端生态如此繁荣、各种工具链花样百出的今天,还真有。
Parcel 就是其中之一,它除了能帮我们处理 wasm 文件,也可以处理直接导入的 rs 文件。
贴一段它官网的例子:
// 同步导入import { add } from './add.rs'
console.log(add(2, 3))
// 异步导入const { add } = await import('./add.rs')
console.log(add(2, 3))
// 在 Rust 侧,你只需要确保函数名不是 mangled 而且函数是 public 的即可。
// #[no_mangle]// pub fn add(a: i32, b: i32) -> i32 {// return a + b// }
还,还有更方便的吗?我懒。
rustwasm 提供了一个 rust-parcel-template ,可以试试。
Parcel 很好,但我选择 Webpack
? Rust + ? WebAssembly + Webpack = ❤️
避免偏题,我们直接使用 rust-webpack-template 生成模板项目。
执行 npm init rust-webpack my-app
,用 VSCode 打开项目。目录结构如下:
我们主要关注四个文件:
js/index,js
里面只有一行 import("../pkg/index.js").catch(console.error);
,用来导入被插件处理过的 WebAssembly。
src/lib.rs
Rust 代码的入口,我们也将把逻辑写在这个文件。
Cargo.toml
Rust 的包管理文件,作用和楼下的那货相当。
package.json
我们在 devDependencies
中能找到 @wasm-tool/wasm-pack-plugin,配合上 webpack-dev-server,让我们修改 Rust 代码的时候也能像写网页一样,享受到热更新的服务。
使用 Dodrio
添加依赖
在 Cargo.toml
中新增:
[dependencies]
dodrio = "0.1.0"
# 模板的只声明了 "console"
# 而我们还需要用到其他的
[dependencies.web-sys]
features = [
"Document",
"HtmlElement",
"Node",
"Window"
]
在 src/lib.rs
中使用依赖:
use dodrio::{builder::*, bumpalo, Node, Render};
定义 Todo
定义待办事项的结构体,仅需要一个标题即可。
struct Todo { title: String,}impl Todo { pub fn new(title: String) -> Self { Todo { title: title } }}impl Render for Todo { fn render<'a, 'bump>(&'a self, bump: &'bump bumpalo::Bump) -> Node<'bump> where 'a: 'bump, { // 这一层层的包裹,似曾相识 ? li(bump) .children([ img(bump) .attr( "src", "https://avatars0.githubusercontent.com/u/33797740?s=48&v=4", ) .finish(), text(bumpalo::format!(in bump, "{}", self.title).into_bump_str()), ]) .finish() }}
Rust 调用 Javascript
为了演示 Rust 调用 Javascript 的方法,我们将过滤需要使用的判断随机数的函数放到 JavaScript 中编写,在 Rust 中导入:
// src/lib.rs#[wasm_bindgen]extern "C" { #[wasm_bindgen(js_namespace = rustFns)] pub fn is_del() -> bool;}
wasm_bindgen 是 Rust 官方的一个包,提供 wasm 和 JavaScript 上层交互的能力 文档:github.com/rustwasm/was
// js/index.js
// 简单粗暴的挂在 window 下window.rustFns = {
is_del: () => Math.random() > 0.5
};
import("../pkg/index.js").catch(console.error);
定义 TodoList
struct TodoList { list: Vec<Todo>,}impl TodoList { // 声明一个供按钮回调使用的函数 // .filter 中就调用了来自 JavaScript 的方法 pub fn set_list(&mut self) { let the_list: Vec<Todo> = self .list .drain(..) .into_iter() .filter(|_todo| is_del()) .collect::<Vec<_>>(); self.list = the_list; }}impl Render for TodoList { fn render<'a, 'bump>(&'a self, bump: &'bump bumpalo::Bump) -> Node<'bump> where 'a: 'bump, { use dodrio::bumpalo::collections::Vec; // 定义一个Vec let mut list = Vec::with_capacity_in(self.list.len(), bump); // render 所有的 todo list.extend(self.list.iter().map(|t| t.render(bump))); div(bump) .children([ // 声明一个按钮 button(bump) .on("click", |root, vdom, _event| { let todos = root.unwrap_mut::<TodoList>(); todos.set_list(); // 在下一帧重新渲染 vdom.schedule_render(); }) .children([text("随机消灭 Todo")]) .finish(), // 整一个 ul 再把 所有的 todo 放进去 ul(bump).children(list).finish(), ]) .finish() }}
以上就定义好了我们需要用到的所有内容,部分代码参考 Dodrio 示例 ,尽可能的简单地描绘出我们需要的结构。
启动函数
// 类似与很多语言(除了 js)的主函数#[wasm_bindgen(start)]pub fn main_js() { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().unwrap(); // 生成十万个待办 let vec: Vec<Todo> = (1..100 * 100 * 10) .map(|num| { let mut title = String::from("待办事项"); title.push_str(&num.to_string()); Todo::new(title) }) .collect(); // 绑定到 body 上 let vdom = dodrio::Vdom::new(&body, TodoList { list: vec }); // 一直运行虚拟 DOM 及其侦听器,不会卸载它 vdom.forget()}
不出意外,进项目的根目录起 yarn start
就能跑啦。
测试性能
伴随着的激动的心,颤抖的手,点下了 Record。
但我马上又取消了操作。。
发现了 Ctrl + E 的快捷键怎么能不用它,重来!
聚焦开发者工具 - Ctrl + E
- 点击随机消灭 todo 按钮 ~
一气呵成,熟练的宛如老手 ~
1, 2, 3, 4, 5 ...
经过多次测试。
Scripting + Rendering + Painiting 总时长平均在四秒以上;对比之前的两种方式:
(Rust + Dodrio): 4000ms - 5000ms
React: 6000ms 左右
原生: 8000ms 以上
ps: 测试结果因机而已,只适用于做一个浅显对比。
总结
Rust 还是挺有意思的, 无论是对于前端的友好性或者像所有权(ownership)这种让 Rust 无需垃圾回收(garbage collector)的特性,都是吸引我的点,也推荐前端小伙伴们去了解一哈。好在使用 Dodrio 的过程中不涉及到很多的 Rust 语法(否则就没这篇文章了),顺利完成了这次测试并实际体验了一下 Rust In Web,哪儿不对还请看官多多担待,告辞。
另外,Dodrio 的作者温馨提醒到:
I reiterate that Dodrio is in a very experimental state. It probably has bugs, and no one is using it in production.
再告辞...
Ref
译:使用 rust 和 wasm 实现基于指针碰撞的高效 virtual dom 运算In Web开发
Dodrio 文档
如何理解虚拟DOM?
入门 Rust 开发 WebAssembly
parcel + rustでwasm_bindingを使うとき
Makeflow (makeflow.com) 是以流程为核心的项目管理工具,让研发团队能更容易地落地和改善工作流,提升任务流转体验及效率。如果你正为了研发流程停留在口头讨论、无法落实而烦恼,Makeflow 或许是一个可以尝试的选项。如果你认为 Makeflow 缺少了某些必要的特性,或者有任何建议反馈,可以通过 GitHub、语雀或页面客服与我们建立连接。