Skip to content

fix(g-canvas): keep group clipPath for children in optimized rendering - #2274

Open
shkomg wants to merge 2 commits into
antvis:masterfrom
userstoriesbot-showcase:fix/optimized-group-clip
Open

shkomg wants to merge 2 commits into
antvis:masterfrom
userstoriesbot-showcase:fix/optimized-group-clip

Conversation

@shkomg

@shkomg shkomg commented Nov 3, 2026 •

Copy link
Copy Markdown

🤔 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

Closes #1915

💡 Background and solution

With enableRenderingOptimization: true, the canvas renderer ignored a clipPath set on a Group for the group's children. renderDisplayObjectOptimized called context.restore() right after drawing the object that owns the clip, so the clip was already gone when the children were drawn.

The optimized path now keeps the clip on renderState.restoreStack until the walk leaves that subtree, like renderDisplayObject does. Since restore() also reverts the styles and transform set inside the clip, the cached context state is saved with the clip and put back on restore, and prevObject is reset so the next object applies its transform and styles again. Objects without a clip get no extra save()/restore().

This also makes the existing 2d-clipPath demo render the same with the option on and off. Added the bugfix-issue_1915 demo, a unit test (__tests__/unit/plugins/canvas-rendering-optimization-clip.spec.ts) and a patch changeset.

Before After
before after

Tested: the new unit test fails without the change and passes with it (it also covers 1,000 sibling clipped groups followed by an unclipped sibling), the full jest unit suite passes (283 tests), and eslint and prettier are clean; the visual integration tests were not run locally.

The video shows the bugfix-issue_1915 demo with the option on: the red rect stays inside the clip outline, also after toggling the option and moving the rect.

antv-g.mp4

Performance

Cost with enableRenderingOptimization: an object with no clip on the stack adds one restoreStack.length check. A clipped object adds one save(), kept until the walk leaves its subtree and then undone with one restore(), plus a copy of the small style cache; the check for the next objects is parent.contains(object), which walks up from the object to the clip owner only and allocates nothing.
Measured on an Apple M4 Max (macOS 26.6.2, chrome-headless-shell 156, 1000x800, DPR 1): per frame one fill change, canvas.render() and a 1x1 getImageData to force rasterization; 60 warm-up and 300 measured frames, median of 3 runs, ms per frame, master -> this PR, p95 in brackets.

  • No clips, 10,000 rects: 10.3 (11.3) -> 10.2 (11.2)
  • 500 clipped groups x 20 children: 11.1 (12.3) -> 10.6 (12.0); 100 groups x 3 nested clips x 10 children: 1.3 (1.5) -> 1.1 (1.3)
  • 10,000 clipped sibling groups x 1 child: 19.1 (22.4) -> 18.2 (21.4)
    The clipped scenes are not like for like: master drops the group clip for children and draws them unclipped, so these numbers show the fix adds no frame time, not that it makes rendering faster.

📝 Changelog

Language Changelog
🇺🇸 English Fix clipPath on a Group not clipping its children when enableRenderingOptimization is enabled.
🇨🇳 Chinese 修复开启 enableRenderingOptimization 时 Group 上的 clipPath 对子元素不生效的问题。

☑️ 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

Fix written with Claude Code; video is an automated browser run of this branch recorded by User Stories Bot.

With enableRenderingOptimization, renderDisplayObjectOptimized restored the
context right after drawing the object that owns the clipPath, so the clip of
a Group never reached its children. Keep the clip in renderState.restoreStack
until the walk leaves the subtree, as renderDisplayObject does, and restore
the cached context state with it so cached styles do not go stale.

Closes antvis#1915
@changeset-bot

changeset-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fe81add

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

This PR includes changesets to release 6 packages
Name Type
@antv/g-canvas Patch
@antv/g-canvaskit Patch
@antv/g-mobile-canvas Patch
@antv/g-plugin-rough-canvas-renderer Patch
@antv/g-plugin-zdog-canvas-renderer Patch
@antv/g-web-components 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

@hustcc

hustcc commented Nov 4, 2026

Copy link
Copy Markdown
Member

@shkomg Will there be a noticeable drop in rendering performance?

@shkomg

shkomg commented Nov 4, 2026

Copy link
Copy Markdown
Author

Thanks for asking. The change only adds work for objects that have a clipPath: objects without one take exactly the same path as before (no extra save/restore, and the subtree check only runs while a clip is active). For each clip it keeps one save() until the walk leaves that subtree and restores a copy of the style cache afterwards.

I measured it on the canvas renderer with enableRenderingOptimization: true (Apple M4 Max, headless Chrome, 1000x800, 300 frames after warm-up, 3 runs; each frame changes one fill, renders and reads a pixel so the canvas actually rasterizes), median ms per frame, base vs this PR:

  • 10,000 rects, no clips: 10.0 vs 10.1
  • 500 clipped groups x 20 children: 10.8 vs 11.0
  • 100 chains of 3 nested clips, 10 children each: 1.3 vs 1.2

So within about 2%, which is near the timer's resolution. In the clipped scenes the base build also skips the clip and draws more pixels, so those two rows are not a perfectly like-for-like comparison. Happy to share the benchmark page or run another scene you care about.

…dering

compareDocumentPosition walked both nodes to the root and scanned siblings
when leaving a clipped subtree, which grows with many sibling clipped
groups. parent.contains(object) walks up to the clip owner only and gives
the same answer here, since an object is never its own pushed clip parent.

This branch has not been deployed

No deployments
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.

canvas 渲染器在设置 enableRenderingOptimization: true 时,Group 上设置的 clipPath 没有生效

2 participants