js map的get 和list比 那个快_网页上用 Rust 渲染十万个待办事项有多快?

本文来自知乎: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 的插入方式。

渲染结果(都挺慢的,转半天?):

ca4e02873120c02245f70fa1088e98a1.png

我们打开 Chrome 的 Performance 工具,对两个页面进行性能分析,经过多次记录随机消灭(一响指的事儿)的时长,得到以下结果:

203ff83830441a8a973036448cb7a2fa.png
0052caca240afc02847f6cc90625a30f.png

实际上在简单渲染文本的情况下,两者都是 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>

渲染结果(这次加载的更慢了?):

2af6464d8b23613a0231898aa340ec2d.png

测试结果:

ea8338059a6e1bd273678f7713fccaef.png
f3fb580deaf2bd311a366b55433a02c8.png

多次记录后发现,在同一环境下,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 打开项目。目录结构如下:

82304e3662730aeab8aefb6d885df420.png

我们主要关注四个文件:

  • 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。

0f5852b13b5be6c1dd707052d8ce6ab5.png

但我马上又取消了操作。。

发现了 Ctrl + E 的快捷键怎么能不用它,重来!

聚焦开发者工具 - Ctrl + E - 点击随机消灭 todo 按钮 ~

一气呵成,熟练的宛如老手 ~

19407e9ff77211965a79531a16882241.png

1, 2, 3, 4, 5 ...

经过多次测试。

0578fcddc096fc369a603ce4eeceb261.png

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、语雀或页面客服与我们建立连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值