Skip to content

perf: create the host MutationObserver only on the iOS native path - #43

Merged
joeblau merged 3 commits into
mainfrom
perf/38-ios-only-observer
Oct 21, 2026
Merged

joeblau merged 3 commits into
mainfrom
perf/38-ios-only-observer

Conversation

@joeblau

@joeblau joeblau commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #38

What

  • The host MutationObserver now lives in its own committed (layout-phase) effect keyed on [native]. Vibration and desktop targets construct zero observers. The click-time hostDisabled() check on the vibration path is unchanged.
  • blockedHost starts true and is synchronized in the layout phase when native turns on, so an actionable overlay is never committed over a host that is disabled, aria-disabled, or inside inert, including on the commit where native becomes true.
  • Redundant blockedHost updates are skipped (for example the observer seeing the overlay's own insertion), so the iOS path doesn't get an extra commit from this change (3 commits per mount, same as main; goes to 2 once Resolve iOS routing with useSyncExternalStore to cut iOS mount renders from 3 to 1 #39 lands, measured on a combined build).
  • New tests: no observer on the vibration path; one observer on iOS, disconnected on unmount; initially disabled / aria-disabled custom hosts and hosts under an inert ancestor never commit an overlay (checked at every Profiler commit, React 18 and 19); dynamic inert toggles.

Implemented with the Codex CLI on the shared box; I added the redundant-update guard and verified by hand.

Benchmark (npm run bench, same box, interleaved runs)

The box was shared with other agents during measurement, so timings are noisy; the ranges show every run.

Metric main 78ec2f2 (5 runs) this PR (3 runs)
Observers/instance (vibration / iOS / desktop) 1 / 1 / 1 0 / 1 / 0
Vibration client mount, 1,000 (median ms) 39.1–48.7 (median of runs 43.0) 31.8–38.9 (median of runs 32.5, about −24%)
Vibration client mount, 100 (median ms) 8.0–9.5 (8.7) 6.0–6.4 (6.1, about −30%)
iOS client mount, 1,000 (median ms) 120–151 (129) 124–147 (127, unchanged as expected)
iOS commits / renders per instance 3 / 3 3 / 3
Full ESM gzip bytes 4,514 4,562 (+48 B)

Verification

  • npm run lint, typecheck (ESM+CJS+DTS build), coverage (140 tests, 100% statements/branches/functions/lines), size, pack:check pass locally.
  • SSR: effects don't run on the server; server markup is unchanged (blockedHost only matters when native is true, which is false on the server).
  • Host actions, focus and keyboard activation unchanged (existing tests pass untouched).

Device-testing notes

These are simulated-browser numbers (Node 24, production React, jsdom with stubbed navigator), not physical-device measurements. No physical device was used. Physical iPhone/Android/iPadOS/VoiceOver certification remains tracked in #9.

@joeblau
joeblau force-pushed the perf/38-ios-only-observer branch from aefa5b1 to 4204d19 Compare October 21, 2026 00:47
@joeblau
joeblau merged commit 1ea44a1 into main Oct 21, 2026
7 checks passed
@joeblau
joeblau deleted the perf/38-ios-only-observer branch October 21, 2026 00:55
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.

Create the host MutationObserver only on the iOS native path

1 participant