Skip to content

perf(image): make HTMLImageElement.src setter async#920

Merged
andycall merged 1 commit intorelease/0.22.25from
feat/async-image-src
May 8, 2026
Merged

perf(image): make HTMLImageElement.src setter async#920
andycall merged 1 commit intorelease/0.22.25from
feat/async-image-src

Conversation

@andycall
Copy link
Copy Markdown
Member

@andycall andycall commented May 7, 2026

Summary

  • Route img.src = url through SetBindingPropertyAsync instead of the sync SetBindingProperty path.
  • The sync path forced a FlushUICommand per write plus a sync FFI round-trip; both are unnecessary for src because the setter is fire-and-forget, the network load is async on Dart anyway, and any subsequent JS read of img.src already calls FlushUICommand internally before its sync read so it still sees the just-written value.
  • Read-after-write semantics, MutationObserver behavior (which never fired for HTMLImageElement to begin with), and the 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 separate FlushUICommand drains. 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

read scenario today after this PR
img.src getter immediately after write flush + sync read returns new value flush + sync read returns new value (GetBindingProperty calls FlushUICommand before the sync read at binding_object.cc:311)
img.src = url1; img.src = url2; (last-wins) last value wins on Dart immediately last value wins on Dart at next flush
img.getAttribute('src') after img.setAttribute('src', url) returns url returns url (unchanged path through ElementAttributes::setAttribute)
img.getAttribute('src') after img.src = url returns null* returns null* (pre-existing limitation, attributes_ mirror is WidgetElement-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

  • Build bridge: npm run build:bridge:macos
  • Existing image tests still pass: cd integration_tests && npm run integration (image-related specs in particular)
  • New regression check: img.src = url; expect(img.src).toBe(url) returns the just-set value (validates the read-side flush trigger)
  • Image onload fires correctly after img.src = url (load lifecycle)
  • Re-capture profile from same flow that produced 59-call burst; confirm count drops and styleRecalc cascade shrinks

🤖 Generated with Claude Code

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
use-case Error Error May 7, 2026 8:21am
vue_usecase Error Error May 7, 2026 8:21am

Request Review

@andycall andycall merged commit cf59d0a into release/0.22.25 May 8, 2026
5 of 10 checks passed
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