Skip to content

perf(record/canvas): 2D ImageBitmap fast-path (SR-4163)#108

Draft
lewgordon-amplitude wants to merge 4 commits into
masterfrom
lewgordon/sr-4163-pillar-3-canvas-fast-path
Draft

perf(record/canvas): 2D ImageBitmap fast-path (SR-4163)#108
lewgordon-amplitude wants to merge 4 commits into
masterfrom
lewgordon/sr-4163-pillar-3-canvas-fast-path

Conversation

@lewgordon-amplitude
Copy link
Copy Markdown

Summary

Resolves the years-old TODO at packages/rrweb/src/record/observers/canvas/serialize-args.ts:99:

// TODO: move `toDataURL` to web worker if possible
const src = value.toDataURL(); // heavy on large canvas

Linear: https://linear.app/amplitude/issue/SR-4163
Confluence plan: https://amplitude.atlassian.net/wiki/spaces/IG/pages/3794403393

What's done

  • 2D fast-path (serialize-args.ts): When OffscreenCanvas and transferToImageBitmap are both available (Chrome 66+, Firefox 119+, Safari 18.2+), serializeArg(HTMLCanvasElement) now returns a Promise<CanvasArg> instead of blocking the main thread with toDataURL. The canvas frame is transferred (zero-copy) to the existing image-bitmap-data-url-worker which encodes it off-thread with OffscreenCanvas.convertToBlob.

  • Worker extended (image-bitmap-data-url-worker.ts): Added a one-shot encodeId-keyed message variant (ImageBitmapEncodeWorkerParams/ImageBitmapEncodeWorkerResponse) alongside the existing FPS-sampling path. Both paths share a new encodeBitmap helper to avoid code duplication.

  • 2D observer async-aware (2d.ts): The setTimeout callback now awaits Promise.all(serializeArgs(...)) so that canvas args resolve before the mutation callback fires.

  • canvas-manager.ts: Forwards dataURLOptions into initCanvasMutationObserver; narrows the FPS-worker's onmessage type to avoid union confusion with the new encode response type.

  • Types (packages/types/src/index.ts): Added ImageBitmapEncodeWorkerParams and ImageBitmapEncodeWorkerResponse — purely additive, no enum reordering.

  • Tests: 8 jsdom unit tests in test/record/canvas-2d-fast-path.test.ts covering: fast-path returns Promise, worker message shape, Promise resolution, fallback paths (OffscreenCanvas absent, transferToImageBitmap absent), and replay back-compat deserialization.

What's deferred

  • Full worker masking pipeline (the larger part of Pillar 3): worker-side maskTextFn/maskInputFn application is deferred until Pillar 0's parity harness is ready to gate it.
  • WebGL toDataURL worker offload: WebGL already uses the FPS-sampling path with worker encoding; the mutation-observer path for WebGL was not changed in this PR.

Back-compat

The serialized output of the fast-path is identical in shape to the existing fallback — { rr_type: 'HTMLImageElement', src: 'data:image/...' }. Existing recordings are fully compatible; deserialize-args.ts required no changes. Environments without OffscreenCanvas/transferToImageBitmap fall through to the synchronous toDataURL path unchanged.

Browser-compat caveats

API Chrome Firefox Safari
transferToImageBitmap 66+ 119+ 18.2+
OffscreenCanvas 69+ 105+ 16.4+

Both must be present for the fast-path to activate. All other environments fall through to the existing synchronous path.

Test plan

  • packages/rrweb type check: tsc -noEmit passes
  • packages/rrweb vite build passes
  • New jsdom tests pass: test/record/canvas-2d-fast-path.test.ts (8 tests)
  • Existing jsdom tests unaffected: serialize-args, 2d-mutation, deserialize-args, webgl-mutation, replay/2d-mutation, seek-cache, checkpoint-index, rrdom, util (all pass)
  • Puppeteer/e2e tests: require Chrome for Testing (pre-existing worktree environment gap)

🤖 Generated with Claude Code

Resolve the years-old TODO at serialize-args.ts:99 by adding a canvas-2D
fast-path that transfers an ImageBitmap to the image-bitmap-data-url
worker for off-thread encoding instead of calling synchronous toDataURL.

- serialize-args: detect OffscreenCanvas + transferToImageBitmap support;
  when both are available, serializeArg returns a Promise<CanvasArg> that
  resolves once the worker finishes encoding. Sync toDataURL is kept as the
  fallback for browsers lacking either API.
- image-bitmap-data-url-worker: extend to handle one-shot 'encodeId'-keyed
  encode requests (new ImageBitmapEncodeWorkerParams type) alongside the
  existing FPS-sampling path, reusing a shared encodeBitmap helper.
- 2d.ts observer: await Promise.all(serializeArgs(...)) so async canvas
  args resolve before the mutation callback fires.
- canvas-manager.ts: forward dataURLOptions into the mutation observer;
  narrow the FPS-worker onmessage type to avoid union confusion.
- types: add ImageBitmapEncodeWorkerParams / ImageBitmapEncodeWorkerResponse
  (additive — no enum reordering).
- test: 8 jsdom unit tests covering both the fast-path and fallback paths,
  plus replay back-compat deserialization.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 6, 2026

⚠️ No Changeset found

Latest commit: f5a1421

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

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.

1 participant