Skip to content

perf: fix severe slowdown with 5+ screenshots#39

Open
kmadhu0 wants to merge 1 commit into
YUZU-Hub:mainfrom
kmadhu0:main
Open

perf: fix severe slowdown with 5+ screenshots#39
kmadhu0 wants to merge 1 commit into
YUZU-Hub:mainfrom
kmadhu0:main

Conversation

@kmadhu0

@kmadhu0 kmadhu0 commented Jun 16, 2026

Copy link
Copy Markdown

Closes #38

Problem

The app becomes almost unusable with more than 2-3 screenshots. Every slider drag triggered: a full synchronous canvas render (1320×2868 px), four additional side-preview renders at the same resolution, an immediate IndexedDB write, and a 14MB pixel-manipulation loop when noise was enabled. All of this on the main thread, per slider tick.

What changed

rAF batching + debounced save

  • updateCanvas() now schedules via requestAnimationFrame, collapsing dozens of synchronous calls during a slider drag into a single render per frame
  • saveState() is debounced to 500ms; exports and slide transitions use updateCanvasNow() to flush synchronously when needed

Side preview resolution

  • Side-preview canvases now render at display resolution (~130px wide) instead of full export resolution (1320×2868), using ctx.scale() so font sizes and all other values stay correct — ~100× fewer pixels per preview

Side preview bitmap cache

  • Non-selected screenshots don't change while you're editing the active one; their rendered bitmaps are cached and reused each frame instead of re-drawn. Cache invalidates on language/device switch, style transfer, and project change.

Canvas reallocation guard

  • canvas.width = x unconditionally clears and reallocates the GPU buffer even when dimensions haven't changed; now skipped when unchanged

Noise

  • Replaced getImageData → pixel loop → putImageData (14MB allocation per frame) with a cached offscreen canvas composited via overlay blend mode. Pattern only regenerates when intensity or canvas size changes.

Text normalization

  • normalizeTextSettings() did a JSON deep-clone on every getText() call. A _normalized flag now short-circuits subsequent calls on the same object. Flag is stripped before IndexedDB serialization.

Blob URL leak

  • getLucideImage() was creating a blob URL per icon render and never revoking it. Now revoked in both onload and onerror.

Testing

Verified with Playwright (headless Chromium):

  • App loads without errors
  • 3-4 screenshots added and navigated between
  • Rapid slider input (20 events, no delay) → 0 errors, 1 render per rAF frame confirmed
  • Side preview canvases confirmed at display resolution (not full export res)
  • updateCanvasNow() + toDataURL() produces valid PNG for export
  • Side preview cache confirms: 3 entries for 4 screenshots (1 active = uncached), stable across frames

Notes on PR #37

PR #37 includes the same revokeObjectURL blob URL fix — if that merges first, this line will be identical and conflict-free. All other changes here are unique to this PR. PR #37's "debounce" is a different mechanism (server-push to MCP backend, not IndexedDB).

🤖 Generated with Claude Code

rAF + debounced save:
- Batch all updateCanvas() calls to one render per animation frame
  (previously each slider tick fired a full synchronous render)
- Debounce IndexedDB writes to 500ms; exports and slide transitions
  flush synchronously via updateCanvasNow()

Side preview optimizations:
- Render side-preview canvases at display resolution via ctx.scale()
  instead of full export resolution (1320×2868 → ~130px wide), ~100x
  fewer pixels per preview
- Cache rendered side-preview bitmaps per screenshot index; non-selected
  screenshots don't change while editing the active one, so re-drawing
  them every frame is pure waste. Cache invalidates on language/device
  switch, style transfer, and project change.

Main canvas:
- Skip GPU buffer reallocation when canvas dimensions haven't changed
  (canvas.width = x always clears + reallocates, even for same value)

Noise:
- Replace getImageData/putImageData pixel loop (14MB alloc per frame)
  with a cached offscreen canvas composited via overlay blend mode.
  Pattern regenerates only when intensity or canvas size changes.

Text:
- Add _normalized flag to text objects; normalizeTextSettings() skips
  the JSON deep-clone on already-normalized objects. Flag is stripped
  before IndexedDB serialization.

Misc:
- Revoke blob URLs in getLucideImage() after Image load to stop
  incremental memory leak on icon color changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.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.

Screenshot editing becomes severely slow and almost hangs when working with more than 2 screenshots

1 participant