Skip to content

perf: use charWidthCache to improve performance by 1.5% - #1978

Merged
wang1212 merged 10 commits into
releasefrom
perf/measureText
Jul 28, 2025
Merged

perf: use charWidthCache to improve performance by 1.5%#1978
wang1212 merged 10 commits into
releasefrom
perf/measureText

Conversation

@Alexzjt

@Alexzjt Alexzjt commented Jul 22, 2025

Copy link
Copy Markdown
Contributor

🤔 This is a ...

  • New feature
  • Bug fix
  • Site / Document optimization
  • TypeScript definition update
  • Refactoring
  • Performance improvement
  • Code style optimization
  • Test Case
  • Branch merge
  • Other (about what?)

🔗 Related issue link

💡 Background and solution

原有逻辑:短暂的、局部的字符缓存

// packages/g-lite/src/services/TextService.ts

private wordWrap(
    text: string,
    parsedStyle: ParsedTextStyleProps,
    offscreenCanvas: CanvasLike,
): string {
    // ...
    
    // 关键点:这个 cache 是在 wordWrap 函数内部创建的局部变量
    const cache: { [key in string]: number } = {}; 
    const calcWidth = (txt: string): number => {
      return this.getFromCache(
        txt,
        letterSpacing,
        cache, // 每次都传入这个新的、空的 cache
        context as CanvasRenderingContext2D,
      );
    };

    // ... 循环处理字符 ...
}
瓶颈分析
  • cache 对象是在 wordWrap 函数被调用时才创建的一个局部变量。
  • 这意味着,这个缓存的生命周期仅限于单次 wordWrap 的执行过程。
  • 当一个文本对象(比如一个单元格的文本)完成换行计算后,这个 cache 就会被销毁。
  • 当滚动表格,下一个新的文本对象进入视图并需要计算布局时,它会再次调用 wordWrap,并创建一个全新的、空的 cache。

结论:现有的字符缓存机制,只能对单个长文本的内部重复字符生效(比如一个段落里有多个'A',只测量一次'A'的宽度)。但它无法在多个不同的文本对象之间共享缓存。在表格滚动场景下,成百上千个单元格被创建和销毁,每个单元格的文本都会触发一次独立的、从零开始的字符宽度测量,这导致 context.measureText 被大量重复调用,从而产生了2%的性能开销。

优化方案

将这个局部的、短暂的 cache 提升为持久的、服务级别的全局缓存,就像 fontMetricsCache 一样。需要让这个缓存与字体关联,因为同一个字符在不同字体下的宽度是不同的。一个理想的缓存结构是:

{ [font: string]: { [char: string]: number } }

1.当第一个表格单元格渲染时,它会计算并缓存其文本中所有字符在特定字体下的宽度。
2.当滚动到新的单元格时,如果它们使用了相同的字体,wordWrap 在计算字符宽度时将几乎总是命中全局缓存 this.charWidthCache。
3.实际调用 context.measureText 的次数将从“每个单元格的每个字符一次”锐减到“整个应用生命周期内,每种字体的每个字符一次”。

📝 Changelog

Before After
image image
image 归零
Language Changelog
🇺🇸 English use charWidthCache to improve performance by 1.5%
🇨🇳 Chinese 实现全局字符宽度缓存提升1.5%的性能

☑️ Self Check before Merge

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • TypeScript definition is updated/provided or not needed
  • Changelog is provided or not needed

@changeset-bot

changeset-bot Bot commented Jul 22, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4b6b4bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@antv/g-lite Patch
@antv/g-camera-api Patch
@antv/g-canvas Patch
@antv/g-canvaskit Patch
@antv/g-components Patch
@antv/g-dom-mutation-observer-api Patch
@antv/g-gesture Patch
@antv/g-image-exporter Patch
@antv/g-lottie-player Patch
@antv/g-mobile-canvas-element Patch
@antv/g-mobile-canvas Patch
@antv/g-mobile-svg Patch
@antv/g-mobile-webgl Patch
@antv/g-pattern Patch
@antv/g-plugin-3d Patch
@antv/g-plugin-a11y Patch
@antv/g-plugin-annotation Patch
@antv/g-plugin-box2d Patch
@antv/g-plugin-canvas-path-generator Patch
@antv/g-plugin-canvas-picker Patch
@antv/g-plugin-canvas-renderer Patch
@antv/g-plugin-canvaskit-renderer Patch
@antv/g-plugin-control Patch
@antv/g-plugin-css-select Patch
@antv/g-plugin-device-renderer Patch
@antv/g-plugin-dom-interaction Patch
@antv/g-plugin-dragndrop Patch
@antv/g-plugin-gesture Patch
@antv/g-plugin-html-renderer Patch
@antv/g-plugin-image-loader Patch
@antv/g-plugin-matterjs Patch
@antv/g-plugin-mobile-interaction Patch
@antv/g-plugin-physx Patch
@antv/g-plugin-rough-canvas-renderer Patch
@antv/g-plugin-rough-svg-renderer Patch
@antv/g-plugin-svg-picker Patch
@antv/g-plugin-svg-renderer Patch
@antv/g-plugin-yoga Patch
@antv/g-plugin-zdog-canvas-renderer Patch
@antv/g-plugin-zdog-svg-renderer Patch
@antv/g-svg Patch
@antv/g-web-animations-api Patch
@antv/g-web-components Patch
@antv/g-webgl Patch
@antv/g-webgpu Patch
@antv/g Patch
@antv/react-g Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @Alexzjt, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request primarily focuses on enhancing rendering performance, particularly for text elements, by optimizing how character widths are measured and cached. The core change involves elevating a previously local character width cache to a global, font-aware service-level cache, drastically reducing redundant measureText calls. Additionally, I've improved the efficiency of font string generation and added a minor optimization to scene graph dirty state propagation.

Highlights

  • Performance Improvement: Global Character Width Caching: I've introduced a new global charWidthCache within the TextService to persist character width measurements across different text objects. Previously, the cache was local to each wordWrap call, leading to redundant context.measureText calls, especially in scenarios like scrolling tables with many text elements. This change aims to significantly reduce these repeated measurements.
  • Memoization for Font String Generation: I've applied memoization to the toFontString utility function in packages/g-lite/src/utils/text.ts. This caches the generated font strings based on text style attributes, preventing redundant computations when the same font style is used repeatedly, further contributing to performance.
  • Scene Graph Dirty State Optimization: I've refined the dirty state propagation logic in DefaultSceneGraphService.ts. The while loop that marks parent elements as dirty now includes a condition to stop traversal if an element is already marked as both dirty and boundsDirty. This can prevent unnecessary re-marking of elements that have already been flagged for update.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a global cache for character widths to improve text rendering performance. The core logic in TextService.ts is sound and directly addresses the performance bottleneck.

However, I've identified a few areas that need attention:

  • A critical issue exists in toFontString where the memoization introduces code duplication and flawed logic for handling fontSize, potentially leading to incorrect font rendering and caching problems.
  • The modification in packages/g-lite/src/services/SceneGraphService.ts is an unrelated optimization and should be submitted in its own PR.
  • There's an inaccurate comment in packages/g-lite/src/services/TextService.ts that needs to be updated.

Addressing the critical issue in toFontString is essential before merging.

Comment thread packages/g-lite/src/utils/text.ts
Comment thread packages/g-lite/src/services/SceneGraphService.ts
Comment thread packages/g-lite/src/services/TextService.ts Outdated
@wang1212

wang1212 commented Jul 22, 2025

Copy link
Copy Markdown
Member

这个改动风险比较大,字体的测量结果不仅与字体相关,可能还与对齐方式等其它属性相关;还有持久缓存可能带来内存泄露问题

@Alexzjt

Alexzjt commented Jul 22, 2025

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a global, persistent charWidthCache using an LRU strategy to improve text rendering performance. The implementation also includes a new LRU cache utility and memoization for toFontString.

A critical issue was identified in TextService.ts where the new LRU cache is accessed incorrectly, which will prevent it from working. A suggestion to further improve performance in toFontString by avoiding a redundant function call was also made.

Once the critical issue is addressed, this change should provide the intended performance benefits.

Comment thread packages/g-lite/src/services/TextService.ts
Comment thread packages/g-lite/src/utils/text.ts
@wang1212
wang1212 changed the base branch from master to release July 23, 2025 05:30
@wang1212

Copy link
Copy Markdown
Member

G2、G6 本地测试通过

@wang1212
wang1212 merged commit ffe2611 into release Jul 28, 2025
2 checks passed
@wang1212
wang1212 deleted the perf/measureText branch July 28, 2025 06:55
wang1212 added a commit that referenced this pull request Jul 30, 2025
* fix: prevent NaN in transform calculations when scale is 0 (#1971)

* fix: prevent NaN in transform calculations when scale is 0

- Add MIN_SCALE constant to avoid zero scaling
- Update scale transform functions to enforce minimum scale value
- Add test case for scale(0) transform
- Add demo example with scale(0) transform

* chore: add changeset

* refactor: extract scale clamping logic into reusable function

- Add clampScale helper function to avoid code duplication
- Replace repeated Math.max(item, MIN_SCALE) calls with clampScale
- Maintain same behavior while improving code maintainability

* chore: add changeset

* perf: use charWidthCache to improve performance by 1.5% (#1978)

* perf: 避免重复dirty父级元素提升2%性能

* perf: accept gemini review

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* refactor: 修复eslint

* perf: use charWidthCache to improve performance by 1.5%

* refactor: 修复评审意见

* refactor: 修复评审意见

* refactor: 使用LRU

* refactor: 使用LRU

* chore: add changeset

---------

Co-authored-by: huiyu.zjt <huiyu.zjt@antgroup.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: wang1212 <mrwang1212@126.com>

* revert: fix element attribute update exception #1968

* chore(release): bump version (#1977)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: huiyu.zjt <Alexzjt@users.noreply.github.com>
Co-authored-by: huiyu.zjt <huiyu.zjt@antgroup.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants