perf(image): make HTMLImageElement.src setter async#920
Merged
andycall merged 1 commit intorelease/0.22.25from May 8, 2026
Merged
perf(image): make HTMLImageElement.src setter async#920andycall merged 1 commit intorelease/0.22.25from
andycall merged 1 commit intorelease/0.22.25from
Conversation
Route `img.src = url` through `SetBindingPropertyAsync` instead of the
sync `SetBindingProperty` path. The sync path forces a `FlushUICommand`
before each write and a sync FFI round-trip; for `src` neither is needed:
* The setter is fire-and-forget — JS never reads anything synchronously
out of it.
* The actual network load is async on the Dart side regardless.
* Subsequent JS reads (`img.src`, `getProperty`) call `FlushUICommand`
internally before their sync read, so they still see the just-written
value — semantics preserved.
* MutationObserver and the `attributes_` mirror in `SetBindingProperty`
are gated on `WidgetElement` only and never fired for HTMLImageElement
today, so dropping the sync path loses no observed behavior.
In profiles, 59 sync `setProperty(src)` calls during a route render burst
forced 59 `FlushUICommand` drains, each of which entered the styleRecalc
cascade that produced ~2,000 nested recalcs per drained insert
(`16,500` recalc spans / ~1.69 s self per session). Folding these writes
into the next natural flush should drop ~250 ms of JS-thread block plus
~600-800 ms of styleRecalc cascade work in the heavy-render hot zone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
img.src = urlthroughSetBindingPropertyAsyncinstead of the syncSetBindingPropertypath.FlushUICommandper write plus a sync FFI round-trip; both are unnecessary forsrcbecause the setter is fire-and-forget, the network load is async on Dart anyway, and any subsequent JS read ofimg.srcalready callsFlushUICommandinternally before its sync read so it still sees the just-written value.attributes_mirror are all preserved.Why
In profiles of the OTC mini-program, 59 sync
setProperty(src)calls in a single route render burst forced 59 separateFlushUICommanddrains. Each drain entered the styleRecalc cascade producing ~2,000 nested recalc spans per inserted node, contributing to a 16,500-span recalc total (1.69 s self) during the hot 67–69 s window with two 246 ms / 196 ms drawFrames.Folding these writes into the next natural flush should drop ~250 ms of JS-thread block plus ~600-800 ms of cascading Dart-side styleRecalc work, smoothing the worst frames in the burst.
What's preserved
img.srcgetter immediately after writeGetBindingPropertycallsFlushUICommandbefore the sync read atbinding_object.cc:311)img.src = url1; img.src = url2;(last-wins)img.getAttribute('src')afterimg.setAttribute('src', url)urlurl(unchanged path throughElementAttributes::setAttribute)img.getAttribute('src')afterimg.src = urlnull*null* (pre-existing limitation, attributes_ mirror isWidgetElement-only)* Pre-existing limitation; not introduced by this PR.
The only actual semantic change is a ≤ 1-frame delay before the network load starts on the Dart side. In practice that's smaller than today's per-write flush stalls (38 ms p95, 250+ ms tail).
Test plan
npm run build:bridge:macoscd integration_tests && npm run integration(image-related specs in particular)img.src = url; expect(img.src).toBe(url)returns the just-set value (validates the read-side flush trigger)onloadfires correctly afterimg.src = url(load lifecycle)🤖 Generated with Claude Code