用 Rust 和 Node-API 开发高性能 Node.js 模块
如果无法正常显示,请先停止浏览器的去广告插件。
1. 第 十 六 届 D 2 前 端 技 术 论 坛
用 Rust 和 Node-API 开发高性能 Node.js 模块
龙逸楠
2. Node.js 中的 Native Addon
Rust 在前端/Node.js
NAPI-RS 介绍
目录
Rust 与 Node 的未来展望
3. Node.js 中的 Native Addon
4. Node.js 中的 Native Addon
https://xcoder.in/2017/07/01/nodejs-addon-history/
Native Addon 的本质
• .node 格式的文件
• 是二进制文件
• 是一个动态链接库 (Windows DLL/Unix dylib/Linux so)
5. Node.js 中的 Native Addon
https://github.com/nodejs/node/blob/2cc7a91a5d855b4ff78f21f1bb8d4e55131d0615/lib/internal/modules/cjs/loader.js#L1172
.node 格式文件
Node.js 对 .node 格式文件的加载方式:
6. Node.js 中的 Native Addon
https://github.com/nodejs/node/blob/2cc7a91a5d855b4ff78f21f1bb8d4e55131d0615/lib/internal/modules/cjs/loader.js#L1172
.node 格式文件
Node.js 对 .node 格式文件的加载方式:
modules/cjs/loader.js
• Node.js 默认将 Native Addon 当作
CommonJS 格式模块加载
• 使用 process.dlopen 函数加载内容
• 目前源码包含对 ESM 的 import assets
逻辑的兼容,但实际上 Node.js 目前的
最新版 (17.1.0) 只支持 json 类型的
import assert
7. Node.js 中的 Native Addon
https://github.com/Brooooooklyn/blake-hash/releases/tag/v1.3.0
Native Addon 文件内容
在 Linux 上使用 hexyl 查看 blake.darwin-x64.node 文件内容
8. Node.js 中的 Native Addon
https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
Native Addon 文件内容
在 Linux 上使用 hexyl 查看 blake.darwin-x64.node 文件内容
9. Node.js 中的 Native Addon
https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#/media/File:ELF_Executable_and_Linkable_Format_diagram_by_Ange_Al
bertini.png
10. Node.js 中的 Native Addon
https://github.com/nodejs/node/blob/v10.23.0/src/node.cc#L1232
Native Addon 文件内容
1. process.dlopen
2. 初始化 env 与 module.exports 对象,并传入 napi_register_module_v1 函数
3. 拿到返回的 module.exports 对象,后续将 native addon 当作普通的 CommonJS 模块处理
11. Rust 在前端/Node.js
12. Rust 在前端/Node.js
https://leerob.io/blog/rust
• Rust 在前端/Node.js 领域应用的历史
• 流行的 Rust 工具与库
• 作为工具与包的作者,如何发布 Rust 编写的前端/Node.js 工具链
13. Node.js 中的 Native Addon
https://github.com/denoland/deno/issues/208#issuecomment-395739171
Rust 在前端/Node.js 领域应用的历史
Parcel 开始
从 C++ 切换
到 Rust,性
能提升 10 倍
以上
https://parce
ljs.org/blog/b
eta3/
SWC 切换到
N-API, 使用
optionalDep
endencies 的
形式分发预
编译产物
Deno 宣布使
用 Rust
Deno 开始使
用 SWC
NAPI-RS 1.0
发布
Vercel 从
next.js 12 开
始正式引入
Rust 提升开
发体验
Rome 宣布
用 Rust 重写
14. Node.js 中的 Native Addon
目前流行的 Rust 编写的前端/Node.js 库
• Parcel 使用 SWC 与 NAPI-RS 实现 JavaScript/TypeScript 编译与优化逻辑,如 Tree-shaking 与 Scope hoist,minify 等。source map 模
块,png/jpeg 无损压缩,css 压缩与编译模块也是由 Rust + NAPI-RS 编写。
• Rome 是一个集 Linter/Compiler/Bundler/Formatter/Test Runner 为一体的工具链,目的是取代 Babel, ESLint, Webpack, Jest, Prettier 等
前端工具链。 最初由 TypeScript 编写,现已全面切换到 Rust。
• Next.js 是 Vercel 出品的前后端一体化 React 框架,目前它将所有定制的 Babel custom transformer 与默认的 TypeScript/JavaScript 编译
部分切换到了 Rust。
• Prisma 是下一代面向 TypeScript/JavaScript 的 ORM,Prisma 的数据库连接与查询引擎使用 Rust 与 NAPI-RS 实现。
• Ali ICE,Shopify FE 等团队使用 SWC 替换自定义 Babel plugin。
15. Node.js 中的 Native Addon
如何发布 Rust 编写的前端/Node.js 工具链
流行的 Native Addon 发布方式
• 只发布源码,通过 postinstall 脚本执行编译脚本在安装的时候编译成二进制。
• 在 CI 阶段预编译不同平台的二进制文件,在 postinstall 的时候下载下来。
• 在 CI 阶段预编译不同平台的二进制文件,将不同平台的二进制文件单独发布成包,然后通过 package.json 中的 cpu 与 os 字段限制这些
包可安装的平台。最终将所有平台的独立包指定为最终发布包的 optionalDependencies,主流的包管理工具比如 npm/pnpm/yarn 就可以
通过预设的 cpu 与 os 字段只下载对应平台的二进制包。
16. Node.js 中的 Native Addon
只发布源码
优点
• 对于库开发者来说,无需任何额外的工具链维护成本,只需要改动源码就可以轻松发布。
• 在用户的机器上用用户的工具链编译,兼容性是最好的。只要编译通过,基本上就可以运行,不会有系统组件比如 GLIBC libstdc++ 等不
兼容的情况。
缺点
• 大型项目的编译时间过长,并且占用大量 CPU 资源。
• 可能需要用户安装编译相关的工具链,比如 GCC CMAKE LLVM NASM Rust 等,前端开发者一般缺乏相关经验,对于很多项目很难设置
正确。
• 编译期间可能产生大量中间文件,不正确清理极其占用硬盘空间。
• 让很多 CI 的缓存逻辑无法工作,不得不重新全量下载 & 编译。
• postinstall 脚本不安全,未来可能会被默认禁用 https://github.com/npm/rfcs/pull/488
17. Node.js 中的 Native Addon
发布预编译产物,postinstall 的时候下载
优点
• 对于用户来说,减少了编译成本。
• 不需要考虑编译时工具链等问题。
• 没有中间编译产物,节省磁盘空间。
缺点
• postinstall 安装产物不受 lockfile 控制,会破坏很多 CI 的缓存逻辑,降低缓存复用率。
• 存放预编译产物的 CDN 无法照顾到全球的用户,比如大量项目使用 GitHub Releases 作为预编译二进制存放的服务,在国内访问困难。
• 因为要安装时下载,需要引入运行时不需要的依赖来完成下载,比如 node-pre-gyp 等。
• postinstall 脚本不安全,未来可能会被默认禁用 https://github.com/npm/rfcs/pull/488
18. Node.js 中的 Native Addon
https://cdn.jsdelivr.net/npm/@napi-rs/blake-hash/package.json
optionalDependencies
19. Node.js 中的 Native Addon
optionalDependencies
优点
• 无感知,与安装普通 JavaScript 编写的 npm 包体验上没有区别。
• lockfile 与 cache 友好。
• 没有 postinstall 脚本。
缺点
• CI 配置复杂,发布流程繁琐。
• CI 环境链接的系统组件与用户使用环境可能存在不兼容情况,比如 GLIBC 版本。https://github.com/swc-project/swc/issues/1410
20. Node.js 中的 Native Addon
使用 Rust 开发 npm 包,如何选择发布模式
optionalDependencies
• Rust 工具链体积巨大,编译缓慢,中间产物庞大,不适合在用户侧编译。
• postinstall 存在潜在的安全风险,前途未卜。
• 在 postinstall 下载预编译产物对 lockfile 与构建缓存非常不友好。
• 大量 Node 应用在企业私有网络内构建,无法访问外部 CDN,postinstall 下载失败率非常高。
• CI 的复杂配置与相关工具链的维护成本被 NAPI-RS cover 了,作为包作者无需关心。
主流包含 Native 工具链的包比如 esbuild SWC 和 next.js 都采用了 optioanlDependencies 的形式发布。https://github.com/evanw/esbuild/pull/1621
21. NAPI-RS
22. NAPI-RS
https://napi.rs
A framework for building compiled Node.js add-ons in Rust via Node-API
• 介绍
• 使用场景
• 与 neon/node-bindgen 对比
23. NAPI-RS
https://napi.rs
介绍
• 简单易用的 API
• TypeScript .d.ts 文件生成
• 深度集成 Rust Tokio 异步运行时,轻松复用 Rust 异步代码
• 开箱即用的 cli,开发者无需为跨平台编译、自动化 CI 烦恼
24. NAPI-RS
https://github.com/napi-rs/node-rs/tree/main/packages/crc32
简单易用的 API
25. NAPI-RS
https://github.com/napi-rs/napi-rs/blob/main/examples/napi/index.d.ts
TypeScript 类型生成
• 支持 Rust 的 struct + impl 生成 TypeScript Class 对应文件
• 联合类型与可选类型
• 异步类型生成,Rust 的 async fn 到 TypeScript 的 () => Promise<T>
• Rust 代码注释透传到 .d.ts 文件
• 为无法自动精确生成的 Rust 函数重写 TypeScript 类型
26. NAPI-RS
https://github.com/napi-rs/napi-rs/blob/main/examples/napi/index.d.ts
TypeScript 类型自动生成
27. NAPI-RS
https://github.com/napi-rs/napi-rs/blob/main/examples/napi/index.d.ts
TypeScript 类型自动生成
28. NAPI-RS
https://github.com/napi-rs/napi-rs/blob/main/examples/napi/index.d.ts
TypeScript 类型自动生成
29. NAPI-RS
https://github.com/napi-rs/napi-rs/blob/main/examples/napi/index.d.ts
TypeScript 类型自动生成
30. NAPI-RS
https://deno.com/blog/v1.9#native-http2-web-server
Tokio 深度集成
31. NAPI-RS
https://github.com/Brooooooklyn/hns
Tokio 深度集成
• 复用 Tokio 生态,比如可以像 deno 那样将 http server 层用 Rust 实现而不是用 JavaScript 实现
• 可以受益于 Tokio 强大的工具链,比如 tokio-console 与 tokio-tracing
• 提前享受 io_uring 等 libuv 未封装的内核新特性
32. NAPI-RS
https://deno.com/blog/v1.9#native-http2-web-server
Tokio 深度集成
33. NAPI-RS
https://github.com/tokio-rs/console
Tokio 深度集成
34. NAPI-RS
https://github.com/tokio-rs/console
Tokio 深度集成
35. NAPI-RS
https://napi.rs/introduction/getting-started
开箱即用的 CLI
• 新建项目与 GitHub Actions 模板,囊括跨平台编译与发布。
• 便捷的 build 命令,预置各种与 Node.js Addons 相关的编译参数,使用便捷。
• optionalDependencies 版本号管理,发布命令,GitHub CI Artifacts 命名与移动等功能全部封装在内,开发者无
需关系底层繁琐的实现细节与维护。
36. NAPI-RS
https://napi.rs/introduction/getting-started
开箱即用的 CLI
37. NAPI-RS
https://napi.rs/introduction/getting-started
开箱即用的 CLI
38. NAPI-RS
https://napi.rs
使用场景
• 性能敏感的计算场景,输入输出简单。
• 需要使用 Node.js 不支持的 Native 特性,比如特定的 CPU 指令集,调用 GPU、USB 等,调用系统 API 等。
• 在 Server 端共享跨语言与框架的代码,比如 Prisma 使用 Rust 开发核心的 query engine,再封装到 Node.js Go
等。
• 利用 Rust 现代化的工具链 (包管理、跨平台编译等) 粘合现有的 C++ 代码,比如
https://github.com/Brooooooklyn/canvas
39. NAPI-RS
https://github.com/Brooooooklyn/rust-to-nodejs-overhead-benchmark
使用场景
调用开销
40. NAPI-RS
https://github.com/napi-rs/node-rs/tree/main/packages/crc32
使用场景
@node-rs/crc32
41. NAPI-RS
使用场景
各种类型 Hash 计算
42. NAPI-RS
https://github.com/microsoft/node-pty/issues/507
使用场景
node-pty
• 封装系统调用
• 跨平台
• 预编译
43. NAPI-RS
https://github.com/Brooooooklyn/canvas
使用场景
@napi-rs/canvas
• 将 C++ 编写的 Skia 项目编译成 static link library,通过 Rust FFI 机制引入使用。
• 组合现有的 Rust crates 比如 cssparser, base64, avif 等。
• 用 NAPI-RS 封装成 npm 包,对比 node-canvas 0 系统依赖,支持更多系统与 CPU 架构,功能更多。
44. NAPI-RS
https://github.com/Brooooooklyn/canvas
使用场景
@napi-rs/canvas
45. NAPI-RS
https://napi.rs/neon
与 Neon/node-bindgen 对比
• NAPI-RS 有更好的性能/更小的调用开销。
• NAPI-RS 提供从开发到发布的全流程解决方案,而不止是仅仅在 Rust 层提供 Node-API。
• NAPI-RS 对比 Neon 提供的 Node-API 更全面,包含了所有 Node-API 的 C API 封装。
• NAPI-RS 对比 node-bindgen 提供可选的 napi 版本选择,node-bindgen 只能支持最高版本的 Node.js,而 NAPI-
RS 可以通过 features 来选择支持的 Node.js 版本,最低支持到 10.0.0。
• NAPI-RS 与 node-bindgen 的异步运行时不同,NAPI-RS 支持的是 Tokio,而 node-bindgen 支持的是 fluvio
46. Rust 与 Node 的未来展望
47. Rust 与 Node 的未来展望
• Rust 是 JavaScript 基建的未来,Next.js Parcel Prisma Rome 仅仅是革命的开始。
• 随着生态的丰富,未来用 Rust 来编写 Native Addon 会更加便捷,各种配套的基础建设会逐步丰富。
• 除了通过 Node-API ,未来 Rust 会有更多形式为 JavaScript 生态提供服务。比如 Rome 打算嵌入 JavaScript 引擎
并提供简单的 JavaScript API ,而不是依托于 Node-API 。比如 Deno 正在实现 –compact 模式
https://github.com/denoland/deno/issues/12577 。届时通过 Deno + Deno FFI + npm 包也能启动存量的
Node.js 项目。(NAPI-RS 已经计划通过 feature flag 支持无缝编译到 Deno FFI)。NAPI-RS 还计划提供无缝编译到
wasm,让包的发布和维护更简单。
• 越来越多的前端与 Node.js 业务相关的代码会出现在 Rust 的 crates 中,各种自研渲染引擎比如小程序和类
Flutter 方案更容易使用 Rust 开发。以后可能会诞生基于 Rust 生态的集运行时、构建、开发调试工具等周边的
一体化解决方案 。
48. Thanks