Author: admin

  • EO2Suite File Open Speed Optimization: A Step-by-Step Exploration

    As an engineer focused on user experience, I’ve always been attentive to the speed at which O-xygen opens files. Improving open speed and enhancing user experience are continuous goals for us. This article will share some of the explorations and attempts our team made in optimizing file open speed.

    Problem Analysis: Where are the Performance Bottlenecks?

    After some analysis, we identified that the time-consuming aspects of opening files mainly concentrate in the following stages:

    1. **JS SDK Loading:** Loading and parsing the large `sdkjs` file consumes a certain amount of time.
    2. **Font File Loading:** Rendering text requires loading multiple font files, and some of these files are large in size, taking time to load.
    3. **X2T Parsing:** Invoking `x2t` parses the file into `Editor.bin`, which is then loaded into the JS environment.

    Among these, `x2t` is a key step involving file format conversion, and it’s an indispensable part of the document opening process.

    Solution Exploration: Various Attempts and Considerations

    To address these bottlenecks, our team brainstormed and tried several optimization solutions, some of which also brought new insights.

    • **Font File Caching + Memmap?**We had considered caching font files and then using `memmap` to load them directly. `memmap` can map files into memory, reducing redundant reads. To implement this solution, we also studied Electron’s `nodeIntegration` API, trying to invoke the Node.js environment in the rendering thread.
      https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions nodeIntegration
      Testing npm library https://www.npmjs.com/package/mmap-io
      However, in practical attempts, we found that `memmap` had some issues: Page Fault Remap might block the rendering thread, causing UI stuttering. Additionally, considering Electron version upgrades and V8 engine restrictions on external memory access, we ultimately abandoned this solution.
    • **V8 Heap Snapshot?**We also considered using V8 Heap Snapshot to accelerate loading. However, we quickly realized that a Heap Snapshot is just a memory snapshot; essentially a data file, it cannot be directly used for code execution. JavaScript code still needs to be parsed and compiled before it can be executed, and this process cannot be omitted.
      Heap Snapshot is not executable code: A Heap Snapshot is a snapshot of the V8 engine's memory state; It is essentially a data file containing information such as objects, types, and references. It's not directly executable JavaScript code or compiled machine code. Compilation is necessary: The JavaScript engine must first parse JavaScript code into an Abstract Syntax Tree (AST), and then compile the AST into machine code before it can be executed. This compilation process is unavoidable. A Heap Snapshot can only be generated after code execution.
      Therefore, this solution also failed to achieve the desired effect.

    New Ideas: The Application of WASM

    During the exploration, we gradually turned our attention to WASM: **Running `x2t` in WASM!**

    Doing so has the following potential advantages:

    1. **Improve File Parsing Efficiency:** WASM has execution efficiency close to native code, which can improve the parsing speed of `x2t`.
    2. **Enhance Security:** WASM runs in a sandbox environment, which can reduce the risk of malicious files affecting the backend.

    Excitingly, we found that a developer on GitHub had already completed the task of compiling `x2t` to WASM: https://github.com/cryptpad/onlyoffice-x2t-wasm.

    With the help of WASM, we can move the process of calling the executable program via Node.js `child_process.spawn` to the rendering thread. Furthermore, we can consider passing the `Editor.bin` content directly through memory to the rendering process, reducing file read/write steps.

    Technical Research: JS and WASM Data Interaction

    After determining the direction of WASM, we need to solve an important problem: how to efficiently exchange data between JS and WASM?

    We consulted AI assistants about various methods of JS and WASM data exchange and obtained the following solutions:

    // 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

    To find a suitable solution for us, we conducted performance testing.

    Performance Testing: Data-Driven Decisions

    With the assistance of tools, we wrote a series of test programs and conducted relatively detailed performance tests on browser-related APIs (WASM Shared Memory, Atomics, SharedArrayBuffer).

    The test results showed:

    • In the JS rendering thread and WebWorker, the performance difference is not significant, but processing large chunks of data may block the UI.
    • WASM requires the use of `WebAssembly.Memory` to instantiate objects, and `SharedArrayBuffer` objects cannot be directly passed to WASM for use.
    • It is feasible not to use `WebAssembly.Memory` to instantiate Memory objects, but to access the memory in WASM directly through WASM-instantiated objects.
    • `WebAssembly.Memory` has memory size limitations.
    • When JS and WASM alternately operate on the same memory location, problems may occur, and the Atomics API needs to be used for synchronization. However, even with the Atomics API, problems may still occur when WASM reads and writes data in some cases.

    We also tested and optimized the performance of WASM reading and writing files and achieved some progress:

    Data SizeBefore Optimization (ms)After Optimization (ms)
    64M11805.45134.99
    256M46676.62576.21
    512M94539.741264.35
    1G188446.262676.79

    In addition, we also tested the performance of `Transferable Objects`. The results show that using `Transferable Objects` to transfer large files in WebWorker can bring significant performance improvements.

    ScenarioTime Consumed (ms)
    IPC1788
    WebWorker357.599
    WebWorkerTransfer145.4

    Regarding data transfer, we compared memory sharing and Transferable Objects. Memory sharing requires complex synchronization mechanisms, while Transferable Objects provide a zero-copy transfer method that “transfers ownership.” Considering that the WASM X2T scenario is to transfer the processing results to the main thread, rather than sharing data, we ultimately chose the Transferable Objects solution. This avoids data copying, improves performance, and simplifies the complexity of concurrent programming. For detailed information, please refer to this article.

    Solution Selection: WebWorker + WASM X2T

    Considering the test results comprehensively, we chose the following solution:

    1. Execute WASM X2T in the rendering process through WebWorker.
    2. Use Electron’s `nodeIntegrationInWorker` to process files using Node.js’s `fs` module in the Worker.
    3. Use Transferable Objects to transfer data between the main thread and the WebWorker.

    Font File Processing: Custom Protocol

    It is worth mentioning that the current version of O-xygen already uses WASM to process font source files. The loading method uses a custom protocol, which can achieve an effect similar to direct disk IO.

    Summary and Outlook

    Currently, we have completed the design and technical verification of the file open speed optimization solution. Whether the solution can achieve the desired effect requires further testing after actual development is completed.

    This optimization process has been full of challenges, and we are constantly exploring and learning. In subsequent development, we will continue to focus on performance and continuously optimize the user experience.

    If you have experience in file processing optimization, you are welcome to share your insights in the comments!

  • “Memory Share” & “Transferable Objects”

    In our previous optimization efforts, we ported x2t to WASM and attempted to share memory between JS and WASM, hoping to reduce data copying and improve performance. However, we encountered some problems in practice, especially **Segmentation Fault** errors, which prompted us to conduct a more in-depth study of WASM’s memory model and the mechanisms of JS and WASM shared memory.

    WASM’s Linear Memory Model

    A WASM instance has a linear memory, which is essentially an `ArrayBuffer` that can be accessed by both WASM code and JS code. This linear memory is the primary location where WASM programs store data.

    • **Memory Page:** WASM’s linear memory is divided into memory pages of fixed size, typically 64KB per page.
    • **Memory Growth:** WASM programs can increase the size of the linear memory by using the `memory.grow` instruction, adding one page at a time. The maximum size of linear memory is limited by the configuration of the WebAssembly.Memory instance, typically 4GB (65536 pages).
    • **Address Space:** WASM code accesses memory through a linear address, which is the offset relative to the starting position of the linear memory.

    How JS Accesses WASM Memory

    JS can obtain the `ArrayBuffer` object of the linear memory through the `buffer` property of the `WebAssembly.Memory` instance. After getting the `ArrayBuffer`, JS can read and write WASM’s memory just like operating on a normal array.

    const wasmMemory = new WebAssembly.Memory({ initial: 10 }); // Initial 10 pages
    const buffer = wasmMemory.buffer; // Get ArrayBuffer
    const uint8Array = new Uint8Array(buffer); // Create TypedArray view
    
    // JS writes data to WASM memory
    uint8Array[0] = 42;
    
    // Read data from WASM memory
    const value = uint8Array[0];
    console.log(value); // Output 42

    Challenges of Sharing Memory Between JS and WASM

    Although JS can directly access WASM’s linear memory, sharing memory also brings some challenges:

    1. **Memory Alignment:**
      • Different types of data need to be aligned at specific byte boundaries. For example, 32-bit integers usually need 4-byte alignment.
      • If JS and WASM access unaligned memory in different ways, it may lead to performance degradation or even errors.
      • **Solution:** Ensure that JS and WASM use the same memory alignment. The `DataView` object can be used for precise memory access.
    2. **Data Type Conversion:**
      • JS and WASM use different data type representations. For example, JS numbers are 64-bit floating-point numbers, while WASM can use 32-bit integers, 64-bit integers, and other types.
      • Type conversion is required when passing data between JS and WASM.
      • **Solution:** Use `TypedArray` (e.g., `Int32Array`, `Float64Array`) to create a view in JS that matches WASM’s memory layout.
    3. **Race Conditions:**
      • If JS and WASM access the same block of memory simultaneously, race conditions may occur, leading to data inconsistency.
      • **Solution:** Use the `Atomics` API for synchronization. The `Atomics` API provides some atomic operations, such as atomic add, atomic subtract, atomic compare and exchange, which can ensure the safety of concurrent access.
    4. **Causes and Solutions for Segmentation Faults:**
      • The Segmentation Fault errors we encountered may be caused by the following reasons:
        • **Out-of-bounds Access:** JS or WASM accessed an address outside the linear memory range. This may be due to calculation errors or unsynchronized memory growth.
        • **Illegal Address:** Access to an unallocated or protected memory address.
        • **Data Race:** Even though the Atomics API was used, race conditions may still exist in some complex concurrent scenarios.
      • **Solution:**
        • **Carefully Check Memory Access Logic:** Ensure that JS and WASM memory accesses are within the valid range of linear memory.
        • **Use Debugging Tools:** Use WASM debugging tools (e.g., Chrome DevTools’ WebAssembly Inspector) to track memory access and find the root cause of the errors.
        • **Finer-Grained Synchronization:** Consider using finer-grained Atomics operations, or other synchronization mechanisms (e.g., mutexes) to protect shared memory.
        • **Avoid Sharing Complex Data Structures:** Avoid sharing complex data structures between JS and WASM as much as possible. If sharing is necessary, you can use serialization/deserialization to transfer data.

    Notes on Using the Atomics API for Synchronization

    Although the `Atomics` API can be used to synchronize JS and WASM memory access, the following points also need to be noted:

    • `Atomics.wait` can only be called in a WebWorker and cannot be used in the main thread.
    • `Atomics` operations still have some performance overhead, and excessive use may affect performance.
    • In complex concurrent scenarios, synchronization strategies need to be carefully designed to avoid problems such as deadlocks.

    What are Transferable Objects?

    Transferable Objects are special objects that, when transferred, do not involve data copying. Instead, they directly transfer ownership of the underlying memory of the object from one context to another. This means that after the transfer, the original context can no longer access the object, while the receiving context has full control over it.

    Common Transferable Objects include:

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

    **Advantages of Transferable Objects:**

    1. **Zero-Copy:** This is the biggest advantage of Transferable Objects. Because there is no data copying, the transfer speed is very fast, especially suitable for transferring large data blocks.
    2. **Avoid Memory Contention:** Due to the ownership transfer, only one context can access the object, thus avoiding memory contention and data inconsistency issues. This simplifies concurrent programming, eliminating the need for complex synchronization mechanisms.

    **Disadvantages of Transferable Objects:**

    1. **Ownership Transfer:** This is a limitation of Transferable Objects. After the transfer, the original context can no longer access the object. If the same data needs to be used simultaneously in multiple contexts, Transferable Objects are not suitable.
    2. **Object Type Restrictions:** Only specific types of objects can be transferred as Transferable Objects. Common object types (e.g., ordinary JavaScript objects) do not support Transferable Objects.
    3. **Unidirectional Transfer:** Transferable Objects are typically used for unidirectional data transfer. If data needs to be exchanged frequently between two contexts, the efficiency may not be high.

    **Comparison of Transferable Objects Scheme and Shared Memory Scheme:**

    FeatureTransferable ObjectsShared Memory (SharedArrayBuffer)
    Data CopyingZero-copyCopy required (initial copy), subsequent modifications are shared
    Concurrency SafetyInherently safe, no synchronization mechanism requiredRequires the use of Atomics API for synchronization, adding complexity
    Object TypesOnly specific types are supported (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas)Supports any type, but serialization/deserialization is required
    Usage ScenariosSuitable for scenarios involving unidirectional transfer of large data blocks, e.g., transferring processed image data from a WebWorker to the main threadSuitable for scenarios where multiple threads/contexts need to simultaneously access and modify the same data, e.g., collaborative document editing
    ComplexitySimpleComplex, requires handling memory alignment, data type conversion, race conditions, etc.
    SuitabilitySuitable for WASM X2T scenarios. After X2T processes the file in WebWorker, the `Editor.bin` data is transferred to the main thread via Transferable Objects. After the main thread receives the data, the WebWorker no longer needs it. This avoids data copying and improves performance.Less suitable for WASM X2T scenarios. The main requirement of X2T is to transfer processing results to the main thread, rather than sharing data. Using shared memory would require complex synchronization between JS and WASM, which would increase complexity.
    WASM IntegrationVery suitable: After WASM processes the data in WebWorker, it can use Transferable Objects to transfer the results to the main thread without any data copying.Can be combined: JS and WASM can share linear memory, but careful handling of memory alignment, data type conversion, and race conditions is required.
    WebWorker SupportGoodGood

    **Conclusion:**

    • Transferable Objects are a simple and efficient data transfer method, suitable for scenarios involving the unidirectional transfer of large data blocks.
    • Shared Memory (SharedArrayBuffer) is suitable for scenarios where multiple threads/contexts need to simultaneously access and modify the same data, but it is more complex to implement.

    **In the WASM X2T scenario, Transferable Objects are likely to be a better choice.** This is because the main requirement of X2T is to transfer the processing results to the main thread, rather than sharing data. Using Transferable Objects can avoid data copying, improve performance, and simplify the complexity of concurrent programming.

  • Solving Emoji Rendering Challenges: Unicode Range and JavaScript in Perfect Harmony

    Emojis, these vivid and expressive symbols, have become an indispensable part of our daily communication. However, in certain technical scenarios, such as in a self-developed character rendering engine, rendering emojis correctly is not always an easy task. This article will delve into the Unicode Range of Emojis and how to correctly handle Emoji Unicode in JavaScript, ultimately achieving perfect Emoji rendering.

    Emoji and Unicode Range

    Unicode is an international standard character set that assigns a unique numeric code, called a code point, to almost every character in the world. Emojis are no exception; they are assigned to multiple code point ranges within Unicode, typically located in the Supplementary Multilingual Plane (SMP), which means their code point values are greater than `0xFFFF`.

    Common Emoji Unicode Ranges include:

    • **Emoticons (1F600–1F64F):** Contains various facial expressions, such as smiles, cries, surprise, and more.
    • **Symbols & Pictographs (1F300–1F5FF):** Contains various symbols and pictographs, such as food, vehicles, locations, and more.
    • **Transport & Map Symbols (1F680–1F6FF):** Contains symbols related to transportation and maps.
    • **Miscellaneous Symbols and Pictographs (1F900–1F9FF):** Contains various miscellaneous symbols and pictographs.

    Because the code point values of Emojis may exceed the Basic Multilingual Plane (BMP), traditional character handling methods may encounter problems.

    Emoji Challenges in Two Rendering Modes

    In our scenario, there are two character rendering modes:

    1. **Font File-Based Character Rendering:** This mode relies on font files to render characters, including Emojis. If an Emoji font file is missing, Emojis cannot be rendered. Even if an Emoji font is provided, correctly identifying and handling the Emoji Unicode is still a challenge.
    2. **Fast Rendering Mode (Canvas.fillText):** This mode directly uses the Canvas `fillText` method to draw characters. In this mode, as long as the character’s Unicode is handled correctly, Emoji rendering can be achieved relatively simply.

    Regardless of the mode, correctly handling the Emoji Unicode is crucial.

    Problems with Traditional Methods: Limitations of charCodeAt and fromCharCode

    In the past code, we used the `String.charCodeAt` method and the `String.fromCharCode` method to handle Unicode and restore characters. However, these two methods have a significant limitation: they can only handle characters with code point values between `0x0000` and `0xFFFF`, and cannot correctly handle Emoji Unicode.

    For example, for an Emoji with a code point value of `0x1F600`, the `charCodeAt` method can only return `0xD83D`, and the `fromCharCode` method can only process values less than `0xFFFF`. This causes Emojis to be incorrectly identified and rendered.

    Solution: Embrace codePointAt and fromCodePoint

    To solve this problem, we need to use the `String.codePointAt` method and the `String.fromCodePoint` method to replace the `String.charCodeAt` method and the `String.fromCharCode` method.

    • **`String.codePointAt(index)`:** Returns the Unicode code point value of the character at the specified index position in the string. Even if the character’s code point value is greater than `0xFFFF`, it can be returned correctly.
    • **`String.fromCodePoint(codePoint)`:** Creates a string using the specified Unicode code point value.

    By using these two methods, we can correctly obtain and restore the Emoji Unicode code point values, laying the foundation for the correct rendering of Emojis.

    // Get the Unicode code point of the Emoji
    const emoji = '😀';
    const codePoint = emoji.codePointAt(0); // 128512 (0x1F600)
    
    // Restore Emoji using the Unicode code point
    const restoredEmoji = String.fromCodePoint(codePoint); // "😀"

    Character Traversal and Glyph Measurement

    After using `codePointAt` and `fromCodePoint`, we also need to pay attention to one issue: because the code point value of an Emoji may be greater than `0xFFFF`, this means that one Emoji character may occupy two JavaScript character positions (UTF-16 encoding).

    Therefore, in the previous logic of measuring and filling characters individually, we need to identify whether a character occupies two units of characters and then process them accordingly.

    For example, we need to determine whether the code point value returned by `codePointAt` is greater than `0xFFFF`. If it is greater, we need to skip the next character, as it is actually part of the current 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++; // Skip the next character
      }
    
      // Perform character measurement and filling
      // ...
    }

    The Challenge of Combined Emojis

    In addition to single Emojis, there is also a special type called Combined Emojis. These Emojis are composed of multiple Unicode characters, such as skin tone modifiers, gender symbols, and more.

    Rendering Combined Emojis is a more complex problem because we need to correctly identify and combine these characters to render the correct Emoji. This may require refactoring the entire rendering logic, measuring and rendering the string as a whole, rather than processing characters individually.

    However, how to correctly split the string to identify the boundaries of Combined Emojis is a very tricky issue. This may require the help of complex Unicode specifications and regular expressions.

    Summary and Outlook

    By using the `String.codePointAt` and `String.fromCodePoint` methods, we can effectively solve the Emoji Unicode handling problem and lay the foundation for the correct rendering of Emojis. However, rendering Combined Emojis remains a challenge and requires further research and practice.

    In the future, we can consider the following directions:

    • **In-depth Research on Unicode Specifications:** Gain a deeper understanding of Unicode specifications, especially regarding Emojis and Combined Emojis.
    • **Application of Regular Expressions:** Use regular expressions to identify and split Combined Emojis.
    • **Rendering Engine Refactoring:** Measure and render the string as a whole, rather than processing characters individually.

    I hope this article will help you better understand the Unicode Range of Emojis and how to correctly handle Emoji Unicode in JavaScript, ultimately achieving perfect Emoji rendering.

  • EO2Suite Font Fallback Mechanism: From Garbled Text to Elegant Display

    In our daily work, we often need to open documents on different devices and operating systems. To ensure that documents appear as the author intended, fonts play a crucial role. However, font rendering in online document preview scenarios presents several challenges. Today, let’s delve into the efforts EO2Suite has made in its font fallback mechanism and how we’ve optimized it to make online document previews more seamless.

    The Challenges of Online Font Rendering

    Online document previews, unlike local applications, cannot directly access the user’s local font files. This leads to two key issues:

    1. **Limited Font Resources:** We cannot rely on the fonts installed on the user’s system, as local applications do. For display purposes, we need to download font files from a server.
    2. **Font Copyright Restrictions:** Commercial fonts typically require licensing, and we cannot directly host these paid fonts on a server for users to download. Therefore, we must choose to use free fonts.

    These two challenges make font fallback crucial in online documents. If a user’s chosen font is not on our font list, we need to find a suitable substitute. A poor fallback logic can lead to distorted text, severely impacting the user experience.

    EO2Suite’s Font Fallback Mechanism: A Detailed “Font-Finding” Journey

    EO2Suite has put a lot of effort into this area. Instead of simply selecting a default font, they’ve built a complex fallback mechanism based on “penalty value calculation” and “Unicode index.”

    1. Font Dictionary: A Comprehensive “Font Map”

    First, EO2Suite maintains a vast font dictionary containing:

    • **Font Names:** e.g., Arial, Times New Roman.
    • **Unicode Ranges:** Describing the supported Unicode character range for the font.
    • **Font File Sizes:** Used to evaluate download costs.

    This font dictionary serves as a detailed “font map,” helping EO2Suite quickly locate available font resources.

    2. Penalty Value Calculation: Selecting the Best Substitute

    When a user’s chosen font is not in the dictionary, EO2Suite calculates a “penalty value” for each candidate font using the information in the dictionary. This penalty value considers:

    • **Similarity of Font Names:** For example, if a user selects “Arial Bold”, a font named “Arial” in the dictionary would have a relatively lower penalty value.
    • **Matching Unicode Ranges:** If a document contains many Chinese characters, fonts supporting Chinese would have lower penalty values.
    • **Font File Size:** Smaller fonts download faster, so they have lower penalty values.

    Ultimately, EO2Suite chooses the font with the smallest penalty value as the fallback font and downloads it. This tries to ensure the displayed document is as close to the user’s expectation as possible.

    3. Unicode Index: A Last “Resort”

    However, even after selecting a fallback font with penalty value calculations, the target font might still lack specific characters in the user’s document. At this point, EO2Suite uses a final fallback mechanism: the Unicode index.

    EO2Suite generates a Unicode index in advance, dividing the entire Unicode space into segments, and each segment corresponds to a font that covers it. This font is the smallest file size font from the supported list. This ensures that any character can find a corresponding glyph in the font set, minimizing instances of characters displayed as “squares.”

    Our Optimization: Special Care for Chinese

    EO2Suite’s font fallback mechanism is already quite robust, but there was still room for improvement in Chinese character display. Considering the complexity and diversity of Chinese fonts, we enhanced the mechanism:

    1. **Introducing Open-Source Chinese Fonts:** We added high-quality open-source Chinese fonts such as Source Han Sans and Source Han Serif. These fonts cover a wider range of Unicode and display Chinese characters better.
    2. **Hardcoding Fallbacks for Common Fonts:** We hardcoded fallbacks for common Chinese fonts like Microsoft YaHei, Songti, Heiti, and PingFang to Source Han Sans or Source Han Serif. This ensures that when users select these common fonts, they fall back to the most appropriate Chinese fonts.

    These optimizations have significantly improved the issue of garbled Chinese characters, making the display of Chinese documents more accurate and aesthetically pleasing.

    Conclusion

    Font fallback is a complex but crucial technical aspect that directly affects the user experience. EO2Suite, through its detailed font dictionary, penalty value calculations, and Unicode index, has built a robust fallback mechanism. And based on that, we have optimized it for Chinese, further enhancing the online document preview experience.

    Of course, font display is a continuous process of optimization. With the advancement of technology, we will continue to explore better solutions to make online document fonts appear perfect.

    I hope this article helps you better understand EO2Suite’s font fallback mechanism. If you have any questions or suggestions, please feel free to leave them in the comment section below.

  • The Secret Behind eo2suite’s Lightning-Fast Document Loading: Optimizing the Text Rendering Engine

    Hello everyone, I’m a programmer who’s been navigating the coding world for many years. Today, I want to share the story behind eo2suite’s document loading speed optimization, especially how we drastically improved the text rendering engine to achieve that “instant document opening” experience.

    Traditional Rendering: The Heavy Burden of Font Loading

    As we all know, eo2suite is a powerful Office suite that handles all sorts of complex documents. And within these documents, text is the most crucial element. In the earlier versions of eo2suite, our text rendering engine relied on the robust [FreeType](https://freetype.org/) library.

    Here’s a simplified overview of how [FreeType](https://freetype.org/) works:

    1. **Font Loading**: When a document is opened, it parses the font information used in the document and attempts to load the corresponding font files. These font files can range from a few MBs to tens of MBs, which undoubtedly adds a burden to document loading. Especially in situations with poor network conditions or limited device performance, waiting for font loading can be frustrating.
    2. **Glyph Retrieval**: After the font is loaded, [FreeType](https://freetype.org/) looks up the glyph information (the actual shapes of the characters we see) in the font file, based on their Unicode values, and calculates the necessary data for rendering.
    3. **Canvas Drawing**: Finally, combined with text styles and font sizes, the glyph is rendered onto the page using the `canvas.drawImage` API.

    This approach has the advantage of rendering accurately, perfectly representing the font designer’s intentions. However, its disadvantage is also obvious: **Font loading is time-consuming and is a major bottleneck impacting document opening speed.**

    Taking a Different Path: The Rise of HTML Canvas

    To solve this issue, we started delving deeper into the capabilities of HTML5 canvas. We discovered that the `canvas.measureText` API can provide us with the text metrics (TextMetrics) of a character in a certain font, including character width, height, baseline offset, and more. **This provided us with a brand new approach to rendering text without relying on font files at all!**

    Here’s our optimization strategy:

    1. **Speed Mode**: We introduced “Speed Mode.” In this mode, instead of loading font files, we use the `canvas.measureText` API to obtain the text metrics of characters and then directly render the text onto the page using the `canvas.fillText` API. This method eliminates the significant time spent loading font files, significantly improving document opening speed.**You can think of this process like this:** Previously, we needed to find a book (font file) from the bookshelf, then flip to the corresponding page (glyph information), and then draw the text on paper based on the content of the book. Now, we only need to know the “space” each character occupies (TextMetrics) and then we can write directly on the paper. Isn’t that much faster?
    2. **Accurate Mode**: To accommodate certain special cases, we retained the traditional “Accurate Mode.” When a document contains WordArt or other effects that require glyph shapes for proper rendering, we automatically switch to “Accurate Mode.” In addition, if a user inserts WordArt in “Speed Mode,” we will automatically switch back to “Accurate Mode,” ensuring that all content is displayed correctly.

    **Two modes working in tandem, balancing speed and precision.**

    Significant Results from Optimization

    With these changes, the document opening speed has taken a qualitative leap, especially in situations with poor network conditions or limited device performance. Users no longer have to wait for a long time for the document to load and can start reading and editing immediately.

    Conclusion and Future Prospects

    This optimization of the text rendering engine is a reflection of our continuous pursuit of excellence and perfection. We believe that through constant technological innovation, we can bring better product experiences to users. In the future, we will continue to explore new technologies to further enhance document processing efficiency, making eo2suite your best partner for work and study.

    I hope today’s sharing has been enlightening. If you are interested in the technical details of eo2suite, please follow my blog, and I’ll see you next time!