Skip to content

OS-83133: opt-in synchronous React mounts via flushSync predicate#3

Draft
ilyakogan wants to merge 3 commits into
masterfrom
OS-83133-sync-mount
Draft

OS-83133: opt-in synchronous React mounts via flushSync predicate#3
ilyakogan wants to merge 3 commits into
masterfrom
OS-83133-sync-mount

Conversation

@ilyakogan

@ilyakogan ilyakogan commented Jun 1, 2026

Copy link
Copy Markdown

Summary

Jira ticket: https://optibus.atlassian.net/browse/OS-83133

  • Adds setSyncMountPredicate (in addon/utils/sync-mount.js) so host apps can opt specific components into synchronous React 18 mounts. When the predicate returns true for a resolved component name, that mount wraps root.render() in flushSync, so all matching siblings mount in the same task and the browser paints once instead of producing a visible cascade across roots.
  • Stashes parsedName.fullNameWithoutType as _resolvedName on the wrapped ReactComponent subclass during resolution, and prefers it over _reactComponent at render time. Without this, the predicate only ever saw a name for {{react-component "foo"}} positional invocations; for the common resolver-driven path ({{my-react-comp}}, PascalCase, DI-registered components) _reactComponent is always undefined and the feature never engaged.

Local results (chronos, optivelo)

Tooltip with 6 React roots (WarningsSection, TripIdSection, ServiceIdSection, VehicleTypesSection, TaskTooltipLowerPart, TooltipButtons), measured via [tt-perf] per-component logs.

Initial open — total time until last root committed

Before After
Total 958 ms 435 ms
Per-root cycle (render → committed) ~120 ms ~52 ms
UX 6 staircased paints over ~1 s one block, single paint at the end

Add-trip mid-flow (3 roots appearing after route selection)

Before After
Total (first render → last committed) 431 ms 315 ms

Trace excerpts (before / after) — last root's committed timestamps from the same tooltip:

Before:  TooltipButtons committed (passive-effect)  +958.4 ms
After:   TooltipButtons committed (passive-effect)  +435.2 ms

Numbers are from a local chronos dev build with optivelo.

ilyakogan added 3 commits June 1, 2026 18:24
React 18's createRoot().render() yields between roots, so a template
that mounts many siblings via {{[name]}} produces a visible mount
cascade (each root's commit + paint + passive-effect completes before
the next root's render starts).

Host apps can now register a predicate via setSyncMountPredicate; when
it returns true for a resolved component name, that mount wraps the
.render() call in flushSync so all matching siblings mount in the same
task and the browser paints once.
shouldSyncMount was reading _reactComponent, which is only populated
for {{react-component foo}} positional invocations and is undefined
for the common resolver-driven path ({{my-react-comp}} /
DI-registered components). Stash parsedName.fullNameWithoutType as
_resolvedName on the wrapped subclass at resolution time, and prefer
it over _reactComponent at render time so the predicate sees a stable
name in every invocation style.
Explain at the call site why we have two render paths (concurrent
yielding vs. single-task sync) and what _resolvedName is for, so a
reader who hasn't internalised React 18's scheduler / Ember's
resolver flow can follow what's happening without leaving the file.
@ilyakogan
ilyakogan marked this pull request as ready for review June 1, 2026 15:54
@ilyakogan
ilyakogan requested review from elad661 and orengriffin June 1, 2026 15:54
@ilyakogan
ilyakogan marked this pull request as draft June 4, 2026 11:57
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