5

网页上用 Rust 渲染十万个待办事项有多快?

 3 years ago
source link: https://zhuanlan.zhihu.com/p/112223727
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

网页上用 Rust 渲染十万个待办事项有多快?

因为 WebAssembly 的出现,很多的编程语言被带到了 Web,进入了更多前端er的视野,Rust 就为其中之一。本文将使用 Dodrio渲染十万个待办事项并随机消灭一半( 灭霸本霸),抱着学习使用的心态顺带测试一下它的速度。

Dodrio 是一个用 Rust 和 WebAssembly 编写的虚拟 DOM 库。它利用了 Wasm 的线性内存和 Rust 的低层次控制 api ,围绕指针碰撞(bump allocation)的方式来设计虚拟 DOM 渲染机制。初步的基准测试结果表明它比现有的虚拟 DOM 库性能都高。
link:http://www.alloyteam.com/2020/01/dodrio-rust-wasm-fast-vdom/


白话简介一下相关名词,建议跳过

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 上层交互的能力 文档:https://github.com/rustwasm/wasm-bindgen

// 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.

再告辞...


Makeflow (makeflow.com) 是以流程为核心的项目管理工具,让研发团队能更容易地落地和改善工作流,提升任务流转体验及效率。如果你正为了研发流程停留在口头讨论、无法落实而烦恼,Makeflow 或许是一个可以尝试的选项。如果你认为 Makeflow 缺少了某些必要的特性,或者有任何建议反馈,可以通过 GitHub语雀或页面客服与我们建立连接。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK