99%之谜:为什么我的 ffmpeg.wasm 应用在终点线卡住了
Source: Dev.to

我一直在构建 VideoSnap,这是一款使用 ffmpeg.wasm 在浏览器中完整处理视频的工具。长期以来,我一直被一个特定且令人沮丧的 bug 纠缠:所谓的“99 % 陷阱”。
用户上传文件后,进度条平稳上升,达到 99 %……随后一切就停住了。UI 变得无响应,风扇转个不停,几分钟后下载才出现,好像什么都没发生过。
99 % 不是 FFmpeg——而是交接
在 ffmpeg.wasm 中,进度条跟踪 FFmpeg 的执行。当进度达到 99 % 时,转码的繁重工作实际上已经完成。
“卡住”发生在交接阶段:当你调用 engine.readFile() 将处理后的视频从 WebAssembly 虚拟内存(MEMFS)拉取到 JavaScript 堆时。
“内存重叠”问题
- 峰值: 在 99 % 时,WASM 内存同时保存你的 500 MB 输入文件 以及 新生成的 500 MB 输出文件 → 1 GB 的 WASM 内存被占用。
- 请求: 你调用
engine.readFile()。JavaScript 现在尝试分配一个 新的 500 MBUint8Array来复制该数据。 - GC 风暴: 浏览器现在要管理接近 1.5 GB–2 GB 的大块连续内存。这会触发一次 “Stop‑The‑World” 垃圾回收事件。主线程在引擎尝试碎片整理内存以寻找 500 MB 空洞时被锁住。你看到的 UI 卡顿正是这种 GC 抖动的表现。
“手术式”修复:打破重叠
一旦我明白卡顿是由 MEMFS 中输入文件和输出文件 simultaneous existence 引起的,解决办法就显而易见:在搬动大箱子之前先清理桌面。
// Optimized handover logic
// 1. FFmpeg is done. Before reading the output, delete the input file
await engine.deleteFile('input.mp4');
// 2. Now the WASM memory has breathing room; read the result
const data = await engine.readFile('output.mp4');
// 3. Immediately delete the WASM copy of the output
await engine.deleteFile('output.mp4');
// 4. Create a Blob from the JS buffer
const blob = new Blob([data.buffer], { type: 'video/mp4' });
通过重新排列这些删除操作,我消除了浏览器在最需要内存的关键时刻出现的大规模内存重叠。99 % 的卡顿并不会凭空消失——浏览器仍然需要时间来分配大型 JS 缓冲区——但此清理削减了关键的几秒 GC 抖动,并防止标签页在处理大文件时窒息。
Why I Didn’t Use WORKERFS or OPFS
- WORKERFS: 挂载文件而不复制它们,听起来很完美,但它使用同步 I/O 桥,使 FFmpeg 运行显著变慢。我用内存换取了巨大的速度惩罚,这不值得。
- OPFS (Origin Private File System): 将数据直接流式写入磁盘,是未来的方向,但它需要一个自定义构建的带有
WASMFS支持的 FFmpeg 核心——官方的@ffmpeg/ffmpeg包并未开箱即用提供。
要点:了解你的交接
在构建高性能 WebAssembly 应用时,请记住:管道中最危险的环节是数据交接。
在 WASM “世界”和 JS “世界”之间移动大量数据会迫使浏览器分配巨大的连续内存块。如果在发起请求之前不先清理内部状态,就会招致一次 GC 风暴。
VideoSnap 现在显著更稳定,并不是因为算法更快,而是因为内存生命周期被精确管理。
我是 VideoSnap 的创建者,专注于写下在浏览器中构建高性能工具的乱象与真实经验。关注我,获取更多深度解析。