feat: add stabilizationAttempts option for blank large images in WebKit captures - #591
feat: add stabilizationAttempts option for blank large images in WebKit captures#591Kebechet wants to merge 3 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #591 +/- ##
=======================================
Coverage 66.50% 66.50%
=======================================
Files 10 10
Lines 612 612
Branches 150 150
=======================================
Hits 407 407
Misses 144 144
Partials 61 61 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds an opt-in capture “stabilization” mechanism to mitigate intermittent blank large images in WebKit-based captures by repeating rasterization until two consecutive outputs match (bounded by a new stabilizationAttempts option).
Changes:
- Introduces
stabilizationAttemptsinOptionswith inline documentation. - Refactors
toCanvasto perform repeated captures and stop early on convergence. - Adds a dedicated spec covering stabilized captures and propagation through
toBlob.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/index.ts |
Implements the multi-attempt stabilization loop in toCanvas (via a new internal captureCanvas). |
src/types.ts |
Adds the new Options.stabilizationAttempts API documentation. |
test/spec/stabilization.spec.ts |
Adds tests validating stabilized rendering and option propagation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Measured on a real iPhone: WebKit rasterizes the capture's intermediate SVG before large embedded images have decoded, blanking them in the output, and a plain retry or pre-decoding does not recover it - only comparing consecutive captures until they agree does. Captures now repeat until two consecutive results are identical (StabilizeAttempts, default 3), and every capture is bounded by CaptureTimeoutMs because upstream's createImage never settles when Safari rejects HTMLImageElement.decode() under memory pressure. Both mitigations are proposed upstream: bubkoo/html-to-image#589 (hang) bubkoo/html-to-image#591 (stabilization) bubkoo/html-to-image#590 (the toBlob type/quality deviation this wrapper already works around) Post-merge cleanup tracked in #1
Two real bugs in the rasterization pipeline (cors-image.tsx, store-promo-card.tsx): - CorsImage fired its onLoad callback the instant the base64 string was computed, before the actual <img> tag existed in the DOM — the "ready to download" gate could flip true before images had really painted, worse with more concurrent images (Rack's logo + up to 3 products). Moved onLoad to the real rendered <img> element. - Rack's product grid used CSS Grid, which html-to-image's SVG-based rasterizer doesn't reliably preserve track sizing for — silently collapsing tiles to zero height in the export even though the images had loaded. Replaced with flexbox (same visual layout), matching the technique already used everywhere else in these cards. Separately, on iOS Safari specifically: confirmed via html-to-image's own issue tracker (bubkoo/html-to-image#591, maintainer-tested on a real iPhone) that WebKit has a documented bug where capturing a subtree with large images intermittently returns them blank — a WebKit SVG-image-loader quirk present in the installed 1.11.13, not a timing bug on our side. A single retry isn't reliable; the only fix that reliably works is re-capturing until two consecutive results are byte-identical. That fix isn't released upstream yet, so replicated it directly in both share-modal.tsx and store-share-modal.tsx's download handlers — effectively free on browsers without the bug (converges on the first comparison), and should resolve it on iOS Safari. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Problem
On WebKit (iOS Safari / WKWebView), capturing a subtree that contains large images intermittently produces an output where those images are blank while text, backgrounds, and small images render fine — the long-standing class of reports around #361.
We measured this on a real device (iPhone 11, iOS 18.7, Safari 26.2) with a self-verifying fixture page: 13 fixtures (small PNG/SVG files, a data-URL image, a lazy image, a CSS
background-image, an inline SVG, two SVG logos, and four 1200×1200 incompressible-noise PNGs as data URLs), captured atpixelRatio: 4, with pixel-sampled verdicts per fixture, cold and warm:toPngtoPngThree findings worth pinning down, because they invalidate the workarounds usually suggested in the issue threads:
<img>, rendered fine on this WebKit build.Imageobjects outside the SVG context changes nothing. The only reliable signal is comparing actual capture output.Fix
An opt-in
stabilizationAttemptsoption (default1= existing behaviour, zero cost for current users). When greater than 1,toCanvasrepeats the capture until two consecutive results are identical and returns the last one, capped at the given number of attempts. All rasterizing entry points (toPng,toJpeg,toBlob,toPixelData) inherit it throughtoCanvas;toSvgis untouched.Measured cost on the device above: the heavy capture went from ~1.0 s (single attempt, blank images) to ~2.0 s (converged, complete) with
stabilizationAttempts: 3.Subtrees with genuinely dynamic content (a running clock, an animation) never produce two identical captures and will take all attempts — documented on the option.
Tests
test/spec/stabilization.spec.ts:toBlob.Related: #589 fixes the complementary WebKit failure where a capture hangs forever because
HTMLImageElement.decode()rejects under the same memory-pressure conditions.