374 - 《Mako 开发日志(7) - NApi》
这几天调整了下 Mako 接入 NApi 的方式,写下心得。
1、背景是,1124 要发内部正式版,然后找了个内网很大的项目做 Benchmark,分别用 Mako、Webpack 和其他基于 Rust 写的构建工具做对比。一对比发现 Mako 咋这么慢。。然后进一步排查发现是和我们使用 Less 的方式有关。这个项目里有 500 多个 Less 文件,意味着要跑 500 多次 Less 编译。而我们之前想着 Less 就是个中间态,就随便上了一种方案,用调 lessc 命令行编译的方式做的。这意味着 lessc 命令行的启动和 less 的初始化都要重复做很多次,小项目可能没啥感觉,但大项目 500 多次就很慢了。
2、新方案是用 NApi 的方式走 Node 函数编译 less,可以让 lessc 命令行的启动和 less 的初始化只做一次。最终结果是,上述项目的模块构建部分的时间,从 21854ms 降低到了 6887ms。
3、NApi 我遇到的有几种使用场景,1)rust 里定义函数,供 js 层调用,这应该是最常见的场景,2)rust 里定义函数,其参数包含 js 函数,rust 里调用 js 函数,3)基于 2,要额外拿到 js 函数的返回值,4)基于 3,js 函数返回值要支持 promise,5)基于 2,js 函数可能在主线程里被调用,也可能在子线程里被调用。
4、定义函数很简单,只要加 #[napi]
即可(注:也可以用 #[js_function()]
)。其第一个参数 env,可选,用于获取当前状态的上下文,但需要注意的是 env 不能在多线程中传递。
#[napi]
fn test(env: Env) {}
test();
5、传 JS 函数给 Rust 用 JsFunction 就好,可以使用他的 .call 方法调用并拿到返回值。需要注意的是,如果要支持异步的 js 方法,比如 Promise,可参考 https://github.com/parcel-bundler/lightningcss/blob/a8b7ea0/node/src/lib.rs#L239 实现。
#[napi]
fn test(env: Env, func: JsFunction) {
let result = func.call(None, &[env.create_string("hello")?])?;
let result: JsString = result.try_into()?;
}
test((message) => {
return `hello ${message}`;
});
6、上述方案比较简单。但是如果要支持多线程,就得用 Threadsafe Function,这时会变得复杂一些。而大部分注重性能的 Rust 应用都会应用多线程,所以这个复杂度通常是绕不开的。一些注意点。1)ThreadsafeFunction 有两个实现,1 是官方的,2 是 lightningcss fork 的,见 https://