Next.js 实战:在服务端渲染的 React 组件中集成 WebAssembly(Rust 编译示例)
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
本指南围绕 Next.js 官方示例 examples/with-webassembly 展开,讲解如何在 Next.js(App Router)中导入.wasm模块,并将其用在被服务端渲染(SSR/预渲染)的 React 组件中——即让 WebAssembly 代码同时运行在 Node.js 服务端与浏览器端。读完本文,你将掌握 Webpack 5 WebAssembly 实验特性的开启方式、.wasm产物路径配置、Rust 源码编译为 wasm 的命令,以及 Server Component 与 Edge Route 中调用 wasm 导出函数的完整写法。
示例要解决的问题:为什么要在服务端跑 wasm
WebAssembly 最常见的用法是纯客户端场景:浏览器下载.wasm后由 JS 实例化执行。而 with-webassembly 示例演示的是一个更进阶的诉求:
import WebAssembly files (
.wasm) and use them inside of a React component that is server rendered, so the WebAssembly code is executed on the server too.
也就是说,希望把“导入 wasm 模块”这件事做成普通的模块导入,让它既可以出现在客户端组件中,也可以出现在服务端渲染阶段。这样一来,涉及 wasm 的计算逻辑(本示例中是 Rust 编写的add_one函数)既能在服务端预渲染时先行执行、输出静态 HTML,又能在交互阶段被复用,保证了同一份 wasm 业务逻辑在前、后端的行为一致。
示例选用 Rust 编译到 wasm:函数体只做一次自增运算,目的是用最小可运行的代码把“Rust → wasm → Next.js 模块系统”这条链路打通,方便读者迁移到自己的真实逻辑。
目录结构与运行方式
示例位于 examples/with-webassembly,关键文件如下:
| 文件 | 职责 |
|---|---|
| src/add.rs | Rust 源码,导出add_one(x: i32) -> i32 |
| add.wasm | 已编译产物(已随示例提交,可直接运行) |
| add.wasm.d.ts | 为.wasm模块补充的 TypeScript 类型声明 |
| next.config.js | Webpack 5 的 wasm 相关配置 |
| components/RustComponent.tsx | 异步 Server Component,导入并调用 wasm |
| app/page.tsx | 首页,读取 URL 参数并把数字传入组件 |
| app/api/edge/route.ts | 在 Edge Runtime 下使用 wasm 的 API 路由 |
一键引导
与仓库中其他示例一致,可用create-next-app引导出本地项目(对应 README 中的 How to use 章节),三种包管理器任选其一:
npx create-next-app --example with-webassembly with-webassembly-appyarn create next-app --example with-webassembly with-webassembly-apppnpm create next-app --example with-webassembly with-webassembly-app引导完成后进入with-webassembly-app目录执行npm run dev(或yarn dev/pnpm dev)即可访问。首页是一个接收?number=查询参数的页面:
- 打开
http://localhost:3000时默认取number = 30; - app/page.tsx 是一个
async页面组件,先把searchParams.number解析为整数,再交给<RustServerComponent number={number} />渲染; - 页面下方通过 Link 输出
/?number=${number + 1}的“+1”入口,每次点击都会触发一次新的服务端预渲染请求,从而用肉眼观察 wasm 在服务端执行的结果如何写入 HTML。
由于RustServerComponent是异步 Server Component,整个 wasm 调用发生在服务端构建/预渲染阶段,打开浏览器“查看网页源代码”即可看到函数返回值的文本——这正是“服务端也执行 wasm”的直接证据。
从 Rust 源码编译出 add.wasm
README 明确指出:示例仓库中已经包含了编译好的 wasm 文件,因此直接npm run dev无需 Rust 环境;只有当你想修改/重编自己的 Rust 代码时才需要安装 Rust 工具链。
源码本体
src/add.rs 只有短短 4 行:
#[no_mangle] pub extern "C" fn add_one(x: i32) -> i32 { x + 1 }两点说明:
#[no_mangle]防止 Rust 对符号做名称改写(mangling),确保导出的函数名在 wasm 里就叫add_one,便于 JS 侧按名称解构取出;extern "C"使用 C ABI,导出的是(x: i32) -> i32的整数函数,规避了 wasm 与 JS 之间传递字符串/复杂结构时的内存所有权问题——这也是示例刻意保持“纯整数运算”的原因。
编译命令
编译动作被封装在 package.json 的scripts中:
"build-rust": "rustc --target wasm32-unknown-unknown -O --crate-type=cdylib src/add.rs -o add.wasm"对应 README 给出的三种执行方式:
npm run build-rust # 或 yarn build-rust # 或 pnpm build-rust逐一拆解该rustc命令的参数含义:
| 参数 | 作用 |
|---|---|
--target wasm32-unknown-unknown | 指定 wasm 裸机目标。需要 Rust 已安装该 target(首次可执行rustup target add wasm32-unknown-unknown) |
-O | 开启优化,控制产物体积 |
--crate-type=cdylib | 以 C 动态库形式产出,等价于导出 wasm 符号表 |
src/add.rs | 输入源码 |
-o add.wasm | 输出到项目根目录(与src/同级的add.wasm) |
依赖 Rust 工具链的步骤都属于“可选重编译”。若只想体验示例本身,跳过 Rust 安装即可,仓库已内置产物。
在 Server Component 中导入并调用 wasm
核心运行逻辑集中在 components/RustComponent.tsx:
export async function RustServerComponent({ number }: { number: number }) { const exports = await import("../add.wasm"); const { add_one: addOne } = exports; return <>{addOne(number)}</>; }写法上值得注意的细节:
- 使用动态
await import()而非静态import:wasm 模块需要异步实例化,而组件本身是async function的 Server Component,天然支持在渲染前await模块就绪; - 顶层
export:Webpack 对异步 wasm 的处理会把导出函数直接放在模块的具名导出上,因此可以const { add_one: addOne } = exports解构出 Rust 函数; - 函数即插即用:拿到
addOne后可直接在 JSX 中调用并渲染其返回值。
由于该组件是 Server Component,这段代码不会被打进浏览器 bundle 再执行第二次 wasm 实例化——页面 HTML 在服务端就已确定,同时示例的组件层级允许同样的模式被复制到"use client"客户端组件中,从而在交互阶段复用同一份 wasm。
TypeScript 对.wasm的识别
直接import "../add.wasm"在 TS 下会因“未知模块类型”报错,示例通过两步解决:
- add.wasm.d.ts 声明模块形状:
export function add_one(number: Number): Number;- tsconfig.json 的
include数组中显式加入"wasm.d.ts"(注意示例实际文件名是add.wasm.d.ts,与声明的通配语义一致,确保类型文件被 TS 编译器收录):
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "wasm.d.ts"]这样编辑器与next build阶段都能获得add_one的签名提示与类型校验。
支撑这一切的 next.config.js:Webpack 5 的 wasm 配置
浏览器原生支持 wasm 并不代表模块打包器默认会处理.wasm导入。Webpack 5 默认不启用WebAssembly,需要在 next.config.js 中手动开启,并解决服务端/客户端的产物路径差异:
/** @type {import('next').NextConfig} */ const nextConfig = { webpack(config, { isServer, dev }) { // Use the client static directory in the server bundle and prod mode // Fixes `Error occurred prerendering page "/"` config.output.webassemblyModuleFilename = isServer && !dev ? "../static/wasm/[modulehash].wasm" : "static/wasm/[modulehash].wasm"; // Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually config.experiments = { ...config.experiments, asyncWebAssembly: true }; return config; }, }; module.exports = nextConfig;两块配置各有其必要原因:
1.experiments.asyncWebAssembly: true
Webpack 5 以“实验特性”形式提供两类 wasm 支持:syncWebAssembly与asyncWebAssembly。由于同步实例化 wasm 与 Webpack 的模块模型存在冲突,官方建议使用异步版本。开启asyncWebAssembly: true后,.wasm文件就能像普通模块那样被import(),并配合顶层export暴露导出函数。注释里 “Webpack 5 doesn't enable WebAssembly by default” 正是该选项存在的原因。
2.output.webassemblyModuleFilename:解决预渲染报错的关键
wasm 需要作为独立资源输出并由运行时异步加载。服务端(Node)与客户端对资源定位方式不同,因此按isServer && !dev分支分别设置文件名模板:
- 服务端且生产模式:
"../static/wasm/[modulehash].wasm"——相对服务端 chunk 输出目录向上回退一层,落到静态资源目录,以保证构建产物与预渲染(prerender)阶段能正确取到文件; - 其余场景(客户端或 dev):
"static/wasm/[modulehash].wasm"——直接指向站点的静态资源路径。
next.config.js 的注释点明了这么做要修复的问题:Error occurred prerendering page "/"。也就是说,如果不区分服务端与客户端路径,next build在做静态预渲染(本示例首页默认无动态参数,会被预渲染为静态 HTML)时就会因找不到 wasm 资源而报错;[modulehash]则保证不同内容的 wasm 各自拥有独立缓存键。
扩展:在 Edge Runtime 的 Route Handler 中实例化 wasm
示例还额外演示了 wasm 在Edge Runtime下的用法:app/api/edge/route.ts:
import type * as addWasmModule from "../../../add.wasm"; // @ts-ignore import addWasm from "../../../add.wasm?module"; const module$ = WebAssembly.instantiate(addWasm); export async function GET() { const instance = (await module$) as any; const exports = instance.exports as typeof addWasmModule; const { add_one: addOne } = exports; const number = addOne(10); return new Response(`got: ${number}`); } export const runtime = "edge";与 Server Component 相比,这段代码展示了另一条更偏底层的路线:
import addWasm from "../../../add.wasm?module"中的?module是Webpack 资源查询参数,它让导入得到的addWasm是一个WebAssembly.Module对象,而非自动实例化后的导出对象;- 模块顶层用
WebAssembly.instantiate(addWasm)手动实例化,并把 Promise 提升为模块级单例module$,避免每次请求重复编译实例化; GET()内await module$后从instance.exports中取出 Rust 导出的add_one,调用并返回文本响应got: 11;- 文件末尾的
export const runtime = "edge"声明该路由运行在 Edge Runtime,验证了 wasm 也能在边缘环境中被WebAssembly.instantiate正常使用; - 类型层面通过
import type * as addWasmModule from "../../../add.wasm"复用同一份 add.wasm.d.ts 声明,为instance.exports提供形状约束(示例代码中为此加了// @ts-ignore与as any,属于对实验性 API 的权宜处理)。
访问http://localhost:3000/api/edge即可看到该 Route Handler 返回的got: 11,它是访问时在 Edge Runtime 中真正执行 Rust 代码得到的结果。
小结与改造建议
with-webassembly 用不到百行的体量完整演示了 Next.js 中 WebAssembly 的两条落地路径:
- Server Component 路径:
await import()自动实例化 +experiments.asyncWebAssembly,把 wasm 当普通模块使用,代码最简洁,适合计算逻辑在服务端预渲染与客户端复用共存的场景; - Edge Route 路径:
?module查询参数取回WebAssembly.Module,用WebAssembly.instantiate手动管理实例生命周期,适合希望复用单例、精确控制实例化时机的场景。
如果要迁移到自己的项目,请核对以下四件事:
- 开启 next.config.js 中的
experiments.asyncWebAssembly,并按isServer && !dev分支设置webassemblyModuleFilename,否则生产构建的预渲染阶段会报错; - 为每个
.wasm提供对应的.d.ts声明并确保被 tsconfig.json 的include覆盖; - 编译 Rust 时使用
wasm32-unknown-unknowntarget +--crate-type=cdylib,并保持导出函数为简单的数值签名(需要更复杂的数据交换时,需另行引入内存拷贝与编码逻辑); - 计算密集型函数建议用
-O优化编译,并评估在服务端/边缘执行与纯客户端执行两种方案的性能与包体取舍。
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考