Skip to content

Add ChatRoom-Svelte and ChatRoom-Angular experimental workloads - #597

Open
canova wants to merge 18 commits into
WebKit:mainfrom
canova:chatroom-svelte-angular
Open

canova wants to merge 18 commits into
WebKit:mainfrom
canova:chatroom-svelte-angular

Conversation

@canova

@canova canova commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

This PR adds ChatRoom-Svelte and ChatRoom-Angular, ports of ChatRoom-React that render the same DOM from the same fixtures. The steps move into a shared chatRoomTests array, so all three suites are measured on identical interactions. ?tags=chat-room runs them side by side.

Depends on #583, only the last five commits are new. Each port has a source commit and a separate build commit.

@netlify

netlify Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for webkit-speedometer-preview ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit e8d32b1
🔍 Latest deploy log https://app.netlify.com/projects/webkit-speedometer-preview/deploys/6af5a3566ac912e74141a84f
😎 Deploy Preview https://deploy-preview-597--webkit-speedometer-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Exercises chat-room switching in a two-pane chat UI, so timeline
teardown/rebuild cost can be profiled. Original React + Vite code with
deterministic, server-free fixtures and no third-party assets. Experimental, so
it is off by default and does not affect the official score.

SwitchRooms uses flushSync so each programmatic click commits one room switch.
flushSync stopped React 18 batching a burst of programmatic clicks into one
deferred render, but it is not what a real app does and it bypasses the
concurrent scheduler this workload exists to exercise. The switch is a plain
state update now, with the step yielding a task between clicks, which makes the
suite async.

The yield is a MessageChannel round-trip: an async step is awaited inside the
measured window, so setTimeout(0)'s clamp would be reported as workload time,
and a microtask would keep all 30 switches in one task. Driving 30 switches:
microtask 329ms, flushSync 348ms, MessageChannel 397ms, setTimeout(0) 427ms.
Chat clients open a room at its most recent message; the timeline mounted at
offset 0. A layout effect now assigns scrollTop = scrollHeight after commit, so
SwitchRooms measures mount plus the scroll to bottom.

Not flex-direction: column-reverse, which inverts scrollTop semantics in ways
browsers have disagreed on, and later steps drive scrollTop explicitly.
…ctive

Bodies were a single plain sentence, so rows were near-uniform in height and a
windowing timeline built on that would inherit an unrealistically easy height
model. They are now structured blocks -- paragraphs, code, blockquotes, lists,
with inline code, links and @mention/#room pills -- giving 48 distinct row
heights over 150 rows. Generation moves to a Math.imul integer hash so fields
derive independently from the seed.

Reaction pills, room pills and reply quotes become real controls, with state in
components so the fixtures stay immutable across iterations. The reply jump
assigns scrollTop from measured rects rather than scrollIntoView, keeping
smooth scrolling out of a timed step.

Also fixes senders never repeating, so grouping never triggered, and room pills
slugifying 20 names against 40 rooms.

SwitchRooms grows from ~245ms to ~450ms, the cost of mounting 150 rich rows.
The app was a read-only viewer. There is now a text input and a Send button
below the timeline; sending appends to the end and scrolls to bottom.

Sent messages live in Timeline state rather than the fixtures, so the generated
data stays immutable and rooms do not grow across iterations. Timestamps
continue from the room's last message rather than reading the clock. Enter is
an explicit onKeyDown, since a dispatched keydown triggers no default action.

No new step: SwitchRooms is unchanged, so this is app surface only.
Message bodies gain an image attachment and an unfurl card block, covering
about 11% of messages. Artwork is SVG data URIs built from a palette and shape
variant by a new graphics module, so the workload still ships no image assets.
The URIs are memoized on a reduced palette/variant key -- with the raw hashes
every message got a distinct gradient id, so the browser decoded 652 images
instead of 127.

Images carry explicit width and height, or a row would resize once the image
decoded and invalidate the height cache. Also fixes bullet lists repeating a
line about 13% of the time.

Row heights now span 24px to 278px with 58 distinct values, up from 229px and
48. SwitchRooms grows from ~367ms to ~442ms.
Speedometer has almost no scroll coverage: across the 32 default suites the
only scrolling inside a timed step is a single scrollIntoView. A chat timeline
is the natural vehicle for that, but only once it is windowed.

Rooms grow from 150 to 1500 messages and the timeline mounts ~15 rows. The
virtualizer is hand-rolled, since a generic one assumes a height it can know up
front and a chat row's depends on how its text wraps. The new height model
estimates, measures, caches and corrects, keeping prefix sums so an offset can
be turned back into a row index without touching the DOM. Three constraints it
has to respect -- the scroller's height lagging the model by a commit, the row
estimate having to sit under the real mean, and overflow-anchor having to be
off -- are documented at their sites.

ScrollTimeline is the new step, driving explicit offsets back through history
and then in long strides. SwitchRooms is unchanged but costs ~4.5x less.
PageElement gains scrollTop/scrollHeight/clientHeight, since scrollIntoView
cannot reach an unmounted row.
Preserving the scroll position while content is inserted above the viewport is
the chat and feed pattern nothing else in Speedometer exercises, and the one
real clients visibly get wrong. A room now opens on the newest 300 of its 1500
messages and fetches 120 more whenever a scroll lands within a viewport of the
top.

The height model grows at the front as well as the end: a row keeps its index
when older history arrives, so measured heights stay valid and only the offsets
shift, uniformly, which makes the correction exact. Anchored by hand rather
than with the browser's scroll anchoring, which would apply a second correction
for the same insertion.

LoadOlderMessages is the new step; ScrollTimeline stops one stride short of the
top so the two stay independent. Following a reply quote can now load history
too.
A profile shows a ~14ms nursery collection landing inside a timed step in three
of the thirty measured windows, and those three are the slowest iteration of
their step. The fixture build is not itself measured, but it pushes ~117MB of
permanently-live data through the nursery per iteration, and a minor GC costs
what it has to promote.

The fixture now shares what it can: bodies come from a pool of 2048 rather than
one per message, reaction rows collapse to the nine distinct values they always
had, and timestamps and initials stop being rebuilt 60,000 times. The pool is
drawn from the same seed space, so the spread of row heights is unchanged. The
per-message preview string is gone; nothing rendered it.

Building the fixtures drops from 197ms to 31ms and retains 14MB instead of
117MB, taking the unmeasured prepare phase from 335ms to 48ms per iteration.
Turning a message id back into a row index is only needed when a reply quote is
clicked, and neither a scroll nor a room switch clicks one. Building it during
render rebuilt a map over the room's whole history on every switch, inside the
timed step, where the profile puts it at 8.8% of SwitchRooms -- the largest
single piece of the app's own JavaScript in the run.

With the fixture sharing beneath it, this takes the content process from 1.04GB
of net allocation to 459MB, minor collections from 77 to 16, and the ones inside
a timed step from 14.1ms to 7.2ms on average. What is left is React's own
reconciliation filling the nursery, which is the work the steps exist to
measure.
Per-render chrome that chat clients carry and this workload had none of: a
labelled rule where the conversation crosses a day, and a marker above the
first message the reader has not seen.

Neither is a row of its own -- a row here is one message, and the height cache,
spacers and offset search are all indexed by message index -- so both are drawn
inside the row they belong to, keyed off the message array rather than the
mounted window.

Day lengths now vary between 8 and 56 messages. At a fixed size the boundary
always landed just above the newest page, so no room opened with a separator in
view; twenty of the forty now do.

Cost is noise: 100.65ms -> 105.00ms over ten Firefox iterations against a
+/-20ms spread.
Output of `npm run build` in suites-experimental/chat-room. Build artifacts
only, so it can be dropped and regenerated whenever the source commits below it
change.
Moves ChatRoom-React's steps into a chatRoomTests array, so other ports of
the workload can be measured on identical interactions.
A Svelte 5 port of ChatRoom-React that renders the same DOM from the same
fixtures.
An Angular port of ChatRoom-React that renders the same DOM from the same
fixtures.

It uses zone.js rather than zoneless change detection: zoneless Angular renders
in a setTimeout/rAF race that the steps' MessageChannel yield does not wait for
in Firefox, so interactions would coalesce and the suite would measure less work
than the other ports.

The repository's Prettier predates Angular's control flow syntax, so .prettierrc
turns off embedded formatting for this workload's TypeScript files.
@canova
canova force-pushed the chatroom-svelte-angular branch from 73ad925 to e8d32b1 Compare November 12, 2026 11:30
mstange added a commit to mozilla/Speedometer that referenced this pull request Nov 12, 2026
The earlier ChatRoom-React commits are already on sp4-exp. PR WebKit#597 changes
them by giving Vite's output files stable names. This brings
suites-experimental/chat-room up to the PR's version.
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