作者: admin

  • “内存共享”与“Transferable Objects”

    在之前的优化工作中,我们将 x2t 移植到 WASM,并尝试在 JS 和 WASM 之间共享内存,以期减少数据拷贝,提升性能。然而,我们在实践中遇到了一些问题,特别是 **Segmentation Fault** 错误,这促使我们对 WASM 的内存模型以及 JS 和 WASM 共享内存的机制进行了更深入的研究。

    WASM 的线性内存模型

    WASM 实例拥有一个线性内存(Linear Memory),它本质上是一个 `ArrayBuffer`,可以被 WASM 代码和 JS 代码同时访问。 这个线性内存是 WASM 程序存储数据的主要场所。

    • **内存页(Memory Page):** WASM 的线性内存被划分为大小固定的内存页,每页通常为 64KB。
    • **内存增长:** WASM 程序可以通过 `memory.grow` 指令增加线性内存的大小,每次增加一页。线性内存的最大大小受限于 WebAssembly.Memory 实例的配置,通常为 4GB (65536 页)。
    • **地址空间:** WASM 代码通过线性地址(Linear Address)访问内存,线性地址是相对于线性内存起始位置的偏移量。

    JS 如何访问 WASM 内存

    JS 可以通过 `WebAssembly.Memory` 实例的 `buffer` 属性获取线性内存的 `ArrayBuffer` 对象。 拿到 `ArrayBuffer` 后,JS 就可以像操作普通数组一样读写 WASM 的内存了。

    const wasmMemory = new WebAssembly.Memory({ initial: 10 }); // 初始 10 页
    const buffer = wasmMemory.buffer; // 获取 ArrayBuffer
    const uint8Array = new Uint8Array(buffer); // 创建 TypedArray 视图
    
    // JS 写入数据到 WASM 内存
    uint8Array[0] = 42;
    
    // 从 WASM 内存读取数据
    const value = uint8Array[0];
    console.log(value); // 输出 42

    JS 与 WASM 共享内存的挑战

    虽然 JS 可以直接访问 WASM 的线性内存,但共享内存也带来了一些挑战:

    1. **内存对齐(Memory Alignment):**
      • 不同类型的数据需要按照特定的字节对齐。例如,32 位整数通常需要 4 字节对齐。
      • 如果 JS 和 WASM 以不同的方式访问未对齐的内存,可能会导致性能下降,甚至出现错误。
      • **解决方案:** 确保 JS 和 WASM 使用相同的内存对齐方式。可以使用 `DataView` 对象进行精确的内存访问。
    2. **数据类型转换(Data Type Conversion):**
      • JS 和 WASM 使用不同的数据类型表示方式。 例如,JS 的数字是 64 位浮点数,而 WASM 可以使用 32 位整数、64 位整数等多种类型。
      • 在 JS 和 WASM 之间传递数据时,需要进行类型转换。
      • **解决方案:** 使用 `TypedArray` (例如 `Int32Array`, `Float64Array`) 在 JS 中创建与 WASM 内存布局匹配的视图。
    3. **竞争条件(Race Conditions):**
      • 如果 JS 和 WASM 同时访问同一块内存,可能会出现竞争条件,导致数据不一致。
      • **解决方案:** 使用 `Atomics` API 进行同步。 `Atomics` API 提供了一些原子操作,例如原子加、原子减、原子比较和交换等,可以保证并发访问的安全性。
    4. **Segmentation Fault 的原因及解决:**
      • 我们遇到的 Segmentation Fault 错误,可能由以下原因导致:
        • **越界访问:** JS 或 WASM 访问了线性内存范围之外的地址。这可能是由于计算错误或内存增长未同步导致的。
        • **非法地址:** 访问了未分配或受保护的内存地址。
        • **数据竞争:** 尽管使用了 Atomics API,但在某些复杂的并发场景下,仍然可能存在竞争条件。
      • **解决方法:**
        • **仔细检查内存访问逻辑:** 确保 JS 和 WASM 的内存访问都在线性内存的有效范围内。
        • **使用调试工具:** 利用 WASM 调试工具 (例如 Chrome DevTools 的 WebAssembly Inspector) 跟踪内存访问,找出错误的根源。
        • **更细粒度的同步:** 考虑使用更细粒度的 Atomics 操作,或者采用其他的同步机制 (例如互斥锁) 来保护共享内存。
        • **避免复杂的数据结构共享:** 尽量避免在 JS 和 WASM 之间共享复杂的数据结构。如果必须共享,可以使用序列化/反序列化来传递数据。

    使用 Atomics API 进行同步的注意事项

    虽然 `Atomics` API 可以用来同步 JS 和 WASM 的内存访问,但也需要注意以下几点:

    • `Atomics.wait` 只能在 WebWorker 中调用,不能在主线程中使用。
    • `Atomics` 操作仍然有一定的性能开销,过度使用可能会影响性能。
    • 在复杂的并发场景下,需要仔细设计同步策略,避免死锁等问题。

    什么是 Transferable Objects?

    Transferable Objects 是一种特殊的对象,它们在传递时不会进行数据复制,而是直接将对象的底层内存的所有权从一个上下文转移到另一个上下文。 这意味着传递后,原始上下文将无法再访问该对象,而接收上下文则拥有该对象的完整控制权。

    常见的 Transferable Objects 包括:

    • `ArrayBuffer`
    • `MessagePort`
    • `ImageBitmap`
    • `OffscreenCanvas`

    **Transferable Objects 的优势:**

    1. **零拷贝(Zero-Copy):** 这是 Transferable Objects 最大的优势。 由于没有数据复制,传递速度非常快,尤其适合传递大型数据块。
    2. **避免内存竞争:** 由于所有权转移,只有一个上下文可以访问该对象,因此可以避免内存竞争和数据不一致的问题。 这简化了并发编程,无需使用复杂的同步机制。

    **Transferable Objects 的劣势:**

    1. **所有权转移:** 这是 Transferable Objects 的一个限制。 传递后,原始上下文将无法再访问该对象。 如果需要同时在多个上下文中使用同一份数据,Transferable Objects 就不适用了。
    2. **对象类型限制:** 只有特定类型的对象才能作为 Transferable Objects 传递。 常见的对象类型 (例如普通 JavaScript 对象) 不支持 Transferable Objects。
    3. **单向传递:** Transferable Objects 通常用于单向传递数据。 如果需要在两个上下文之间频繁地交换数据,效率可能不高。

    **Transferable Objects 方案与内存共享方案的对比:**

    特性Transferable Objects内存共享 (SharedArrayBuffer)
    数据拷贝零拷贝需要拷贝 (初始拷贝), 后续修改共享
    并发安全天然安全,无需同步机制需要使用 Atomics API 进行同步,增加了复杂性
    对象类型仅支持特定类型 (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas)支持任意类型,但需要序列化/反序列化
    使用场景适用于单向传递大型数据块的场景,例如:从 WebWorker 向主线程传递处理后的图像数据适用于多个线程/上下文需要同时访问和修改同一份数据的场景,例如:多人协作编辑文档
    复杂性简单复杂,需要处理内存对齐、数据类型转换、竞争条件等问题
    适用性适合 WASM X2T 的场景,X2T 在 WebWorker 中处理文件后,将 `Editor.bin` 数据通过 Transferable Objects 传递给主线程,主线程接收数据后,WebWorker 就不再需要这份数据了。这样可以避免数据拷贝,提高性能。不太适合 WASM X2T 的场景。X2T 的主要需求是将处理结果传递给主线程,而不是共享数据。如果使用内存共享,需要在 JS 和 WASM 之间进行复杂的同步,反而会增加复杂性。
    与 WASM 结合非常适合: WASM 在 WebWorker 中处理数据后,可以使用 Transferable Objects 将结果传递给主线程,无需任何数据拷贝。可以结合: JS 和 WASM 可以共享线性内存,但需要小心处理内存对齐、数据类型转换和竞争条件。
    WebWorker 支持良好良好

    **结论:**

    • Transferable Objects 是一种简单而高效的数据传递方式,适用于单向传递大型数据块的场景。
    • 内存共享 (SharedArrayBuffer) 适用于多个线程/上下文需要同时访问和修改同一份数据的场景,但实现起来更加复杂。

    **在 WASM X2T 场景中,Transferable Objects 可能是更好的选择。** 因为 X2T 的主要需求是将处理结果传递给主线程,而不是共享数据。 使用 Transferable Objects 可以避免数据拷贝,提高性能,并且简化并发编程的复杂性。

  • 怡氧文件打开速度优化:一次步步深入的探索

    作为一名关注用户体验的工程师,我一直对“怡氧”打开文件的速度有所关注。提升打开速度,改善用户体验,是我们一直在努力的方向。本文将分享我们团队在优化文件打开速度过程中的一些探索和尝试。

    问题分析:性能瓶颈在哪里?

    经过一番分析,我们发现文件打开的耗时主要集中在以下几个阶段:

    1. **JS SDK 加载:** 庞大的 `sdkjs` 文件在加载和解析时会消耗一定时间。
    2. **字体文件加载:** 渲染文字需要加载多个字体文件,部分字体文件体积较大,加载需要时间。
    3. **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 中!**

    这样做具有以下潜在优势:

    1. **提高文件解析效率:** WASM 具有接近原生代码的执行效率,可以提升 `x2t` 的解析速度。
    2. **增强安全性:** 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)
    64M11805.45134.99
    256M46676.62576.21
    512M94539.741264.35
    1G188446.262676.79

    此外,我们还测试了 `Transferable Objects` 的性能,结果表明,在 WebWorker 中使用 `Transferable Objects` 传递大文件,能够带来明显的性能提升。

    场景耗时 (ms)
    IPC1788
    WebWorker357.599
    WebWorkerTransfer145.4

    在数据传递方面,我们对比了内存共享和 Transferable Objects 两种方案。 内存共享需要复杂的同步机制,而 Transferable Objects 则提供了一种“所有权转移”的零拷贝传递方式。 考虑到 WASM X2T 的场景是将处理结果传递给主线程,而不是共享数据,我们最终选择了 Transferable Objects 方案。 这样可以避免数据拷贝,提高性能,并且简化并发编程的复杂性。详细说明请参考这篇文章

    方案选择:WebWorker + WASM X2T

    综合考虑测试结果,我们选择了以下方案:

    1. 在渲染进程中通过 WebWorker 执行 WASM X2T。
    2. 利用 Electron 的 `nodeIntegrationInWorker` 在 Worker 中使用 Node.js 的 `fs` 模块处理文件。
    3. 使用 Transferable Objects 在主线程和 WebWorker 之间传递数据。

    字体文件处理:自定义协议

    值得一提的是,目前版本的“怡氧”已经在使用 WASM 处理字体源文件了。加载方式是通过自定义协议,可以实现类似直接磁盘 IO 的效果。

    总结与展望

    目前,我们已经完成了文件打开速度优化方案的设计和技术验证。方案能否达到预期效果,还需要在实际开发完成后进行进一步测试。

    这次优化过程充满了挑战,我们也在不断探索和学习。在后续的开发中,我们也会继续关注性能,持续优化用户体验。

    如果您在文件处理优化方面有经验,欢迎在评论区分享您的见解!

  • 解决 Emoji 渲染难题:Unicode Range 与 JavaScript 的完美结合

    Emoji,这些生动形象的表情符号,已经成为我们日常沟通中不可或缺的一部分。然而,在某些特定的技术场景下,例如在自研的字符渲染引擎中,要正确渲染 Emoji 却并非易事。本文将深入探讨 Emoji 的 Unicode Range,以及如何在 JavaScript 中正确处理 Emoji 的 Unicode,最终实现 Emoji 的完美渲染。

    Emoji 与 Unicode Range

    Unicode 是一种国际标准字符集,它为世界上几乎所有的字符都分配了一个唯一的数字编码,也就是码点。Emoji 也不例外,它们被分配在 Unicode 的多个码点范围内,通常位于补充平面(Supplementary Multilingual Plane,SMP)中,这意味着它们的码点值大于 `0xFFFF`

    常见的 Emoji Unicode Range 包括:

    • **Emoticons (1F600–1F64F):** 包含各种面部表情,如笑脸、哭脸、惊讶等等。
    • **Symbols & Pictographs (1F300–1F5FF):** 包含各种符号和象形文字,如食物、交通工具、地点等等。
    • **Transport & Map Symbols (1F680–1F6FF):** 包含交通工具和地图相关的符号。
    • **Miscellaneous Symbols and Pictographs (1F900–1F9FF):** 包含各种杂项符号和象形文字。

    由于 Emoji 的码点值可能超出基本多文种平面(Basic Multilingual Plane,BMP),因此传统的字符处理方法可能会遇到问题。

    两种渲染模式下的 Emoji 挑战

    在我们的场景中,存在两种字符渲染模式:

    1. **基于字体文件的字符渲染:** 这种模式依赖于字体文件来渲染字符,包括 Emoji。如果缺少 Emoji 字体文件,则无法渲染 Emoji。即使提供了 Emoji 字体,如何正确识别和处理 Emoji 的 Unicode 也是一个挑战。
    2. **极速渲染模式(Canvas.fillText):** 这种模式直接使用 Canvas 的 `fillText` 方法来绘制字符。在这种模式下,只要能正确处理字符的 Unicode,就可以相对简单地实现 Emoji 渲染。

    无论哪种模式,正确处理 Emoji 的 Unicode 都是关键。

    传统方法的问题:charCodeAt 和 fromCharCode 的局限性

    在过去的代码中,我们使用 `String.charCodeAt` 方法和 `String.fromCharCode` 方法来处理 Unicode 和还原字符。然而,这两种方法存在一个很大的局限性:它们只能处理码点值在 `0x0000``0xFFFF` 之间的字符,无法正确处理 Emoji 的 Unicode。

    例如,对于一个码点值为 `0x1F600` 的 Emoji,`charCodeAt` 方法只能返回 `0xD83D``fromCharCode` 方法也只能处理小于 `0xFFFF` 的值。这导致 Emoji 无法正确识别和渲染。

    解决方案:拥抱 codePointAt 和 fromCodePoint

    为了解决这个问题,我们需要使用 `String.codePointAt` 方法和 `String.fromCodePoint` 方法来替代 `String.charCodeAt` 方法和 `String.fromCharCode` 方法。

    • **`String.codePointAt(index)`:** 返回字符串中指定索引位置字符的 Unicode 码点值。即使字符的码点值大于 `0xFFFF`,也能正确返回。
    • **`String.fromCodePoint(codePoint)`:** 使用指定的 Unicode 码点值创建一个字符串。

    通过使用这两个方法,我们可以正确地获取和还原 Emoji 的 Unicode 码点值,从而为 Emoji 的正确渲染奠定基础。

    // 获取 Emoji 的 Unicode 码点
    const emoji = '😀';
    const codePoint = emoji.codePointAt(0); // 128512 (0x1F600)
    
    // 使用 Unicode 码点还原 Emoji
    const restoredEmoji = String.fromCodePoint(codePoint); // "😀"

    字符遍历与字形测量

    在使用 `codePointAt``fromCodePoint` 后,我们还需要注意一个问题:由于 Emoji 的码点值可能大于 `0xFFFF`,这意味着一个 Emoji 字符可能占用两个 JavaScript 字符的位置(UTF-16 编码)。

    因此,在之前逐个字符进行测量和填充的逻辑中,我们需要识别一个字符是否占用两个单位的字符,然后进行相应的处理。

    例如,我们需要判断 `codePointAt` 返回的码点值是否大于 `0xFFFF`,如果大于,则需要跳过下一个字符,因为它实际上是当前 Emoji 的一部分。

    const text = 'Hello 😀 World';
    for (let i = 0; i < text.length; i++) {
      const codePoint = text.codePointAt(i);
      console.log(`Character at index ${i}: ${String.fromCodePoint(codePoint)}, Code Point: ${codePoint}`);
    
      if (codePoint > 0xFFFF) {
        i++; // 跳过下一个字符
      }
    
      // 进行字符测量和填充
      // ...
    }

    Combined Emoji 的挑战

    除了单个 Emoji 之外,还存在一种叫做 Combined Emoji 的特殊类型。这些 Emoji 由多个 Unicode 字符组合而成,例如肤色修饰符、性别符号等等。

    Combined Emoji 的渲染是一个更加复杂的问题,因为我们需要正确地识别和组合这些字符,才能渲染出正确的 Emoji。这可能需要对整个渲染逻辑进行重构,将字符串作为一个整体进行测量和渲染,而不是逐个字符进行处理。

    然而,如何正确地分割字符串,以识别 Combined Emoji 的边界,又是一个非常棘手的问题。这可能需要借助复杂的 Unicode 规范和正则表达式来实现。

    总结与展望

    通过使用 `String.codePointAt``String.fromCodePoint` 方法,我们可以有效地解决 Emoji 的 Unicode 处理问题,为 Emoji 的正确渲染奠定基础。然而,Combined Emoji 的渲染仍然是一个挑战,需要进一步的研究和实践。

    未来,我们可以考虑以下方向:

    • **Unicode 规范的深入研究:** 更加深入地了解 Unicode 规范,特别是关于 Emoji 和 Combined Emoji 的部分。
    • **正则表达式的应用:** 使用正则表达式来识别和分割 Combined Emoji。
    • **渲染引擎的重构:** 将字符串作为一个整体进行测量和渲染,而不是逐个字符进行处理。

    希望本文能够帮助你更好地理解 Emoji 的 Unicode Range,以及如何在 JavaScript 中正确处理 Emoji 的 Unicode,最终实现 Emoji 的完美渲染。

  • 怡氧 字体 Fallback 机制:从乱码到优雅显示

    在日常办公中,我们经常需要在不同的设备、不同的系统中打开文档。为了保证文档在各种环境下都能呈现出作者希望的效果,字体扮演着至关重要的角色。然而,在线文档预览场景下,字体渲染却面临着诸多挑战。今天,就让我们一起深入探讨 怡氧 在字体 fallback 机制上所做的努力,以及我们如何在此基础上进行优化,让在线文档预览更加完美。

    在线字体渲染的难题

    在线文档预览,与本地应用最大的不同在于,我们无法直接访问用户的本地字体文件。这导致了两个核心问题:

    1. **字体资源有限:** 我们无法像本地应用一样,依赖用户系统安装的字体。为了显示效果,我们需要从服务器下载字体文件。
    2. **字体版权限制:** 商业字体通常需要授权才能使用,我们不能直接将这些付费字体放在服务器上供用户下载。因此,我们只能选择使用免费字体。

    这两个难题使得在线文档的字体 fallback 变得尤为重要。如果用户设置的字体在我们的字体列表中不存在,就需要找到一个合适的替代字体。如果 fallback 逻辑不佳,就会导致文档显示错乱,严重影响用户体验。

    怡氧 的字体 fallback 机制:一个精细的“寻字”之旅

    怡氧 在这方面做出了很多努力。他们并没有简单粗暴地选择一个默认字体,而是建立了一套复杂的 fallback 机制,其核心思路可以概括为“惩罚值计算”+“Unicode 索引”。

    1. 字体字典:信息完备的“字体地图”

    首先,怡氧 维护了一个庞大的字体字典,其中包含了:

    • **字体名称:** 例如:Arial、Times New Roman 等。
    • **Unicode 范围:** 描述该字体支持的 Unicode 字符范围。
    • **字体文件大小:** 用于评估下载成本。

    这个字体字典相当于一个详细的“字体地图”,帮助 怡氧 快速定位可用的字体资源。

    2. 惩罚值计算:优中选优的替代策略

    当用户设置的字体不在字典中时,怡氧 会根据字典中的信息,计算每个候选字体的“惩罚值”。这个惩罚值综合考虑了以下因素:

    • **字体名称的相似度:** 例如,如果用户设置的字体名为 “Arial Bold”,那么字典中名为 “Arial” 的字体的惩罚值会相对较低。
    • **Unicode 范围的匹配程度:** 如果文档中包含大量中文字符,那么支持中文的字体的惩罚值会相对较低。
    • **字体文件大小:** 体积较小的字体,下载速度更快,因此惩罚值会相对较低。

    最终,怡氧 会选择惩罚值最小的字体作为 fallback 字体,并将其下载到本地。这样可以尽量保证文档的显示效果与用户的期望接近。

    3. Unicode 索引:最后的“救命稻草”

    然而,即便通过惩罚值计算选择了 fallback 字体,也可能出现目标字体没有用户文档中特定字符的情况。这时,怡氧 会启用最后的 fallback 机制:Unicode 索引。

    怡氧 事先会生成一个 Unicode 索引,将整个 Unicode 空间划分成小段,每一小段都对应到一个包含了此段的字体,这个字体是锁支持的列表中文件大小最小的一个。这可以确保任何字符都能在字体集中找到对应的字形,从而最大程度地避免显示为“方块”的乱码情况。

    我们的优化:针对中文的“特殊关照”

    怡氧 的字体 fallback 机制已经相当完善,但在中文显示方面,仍然存在一定的优化空间。考虑到中文字体的复杂性和多样性,我们基于 怡氧 的机制进行了改进:

    1. **引入开源中文字体:** 我们增加了思源黑体、思源宋体等高质量的开源中文字体,这些字体覆盖的 Unicode 范围更广,能够更好地显示中文内容。
    2. **硬编码常用字体 fallback:** 我们硬编码了一些常用的中文字体,例如微软雅黑、宋体、黑体、平方等,固定 fallback 到思源黑体或者思源宋体。这样,当用户设置这些常见字体时,可以确保 fallback 到最适合的中文字体。

    通过这些优化,我们在很大程度上解决了中文文档乱码的问题,让中文文档的显示更加准确和美观。

    总结

    字体 fallback 是一个复杂而又至关重要的技术环节,它直接影响着用户体验。怡氧 通过其精密的字体字典、惩罚值计算和 Unicode 索引,构建了一套健壮的 fallback 机制。而我们在此基础上,针对中文进行了优化,进一步提升了在线文档预览的体验。

    当然,字体显示永远是一个持续优化的过程。随着技术的发展,我们也将不断探索更好的解决方案,让在线文档的字体显示更加完美。

    希望这篇文章能帮助你更好地理解 怡氧 的字体 fallback 机制。如果你有任何问题或者建议,欢迎在评论区留言讨论。

  • 怡氧文档极速加载背后的秘密:文字渲染引擎的优化之路

    大家好,我是一位在代码世界里摸爬滚打多年的程序员。今天想和大家聊聊 怡氧 文档加载速度优化背后的故事,特别是我们如何对文字渲染引擎进行大刀阔斧的改进,从而实现“秒开文档”的体验。

    传统渲染:字体加载的沉重负担

    我们都知道,怡氧 作为一个功能强大的 Office 套件,它需要处理各种复杂的文档。而文档的呈现,文字是其中最重要的组成部分。在 怡氧 早期版本中,我们的文字渲染引擎依赖于强大的 [FreeType](https://freetype.org/) 库。

    [FreeType](https://freetype.org/) 的工作原理大致是这样的:

    1. **字体加载**:当打开一个文档时,它会解析文档中使用的字体信息,并尝试加载相应的字体文件。字体文件少则几 MB,多则几十 MB,这无疑给文档的加载带来了不小的负担,尤其是在网络状况不佳或者设备性能有限的情况下,等待字体加载完成会让人感到不耐烦。
    2. **字形获取**:加载字体后,[FreeType](https://freetype.org/) 会根据 Unicode 编码查询字体文件中字符对应的字形信息(也就是我们看到的文字形状的轮廓),并计算出渲染所需的数据。
    3. **画布绘制**:最后,结合文字样式和字号等信息,通过 `canvas.drawImage` API 将字形渲染到页面上。

    这种模式的优点是渲染效果精确,能完美呈现字体设计师的意图,但缺点也显而易见:**字体加载耗时长,是影响文档打开速度的主要瓶颈之一**

    另辟蹊径:HTML Canvas 的崛起

    为了解决这个问题,我们开始深入研究 HTML5 canvas 的能力。我们发现 `canvas.measureText` API 可以获取某个字体中字符的文本度量信息(TextMetrics),包括字符宽度、高度、基线偏移等。**这为我们提供了一个完全不依赖字体文件,就能渲染文字的全新思路**

    我们的优化方案是:

    1. **极速模式**:我们引入了“极速模式”。在这种模式下,我们不再加载字体文件,而是利用 `canvas.measureText` API 获取字符的文本度量信息,然后使用 `canvas.fillText` API 将文字直接渲染到页面上。这种方法省去了加载字体文件的大量时间,显著提升了文档的打开速度。**我们可以把这个过程想象成这样:** 以前我们需要从书架上找到一本书(字体文件),然后翻到对应的页码(字形信息),再根据书上的内容在纸上画出文字。现在我们只需要知道每个字的“占位”大小(TextMetrics),就可以直接在纸上写字,是不是快多了?
    2. **精确模式**:为了兼容一些特殊情况,我们保留了传统的“精确模式”。当文档中包含艺术字等需要通过字形图才能实现的效果时,我们会自动切换到“精确模式”。同时,如果用户在“极速模式”下插入了艺术字,也会自动切换回“精确模式”,保证所有内容都能正确显示。

    **两种模式,相辅相成,平衡了速度与精度。**

    优化带来的显著效果

    经过这样的改造,文档的打开速度得到了质的飞跃,尤其是在网络状况不佳或者设备性能有限的情况下,提升效果更加明显。用户再也不用长时间等待文档加载了,可以立即开始阅读和编辑。

    总结与展望

    这次文字渲染引擎的优化,是我们不断追求卓越、精益求精的一个缩影。我们相信,通过持续的技术创新,我们可以为用户带来更好的产品体验。未来,我们还会继续探索新的技术,进一步提升文档处理效率,让 怡氧 成为您工作学习的最佳伙伴。

    希望今天的分享对你有所启发。如果你对 怡氧 技术细节感兴趣,欢迎关注我的博客,我们下期再见!

  • 怡氧 Online

    秒级集成的强大办公套件