想象两座隔水相望的城市:东岸是 JS 城,街道蜿蜒、生活便利,几乎所有网页居民都住在这里;西岸是 WASM 城,工厂林立、机器轰鸣,能以接近原生引擎的速度完成重型计算。两城之间只靠一座收费关桥相连。关桥的规矩很奇怪:不管你是去送一封信,还是去扛十台机器,只要过桥,就交一笔固定的过桥费。于是精彩的故事开始了——有人为了算一个数来回过桥十趟,结果过桥费比算数本身还贵;有人把一车货集中运一次,成本立刻降了下来;还有人在桥中间修了一座共享保税仓,双方凭货位号直接取货,连包装都省了。这个故事讲的就是 WASM 与 JS 互操作的性能边界:真正昂贵的往往不是计算,而是“过桥”的频率、货物的“打包”方式,以及谁来修仓库、谁来管物流。本文就带你逛遍这座关桥,看清费用从哪来、怎么省、什么时候干脆别过桥。

岗位关联

理解这条性能边界,能帮你在前端工程化决策中准确判断“哪些计算该进 WASM、哪些留在 JS”,避免一顿优化反而更慢的尴尬,是浏览器端 AI 应用落地的基本功。

目录
  1. 可视化演示
  2. 互操作成本来源
  3. 内存共享策略
  4. 数据序列化开销
  5. 批量调用优化
  6. 回调频率管理
  7. 工具链方案对比
  8. 性能测量与落地
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示WASM与JS互操作的性能边界:边界穿越开销、线性内存共享与批量调用优化策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

边界穿越成本 JS调用 穿越 WASM import 每次穿越有固定开销

互操作成本来源

在 JS 城与 WASM 城之间,那座收费关桥的规矩是:不管你是去送一封信,还是去扛十台机器,只要过桥,就收 2 元。小镇居民很快发现,让办事员为了核对一个数字就来回跑一趟,过桥费比办事本身还贵。这听起来荒诞,却是无数 WASM 项目性能翻车的真实原因。

落在工程语言里,“过桥”就是一次 JS 调用 WASM 函数。浏览器在执行这次调用时,要完成引擎切换、参数校验、栈空间切换等一系列动作,这些动作的总和是一笔固定开销,与函数内部的计算量无关。你调用一个 1 微秒的函数,和一个 1 毫秒的函数,过路费一样。

于是互操作的总耗时可以写成:调用次数乘单次固定开销,再加上真实计算时间。当单次计算很轻、调用又频繁时,固定开销项会占绝对主导,甚至让 WASM 比纯 JS 更慢——因为纯 JS 至少不必反复“换乘引擎”。

公式看懂了,桥对面也就看清了:要提高互操作性能,第一优先级往往不是优化 WASM 里的算法,而是减少过桥次数。桥费是按趟收的,聪明人不会让办事员一趟趟空跑。

T总=N×C过桥+∑i=1Nt计算(i)\displaystyle T_{\text{总}} = N \times C_{\text{过桥}} + \sum_{i=1}^{N} t_{\text{计算}}^{(i)}
这条公式是说:总耗时由“过桥费”和“真干活”两部分组成,过桥费按次数收、与任务大小无关。
  1. 调用次数 \(N\)业务中调用 WASM 函数的次数,是成本的第一放大器,N 越大固定开销越吓人。
    N∈{1,2,3,… }N \in \{1, 2, 3, \dots\}
  2. 固定开销 \(C_{\text{过桥}}\)每次跨界都要支付的“过桥费”,包括引擎切换、参数校验与栈切换。
    C过桥=t切换+t校验C_{\text{过桥}} = t_{\text{切换}} + t_{\text{校验}}
  3. 计算时间累加 \(\sum t^{(i)}\)把每次调用里真正干活的时间加起来,这部分才是 WASM 的价值所在。
    ∑i=1Nt计算(i)=N×tˉ\sum_{i=1}^{N} t_{\text{计算}}^{(i)} = N \times \bar{t}

内存共享策略

一位聪明的市长提出:与其让大家一趟趟搬货过桥,不如在桥中央修一座共享保税仓。双方各拿一把钥匙,JS 城把“货位编号”写在纸条上递过去,WASM 城凭编号直接进仓取货。桥还是那座桥,但大宗货物再也不需要真的过桥了。

WASM 的线性内存就是这座保税仓。它本质上是一块连续的 ArrayBuffer,JS 侧通过 TypedArray 创建“视窗”,可以直接读写 WASM 的堆内存,全程零拷贝。双方约定的“货位编号”就是内存偏移量(指针)加长度。

因此,传递大数据时要抛弃“传值”思维,改用“传址”思维:把数据写进线性内存,把指针和长度传给 WASM;计算完再让 WASM 把结果写回同一块内存,JS 回头直接读。唯一的代价是你要手动管理这块内存的读写时机,避免两边同时乱翻货架,还要在数据量增长时用 memory.grow 给仓库扩容。

有了共享保税仓,“搬运”这个词就从互操作的成本清单里消失了。剩下要操心的,只是仓库扩容的时机和存取纪律——用一点内存管理的麻烦,换取几十倍的传递开销下降,这笔生意很划算。

C复制=O(len)⟶C指针=O(1)\displaystyle C_{\text{复制}} = O(\text{len}) \qquad \longrightarrow \qquad C_{\text{指针}} = O(1)
复制成本随数据长度线性增长,而传指针的成本恒为常数——这就是“共享仓库”省钱的数学依据。
  1. 复制成本 \(C_{\text{复制}}\)传统方式要把整块数据复制过桥,成本与数据长度成正比。
    C复制=k×lenC_{\text{复制}} = k \times \text{len}
  2. 指针成本 \(C_{\text{指针}}\)共享内存模式下只传递一个整数偏移量,成本是常数。
    C指针=1 次整数传递C_{\text{指针}} = 1 \text{ 次整数传递}
  3. 对比结论len 越大,“传址”相对“传值”的收益越明显,这就是大数据必须走共享内存的原因。

数据序列化开销

保税仓没修好之前,人们只能把货物装箱过桥。标准包装箱叫 JSON:出发前,办事员要把货品的每个属性抄成清单(stringify);到了对岸,对方再按清单一件件重新组装(parse)。箱子越大,抄单子和拆箱子的时间就越长。

序列化是互操作里最容易被低估的开销。JSON 对每个数组元素都要做字符串转换和结构重建,复杂度是 O(n);对 10MB 级的浮点数组,光 stringify 就能吃掉几十毫秒,而 WASM 算同样的数据可能只要几毫秒。包装比干活还久,成了关桥上的奇观。

反观 TypedArray 直读:JS 拿到一块内存视图,数据就在那里,无需解析。这就是“零拷贝”的含义。工程上的判断标准很简单——数据是不是连续数值数组?是,就走内存视图;是复杂嵌套对象,才考虑 JSON。超过百 KB 的连续数组,永远优先共享内存。

老居民总结出一条经验:能放进保税仓按件取的货,千万别装箱。JSON 这只标准包装箱,只留给那些真正需要跨城传递、又没法共享的复杂物件。

TJSON=c×nvsT直读=c′  (c′≪c×n)\displaystyle T_{\text{JSON}} = c \times n \qquad \text{vs} \qquad T_{\text{直读}} = c' \;(c' \ll c \times n)
序列化时间随元素个数线性增长,而直读只需一次视图创建,几乎不随数据量变化。
  1. JSON 成本 \(T_{\text{JSON}}\)每个元素都要“装箱”(转字符串)再“拆箱”(解析还原)。
    TJSON=c×n,  c=t装箱+t拆箱T_{\text{JSON}} = c \times n,\; c = t_{\text{装箱}} + t_{\text{拆箱}}
  2. 直读成本 \(T_{\text{直读}}\)TypedArray 视图创建后直接指向线性内存,一次到位。
    T直读=常数T_{\text{直读}} = \text{常数}
  3. 阈值判断当 \(n\) 超过几百 KB 量级,JSON 的线性增长就会击穿性能预算。

批量调用优化

集装箱运输彻底改变了关桥的经济学。单个快递员一次只能带一件货,过桥费照收;集装箱车一次装几百件,过桥费还是一趟的钱。从此,人们过桥前养成了一个习惯:先攒货,再出发。

在代码里,这就是批量调用(batching):不再在循环里逐次调用 WASM 函数,而是一次传入整个数组或结构化缓冲区,让 WASM 在内部循环处理,最后再返回聚合结果。固定开销从 N 次摊薄到 1 次,单价立刻降下来。

实现要点包括:把入参组织成连续内存块(正好用上共享仓库);在 WASM 侧写成批量处理入口而非单元素入口;返回值也尽量聚合,比如一次返回整批结果的统计摘要。代价是增加少量参数组装逻辑和一次性的等待延迟——但对吞吐量而言,这几乎是稳赚不赔的交易。

攒够一集装箱再出发,听起来是在拖时间,实际上是把固定成本摊到每一件货上。对吞吐量而言,这是最划算的过桥姿势——前提是别攒得太久,否则延迟也会累积成新问题。

加速比=n×C过桥+n×t件C过桥+n×t件+C组装\displaystyle \text{加速比} = \frac{n \times C_{\text{过桥}} + n \times t_{\text{件}}}{C_{\text{过桥}} + n \times t_{\text{件}} + C_{\text{组装}}}
分子是逐个过桥的总成本,分母是合并后只付一次过桥费的成本;批量越大,加速比越接近 \(n\)。
  1. 分子逐个调用成本:n 件货每件都要付一次过桥费,还有每件的计算时间。
    n×C过桥+n×t件n \times C_{\text{过桥}} + n \times t_{\text{件}}
  2. 分母批量调用成本:只付一次过桥费,加上全部计算时间和少量组装开销。
    C过桥+n×t件+C组装C_{\text{过桥}} + n \times t_{\text{件}} + C_{\text{组装}}
  3. 加速比含义当 \(C_{\text{过桥}}\) 远大于 \(t_{\text{件}}\) 时,加速比逼近 \(n\),批量越大赚得越多。

回调频率管理

关桥是双向的。WASM 城的工厂每拧完一颗螺丝,就向 JS 城打一次国际长途汇报进度。刚开始大家觉得理所应当,直到话费账单寄来才发现:汇报的电话费比拧螺丝的工钱还贵。

WASM 回调 JS 同样是跨界调用,同样要付固定开销。逐像素、逐帧、逐元素地回调,会让主线程疲于奔命,掉帧卡顿随之而来。正确的姿势是把事件先记在工厂的黑板上,攒到一定的量,再按帧(requestAnimationFrame)或按时间窗口统一“汇报”一次。

工程实现通常是在 WASM 侧维护一个结果缓冲区,事件只写入缓冲区;JS 侧按 rAF 或定时器拉取并清空。这样事件数量不变,但跨界次数从每秒几千次降到每秒 60 次以内。对实时性要求高的场景,还可以设置节流阈值,让最紧急的事件插队上报。

汇报频率压到“够用就好”,话费自然降下来。这条规矩同样适用于反向过桥——边界两边的居民,都得学会少打电话、多写黑板,然后按点统一念。

f回调=N事件ΔT目标:f回调≤f预算\displaystyle f_{\text{回调}} = \frac{N_{\text{事件}}}{\Delta T} \qquad \text{目标:} f_{\text{回调}} \leq f_{\text{预算}}
回调频率等于事件数除以时间窗口;优化的本质是让实际频率降到性能预算以内。
  1. 事件数 \(N_{\text{事件}}\)WASM 侧产生的结果/进度事件总量,这个数字通常没法减少。
    N事件=任务数×每任务事件数N_{\text{事件}} = \text{任务数} \times \text{每任务事件数}
  2. 时间窗口 \(\Delta T\)统计频率的窗口,比如一帧(16.7ms)或一秒。
    ΔT=1f刷新\Delta T = \frac{1}{f_{\text{刷新}}}
  3. 预算上限 \(f_{\text{预算}}\)由 UI 流畅度决定的跨界频率上限,缓冲聚合就是要让实际频率压过这条线。

工具链方案对比

口岸上渐渐出现了三家物流公司。老牌国企 Emscripten,专营 C/C++ 货物,能力最强但手续繁琐;现代快递 wasm-bindgen,依托 Rust 的类型系统,自动打包、极少出错;轻骑兵 AssemblyScript,讲 TypeScript 方言,上手最快,适合小件急件。

选型本质是在比三家公司的“总账单”:开发成本、运行成本、维护成本。Emscripten 适合把现有 C/C++ 算法库搬上浏览器,但胶水代码粗放,很多内存管理要自己操心;wasm-bindgen 适合新写 Rust 项目,生成的胶水代码类型安全、转换高效,互操作体验最省心;AssemblyScript 适合团队只会 JS/TS 的中小型项目,语法亲切但生态相对年轻。

运行性能上,三者编译产物都能逼近原生,真正的差异在互操作体验:wasm-bindgen 的自动序列化和类型映射最省心,Emscripten 更依赖开发者手动规划共享内存,AssemblyScript 则用简洁语法换取较低的生态成熟度。选哪家,先看你的货是存量还是增量、团队会哪门语言、打算维护几年。

选物流公司就是选长期合作伙伴,别只看首单运费。先拿自家最痛的场景试运一票,账算清了再签约,免得上了船才发现换船更贵。

总成本=T开发+T运行+T维护\displaystyle \text{总成本} = T_{\text{开发}} + T_{\text{运行}} + T_{\text{维护}}
选工具链不能只看运行速度,开发效率与长期维护成本往往才是真正的分水岭。
  1. 开发成本 \(T_{\text{开发}}\)团队学习曲线、胶水代码编写、调试难度。
    T开发=t学习+t编码+t调试T_{\text{开发}} = t_{\text{学习}} + t_{\text{编码}} + t_{\text{调试}}
  2. 运行成本 \(T_{\text{运行}}\)编译产物体积、启动时间、互操作开销。
    T运行=t加载+N×C过桥T_{\text{运行}} = t_{\text{加载}} + N \times C_{\text{过桥}}
  3. 维护成本 \(T_{\text{维护}}\)依赖升级、语言生态活跃度、 bug 修复难度。
    T维护=t升级+t修bugT_{\text{维护}} = t_{\text{升级}} + t_{\text{修bug}}

性能测量与落地

关桥运营方在口岸设了一个绩效办公室。规矩简单:每个季度随机抽查通关记录,算清平均过桥时间;哪类货最耗时,就在哪类货上加盖“性能预算”章,超预算的运输方案一律打回重新设计。

性能测量与落地也是同样的流程。先用 performance.now、DevTools Profiler 或 wasm-opt 的基准工具,把互操作热点的耗时量化出来,比如“每秒跨界次数”“单次跨界微秒数”;再据此设定性能预算,例如“滤镜预览每帧跨界不超过 10 次”。

落地路径上,先做概念验证(POC):把性能最关键的一个模块移植到 WASM,测量收益与工程成本,用收益比决定是否全量推进;进入生产后,把基准测试挂到 CI 上,让任何一次改动导致的性能回退都在合并前现出原形。持续监控的意义在于,性能边界是动态的,代码每多一次跨界调用,账单都会悄悄变厚。

桥要不要扩建,从来不看广告,只看三个月的流量报表。有了数据,边界画在哪里,答案自己会浮现出来。

ROI=S速度提升C工程投入+C维护\displaystyle \text{ROI} = \frac{S_{\text{速度提升}}}{C_{\text{工程投入}} + C_{\text{维护}}}
收益比 = 速度提升 ÷(开发+维护成本)。只有这个比值明显大于 1,才值得把模块搬进 WASM。
  1. 速度提升 \(S_{\text{速度提升}}\)POC 实测的量化收益,比如耗时从 200ms 降到 20ms。
    S=TJS 原实现−TWASM 实现S = T_{\text{JS 原实现}} - T_{\text{WASM 实现}}
  2. 工程投入 \(C_{\text{工程投入}}\)移植、胶水代码、调试的人力成本。
    C工程投入=人日×单价C_{\text{工程投入}} = \text{人日} \times \text{单价}
  3. 维护成本 \(C_{\text{维护}}\)上线后的持续监控与 bug 修复成本。
    C维护=t监控+t修复×次数C_{\text{维护}} = t_{\text{监控}} + t_{\text{修复}} \times \text{次数}

AI 应用场景

浏览器端 AI 推理的图像预处理

阶段:推理前处理(数据管线)

解决的问题:AI 模型(如 ONNX Runtime Web)跑在 WASM 上,输入图像像素动辄几 MB。传统做法是把图像转成 JSON 或 JS 数组再传入,序列化耗时远超推理本身。借助共享线性内存 + TypedArray 直写像素,可以把预处理开销从 O(n) 降到 O(1)。

具体案例:一个实时人像分割的 PWA:摄像头帧 → JS 写入 WASM 线性内存 → WASM 推理 → 结果掩码写回同一内存 → JS 直接读取绘制。全程无 JSON、无拷贝,移动端保持 30 FPS。

实时数据大屏的批量聚合

阶段:流式数据处理

解决的问题:每秒上万条事件(如点击流、埋点日志)需要在 WASM 中做统计聚合,逐条跨界调用会让主线程卡死。批量调用把固定开销从 N 次摊到 1 次,事件处理吞吐提升一个量级。

具体案例:某电商大屏项目把事件先攒成数组,每秒一次批量调用 WASM 完成去重、排序、聚合;结果以缓冲区形式批量返回;进度回调按帧节流,只上报更新后的汇总指标,图表依旧顺滑。

音视频转码的帧交换

阶段:推理后处理 / 媒体处理

解决的问题:浏览器端音视频转码(如 WebCodecs + WASM 编解码器)每帧数据频繁跨界,拷贝与序列化是瓶颈。用 SharedArrayBuffer + 双缓冲轮流读写帧数据,可以把跨界次数降到每帧一两次。

具体案例:某在线剪辑工具用双缓冲机制:WASM 处理当前帧时,JS 写入下一帧;处理完只写一个标志位,主线程按帧读取,转码速度提升近 3 倍。

Python 演示

python34 行
"""Simulate the performance boundary between WASM and JS interop. A single cross-boundary call has a fixed overhead (engine switch,argument marshalling, stack switch). Batching amortizes this cost."""import math FIXED_PER_CALL = 50.0   # fixed cost of one cross-boundary call (us)PER_ITEM_WORK = 1.0     # real computation per data item (us)  def estimate(total_items, batch_size):    """Estimate total time (us) for processing total_items with given batch."""    calls = math.ceil(total_items / batch_size)    fixed_cost = calls * FIXED_PER_CALL    work_cost = total_items * PER_ITEM_WORK    return calls, fixed_cost, work_cost  def main():    total = 10000    print("items={}  per_item_work={}us  fixed_per_call={}us".format(        total, PER_ITEM_WORK, FIXED_PER_CALL))    print("{:>8} {:>10} {:>12} {:>12} {:>10}".format(        "batch", "calls", "fixed(us)", "total(us)", "fixed%"))    for batch in (1, 5, 25, 100, 500, 10000):        calls, fixed, work = estimate(total, batch)        total_cost = fixed + work        print("{:>8} {:>10} {:>12.1f} {:>12.1f} {:>10.1f}%".format(            batch, calls, fixed, total_cost, 100.0 * fixed / total_cost))  if __name__ == "__main__":    main()
目的:模拟“逐次跨界调用”与“批量调用”的总耗时差异,直观展示固定开销量级。
  1. FIXED_PER_CALL = 50.0定义单次跨界的固定开销,相当于关桥的“过桥费”
  2. calls = math.ceil(total_items / batch_size)计算跨界次数:批量越大,过桥趟数越少
  3. fixed_cost = calls * FIXED_PER_CALL固定开销总额 = 趟数 × 单趟费用
  4. work_cost = total_items * PER_ITEM_WORK真实计算量,与批量大小无关
  5. 100.0 * fixed / total_cost固定开销占比,占比越低说明桥费越不是瓶颈

常见误区

误区听说 WASM 接近原生性能,以为把所有业务逻辑都搬进 WASM 就能全面提速,结果小函数频繁调用反而更慢了,还搭进去大量重构时间。

正解WASM 快在“计算密集且循环内无跨界”的场景。互操作有固定开销,就像过桥费;任务太小、过桥太频,桥费会吞掉计算收益。正确姿势是先量化热点,只把最重的计算模块送过桥。

为何常见:宣传材料总强调 WASM 的计算速度,很少提跨界成本;小白自然只看到“快”字。

误区以为 JSON 是通用数据格式,把几 MB 的数组 stringify 后传给 WASM 最省事,反正两边都能解析。

正解JSON 序列化是 O(n) 且数值转字符串开销极大,10MB 数组光打包就要几十毫秒。连续数值数组应走 TypedArray 直读共享内存,零拷贝才是正道。

为何常见:JSON 在前后端通信里太常见,惯性思维让人忽略了浏览器内部可以用内存视图走近道。

误区遵循软件工程的高内聚低耦合,把 WASM 导出函数拆成一个个细粒度小函数,JS 侧灵活组合调用,多优雅。

正解每个导出函数都是一座收费桥。拆得越细,调用次数越多,固定开销越膨胀。跨界接口要粗粒度:一次调用完成一段完整计算,内部再自己拆解。

为何常见:传统代码评审鼓励小函数;但互操作边界有自己的经济规律——过桥费面前,粗粒度才是美德。

误区认为只有 JS 到 WASM 方向才贵,WASM 调 JS 是“内部调用”,随便回调没关系。

正解反向跨界同样要付引擎切换和参数包装的固定开销。逐事件回调会拖垮主线程;应在 WASM 侧缓冲、按帧或按窗口批量 flush,把跨界次数压到每秒几十次以内。

为何常见:直觉上把 WASM 当成“运行在 JS 里”,忘了它是另一个执行环境,每次跨环境都要“过桥”。

误区把热点模块搬进 WASM 后就再也不管边界了,新功能继续随意加跨界调用。

正解性能边界是动态的。代码演进会引入新的跨界热点,必须用基准测试 + CI 性能预算持续监控,防止互操作成本悄悄涨回来。

为何常见:优化完成后的“完工感”让人放松警惕,而性能退化总是渐进发生的,不测就看不见。

面试题

Q1. WASM 与 JS 互操作的主要开销来源是什么?

单选题 · easy

  • A.编译器优化能力弱
  • B.跨边界调用的固定开销← 正确答案
  • C.网络传输的往返延迟
  • D.浏览器渲染的大量阻塞

答案:B。互操作开销主要来自每次跨界调用时的引擎切换、参数校验和栈切换,这是一笔与任务轻重无关的固定开销。编译器优化、网络和渲染都发生在边界之外,不属于互操作成本。

Q2. 把一个大数组传给 WASM,最高效的方式是哪一种?

单选题 · easy

  • A.序列化为 JSON 字符串
  • B.逐元素拼接成文本串
  • C.共享内存上直读视图← 正确答案
  • D.转成 Base64 编码串

答案:C。连续数值数组应通过 TypedArray 直接读写 WASM 的线性内存,零拷贝、O(1) 成本。JSON、文本拼接、Base64 都属于序列化,都会有 O(n) 的编解码开销。

Q3. Rust 代码编译为 WASM 并生成 JS 胶水代码,应选择哪个工具链?

单选题 · easy

  • A.Emscripten 且配合 C
  • B.wasm-bindgen 胶水代码← 正确答案
  • C.Babel 转译 JavaScript
  • D.Vite 构建前端项目

答案:B。wasm-bindgen 是 Rust 生态的标准胶水代码生成工具,配合 wasm-pack 使用。Emscripten 面向 C/C++,Babel 做 JS 转译,Vite 是前端构建工具,都跟 Rust 编译 WASM 无关。

Q4. 关于 WASM 线性内存,下列说法正确的是?

单选题 · mid

  • A.与 JS 内存完全隔离
  • B.本质是 ArrayBuffer 内存← 正确答案
  • C.只能存储整数类型
  • D.存储在 GPU 显存中

答案:B。WASM 线性内存本质是一块连续 ArrayBuffer,JS 可以用 TypedArray 视图直接读写,这也是共享内存策略的基础。它并不在 GPU 上,也能存储浮点数,JS 完全可以通过视图访问它。

Q5. 批量调用 WASM 函数的最主要收益是什么?

单选题 · mid

  • A.让单次计算更快速
  • B.摊薄固定跨界开销← 正确答案
  • C.减少代码编写量
  • D.提升代码的可读性

答案:B。批量调用把固定跨界开销从 N 次摊薄到 1 次,这是收益核心。单次计算速度不会变,代码量甚至略有增加(参数组装),而可读性不是性能优化的目的。

Q6. 某任务单次计算耗时 2 微秒,跨界固定开销 50 微秒。1000 次独立小调用与一次批量调用的总耗时比约为?

单选题 · mid

  • A.约为 1 比 1
  • B.约为 26 比 1← 正确答案
  • C.约为 5 比 1
  • D.约为 100 比 1

答案:B。独立调用总耗时 = 1000 × (50 + 2) = 52000 微秒;批量调用总耗时 = 50 + 1000 × 2 = 2050 微秒;比值约 25.4,最接近 26 比 1。可见跨界固定开销主导时,批量收益巨大。

Q7. WASM 高频回调 JS 导致掉帧,最佳对策是什么?

单选题 · hard

  • A.在回调中打印日志排查
  • B.缓冲区聚合按帧刷新← 正确答案
  • C.删除全部外部回调
  • D.改用全局变量传递状态

答案:B。反向跨界同样有固定开销,正确做法是在 WASM 侧缓冲事件,按 requestAnimationFrame 或时间窗口批量 flush。打印日志只会加重负担,删除回调会丢功能,全局变量则绕不开线程模型约束。

Q8. 以下哪些手段可以有效降低 WASM 与 JS 的互操作成本?

多选题 · mid

  • A.批量合并小任务调用← 正确答案
  • B.共享内存直读大数据← 正确答案
  • C.回调聚合按帧推送← 正确答案
  • D.增加跨界函数数量

答案:ABC。批量调用摊薄固定开销、共享内存直读消除拷贝、回调聚合降低反向跨界频率,都是正确手段。增加跨界函数数量只会放大固定开销,是典型的反模式。

Q9. 关于用 JSON 在 JS 与 WASM 之间传数据,下列说法正确的有哪些?

多选题 · mid

  • A.需要字符串双向转换← 正确答案
  • B.数值转字符串有精度开销← 正确答案
  • C.大数据量下开销显著← 正确答案
  • D.是大数组传递最优方案

答案:ABC。JSON 传递需要 stringify 和 parse 双向转换,数值转字符串既慢又有精度风险,数据量越大开销越惊人。对连续大数组,最优方案是 TypedArray 共享内存直读,而非 JSON。

Q10. 哪些信号说明某个场景值得把模块移植到 WASM?

多选题 · hard

  • A.计算密集型且循环内无跨界← 正确答案
  • B.小数据高频调用场景
  • C.团队已有 C/Rust 代码资产← 正确答案
  • D.需要 JSON 高频传大数据

答案:AC。计算密集且循环内不跨界,WASM 才能发挥原生速度;已有 C/C++/Rust 资产,移植成本低、复用价值高。小数据高频调用会被固定开销反噬,JSON 高频传大数据则应先改共享内存方案,而不是指望 WASM 兜底。