作为一名关注用户体验的工程师,我一直对“怡氧”打开文件的速度有所关注。提升打开速度,改善用户体验,是我们一直在努力的方向。本文将分享我们团队在优化文件打开速度过程中的一些探索和尝试。
问题分析:性能瓶颈在哪里?
经过一番分析,我们发现文件打开的耗时主要集中在以下几个阶段:
- **JS SDK 加载:** 庞大的
`sdkjs`文件在加载和解析时会消耗一定时间。 - **字体文件加载:** 渲染文字需要加载多个字体文件,部分字体文件体积较大,加载需要时间。
- **X2T 解析:** 调用
`x2t`将文件解析为`Editor.bin`,再将文件加载到 JS 环境中。
其中,`x2t` 是一个关键环节,涉及到文件格式转换,是文档打开流程中不可或缺的一步。
方案探索:各种尝试与思考
针对以上瓶颈,我们团队集思广益,尝试了一些优化方案,其中一些方案也带来了一些新的思考。
- **字体文件缓存 + Memmap?**我们曾考虑过缓存字体文件,然后使用
`memmap`直接加载。`memmap`可以将文件映射到内存,减少重复读取。为了实现这个方案,我们还研究了 Electron 的`nodeIntegration`API,尝试在渲染线程调用 Node.js 环境。
https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions nodeIntegration
测试用的 npm 库 https://www.npmjs.com/package/mmap-io 然而,在实际尝试中,我们发现 `memmap` 存在一些问题:Page Fault Remap 时可能会阻塞渲染线程,导致界面出现卡顿。考虑到 Electron 的版本升级,以及 V8 引擎对外部内存访问的限制,我们最终放弃了这个方案。 - **V8 Heap Snapshot?**我们也曾考虑过利用 V8 Heap Snapshot 加速加载。但很快我们意识到,Heap Snapshot 只是内存快照,本质上是数据文件,并不能直接用于代码执行。JavaScript 代码仍然需要经过解析和编译才能执行,这个过程无法省略。
Heap Snapshot 不是可执行代码: Heap Snapshot 是 V8 引擎内存状态的快照,它本质上是一个数据文件,包含了对象、类型、引用关系等信息。它不是可以直接执行的 JavaScript 代码或者编译后的机器码。编译是必须的: JavaScript 引擎必须先将 JavaScript 代码解析成抽象语法树 (AST),然后再将 AST 编译成机器码才能执行。 这个编译过程是不可避免的。 Heap Snapshot 只能在代码执行后生成。
因此,这个方案也未能达到预期的效果。
新思路:WASM 的应用
在探索过程中,我们逐渐将目光转向 WASM:**将 `x2t` 运行在 WASM 中!**
这样做具有以下潜在优势:
- **提高文件解析效率:** WASM 具有接近原生代码的执行效率,可以提升
`x2t`的解析速度。 - **增强安全性:** WASM 运行在沙箱环境中,可以降低恶意文件对后端造成影响的风险。
令人欣喜的是,我们发现 GitHub 上已经有开发者完成了将 `x2t` 编译为 WASM 的工作:https://github.com/cryptpad/onlyoffice-x2t-wasm。
借助 WASM,我们可以将原来通过 Node.js `child_process.spawn` 调用可执行程序的过程转移到渲染线程。并且,我们还可以考虑直接通过内存将 `Editor.bin` 内容传递给渲染进程,减少文件读写的步骤。
技术调研:JS 与 WASM 的数据交互
确定 WASM 的方向后,我们需要解决一个重要问题:如何在 JS 和 WASM 之间高效地交换数据?
我们向 AI 助手咨询了 JS 和 WASM 数据交换的多种方式,得到了以下方案:
// 1. Direct numeric parameter passing (fastest, but limited to numbers)
// 2. Linear Memory Access (high performance for bulk data)
// 3. SharedArrayBuffer (zero-copy, but requires cross-origin isolation)
// 4. Structured Data Exchange (for complex objects)
// 5. Transferable Objects (e.g., ArrayBuffer)
// 6. WebAssembly.Table for function references
// 7. Streaming SIMD Data
// 8. Asyncify for Async Operations
// 9. Web Workers with SharedArrayBuffer
// 10. Atomics for Synchronization
为了找到适合我们的方案,我们进行了性能测试。
性能测试:数据驱动决策
我们利用工具辅助,编写了一系列测试程序,针对浏览器相关 API (WASM Shared Memory, Atomics, SharedArrayBuffer) 进行了较为详细的性能测试。
测试结果显示:
- 在 JS 渲染线程和 WebWorker 中,性能差异不明显,但大块数据的处理可能会阻塞 UI。
- WASM 需要使用
`WebAssembly.Memory`实例化对象,`SharedArrayBuffer`对象不能直接传递给 WASM 使用。 - 不使用
`WebAssembly.Memory`实例化 Memory 对象,而是直接通过 WASM 实例化的对象访问 WASM 中的内存是可行的。 `WebAssembly.Memory`存在内存大小限制。- JS 和 WASM 两个交替操作相同位置的内存时,可能会出现问题,需要用 Atomics API 进行同步。但在某些情况下,即便使用了 Atomics API,WASM 读写数据仍然可能出现问题。
我们还对 WASM 读写文件的性能进行了测试和优化,并取得了一定的进展:
| 数据量 | 优化前 (ms) | 优化后 (ms) |
|---|---|---|
| 64M | 11805.45 | 134.99 |
| 256M | 46676.62 | 576.21 |
| 512M | 94539.74 | 1264.35 |
| 1G | 188446.26 | 2676.79 |
此外,我们还测试了 `Transferable Objects` 的性能,结果表明,在 WebWorker 中使用 `Transferable Objects` 传递大文件,能够带来明显的性能提升。
| 场景 | 耗时 (ms) |
|---|---|
| IPC | 1788 |
| WebWorker | 357.599 |
| WebWorkerTransfer | 145.4 |
在数据传递方面,我们对比了内存共享和 Transferable Objects 两种方案。 内存共享需要复杂的同步机制,而 Transferable Objects 则提供了一种“所有权转移”的零拷贝传递方式。 考虑到 WASM X2T 的场景是将处理结果传递给主线程,而不是共享数据,我们最终选择了 Transferable Objects 方案。 这样可以避免数据拷贝,提高性能,并且简化并发编程的复杂性。详细说明请参考这篇文章。
方案选择:WebWorker + WASM X2T
综合考虑测试结果,我们选择了以下方案:
- 在渲染进程中通过 WebWorker 执行 WASM X2T。
- 利用 Electron 的
`nodeIntegrationInWorker`在 Worker 中使用 Node.js 的`fs`模块处理文件。 - 使用 Transferable Objects 在主线程和 WebWorker 之间传递数据。
字体文件处理:自定义协议
值得一提的是,目前版本的“怡氧”已经在使用 WASM 处理字体源文件了。加载方式是通过自定义协议,可以实现类似直接磁盘 IO 的效果。
总结与展望
目前,我们已经完成了文件打开速度优化方案的设计和技术验证。方案能否达到预期效果,还需要在实际开发完成后进行进一步测试。
这次优化过程充满了挑战,我们也在不断探索和学习。在后续的开发中,我们也会继续关注性能,持续优化用户体验。
如果您在文件处理优化方面有经验,欢迎在评论区分享您的见解!
发表回复