perf(canvas): remove per-frame canvas reset and frame-loop allocations#123
Merged
Conversation
Items 3 and 5 from the Canvas2D hot-path review — the frame tax every node pays on the canvas backend: - reset() no longer tears down the canvas backing store via the `canvas.width = canvas.width` trick; setTransform + clearRect does the same job (frame save/restore pairs are balanced). The clear-color fill is skipped when the clear color's alpha byte is 0 — previously the default transparent color still paid a full-screen fillRect. - addQuad clips with beginPath/rect/clip instead of allocating a Path2D per clipped node per frame. - The shader renderContext closure is replaced with one preallocated arrow reading node/texture scratch fields set around the shader render() call. - parseColor returns a shared scratch object instead of allocating per non-white node; its only caller chain consumes it synchronously. - The Rounded / RoundedWithShadow / roundedRectWithBorder clip paths use beginPath on the context instead of Path2D. The evenodd border fill keeps its Path2D (fill(path, 'evenodd') needs a path object). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Items 3 and 5 from the Canvas2D hot-path review (
docs/canvas-renderer-review.md) — the per-frame tax every node pays on the canvas backend, which is the fallback path for the weakest target devices.reset()no longer reallocates the canvas (item 3)canvas.width = canvas.widthreset trick forced the browser to tear down and reinitialize the canvas backing store every frame — one of the most expensive single operations in Canvas2D on embedded Blink. Replaced withsetTransform(1,0,0,1,0,0)+clearRect, which is equivalent because the frame'ssave/restorepairs are balanced andglobalAlphais restored after each image draw.if (this.clearColor)is always true sincenormalizeCanvasColor(0x00000000)returns the non-empty string"rgba(0,0,0,0)"— so the default transparent clear color still paid a full-screenfillRectevery frame. The fill is now gated on the color's alpha byte, tracked in a newclearColorAlphafield kept in sync byupdateClearColor.Frame-loop allocations killed (item 5)
Path2Dper clipped node inaddQuad→beginPath()/rect()/clip(), allocation-free. In the standard TV layout every card sits under the viewport clip, so this was one allocation per node per frame.renderContextclosure per shader node per frame → one preallocated arrow function field readingshaderContextNode/shaderContextTexturescratch fields, set before the shader'srender()call and nulled after so a destroyed node or its texture isn't pinned.parseColor()object per non-white node → returns a module-level scratch object, documented as consume-synchronously-never-retain. The only caller chain (CanvasRenderer.renderContext→CanvasTexture.getImage) reads it synchronously and retains only the derivedformatRgbastring key.Path2Ds inRounded,RoundedWithShadow, androundedRectWithBorder→beginPathon the context. The border's evenodd fill keeps itsPath2Dsincefill(path, 'evenodd')needs a path object.Deliberately out of scope
addQuad's save/restore model, better as its own PR.Tests
CanvasRendererunit tests: reset semantics (including asserting zerocanvas.widthwrites via a setter spy), transparent-clear skip, opaque-clear fill,updateClearColorsync, allocation-free clipping calls, and shader-callback reference identity across nodes.colorParser.test.tscovering the scratch-reuse contract (same object across calls, white singleton untouched).tsc --buildclean.pnpm test:visualfor that.🤖 Generated with Claude Code