SVG DOM Update Benchmark
This page measures how fast the browser can show each new frame of an SVG
component. It runs the component directly, records sequences of real frames,
and replays each sequence through eight ways to put SVG output into the page.
<qip-play svg-inline> currently parses each frame with DOMParser and swaps
the whole tree with replaceChildren. morphdom and React instead patch the
existing nodes, so text selection, hover state and CSS transitions on
unchanged elements survive a render. This page shows what that costs or saves.
Pick a component:
| Component | Elements per frame | Scenarios |
| OpenAI vs Anthropic ARR chart | about 80 | Scale tween, hover sweep, arrow keys |
| The same chart, keyed by part | about 100 | Scale tween, hover sweep, arrow keys |
| Orders data grid: 1,000 rows, 17 rendered, keyed by order | about 190 | Scroll, sort, type to filter, hover rows |
| Date range picker: two months, keyed by date and month | about 290 | Month paging, stay preview, arrow keys |
The keyed chart draws the same chart, but wraps each part in a <g> with a
data-key and data-rev: the title, each button, the plot frame, each axis,
each series, the selection, the tooltip and the detail panel. The selected
point's larger dot and ring move into their own group, so a hover that moves
the selection leaves both series unchanged. The axes fade by the group's
opacity, so a scale tween leaves their tick marks unchanged. It uses about 25
more elements than the plain chart, and only this page uses it.
The grid and the date picker are shaped like components that React apps
commonly build by hand. Their rows, days and month panels carry a data-key
attribute. morphdom and React use it as the node key, as a React developer
would with key.
Each keyed element also carries data-rev, an FNV-1a digest of its content
that the component writes as it renders. The element's own attributes are left
out of the digest, so a grid row that only scrolls to a new position or changes
its hover fill keeps its digest. Two strategies use it:
- morphdom + data-rev still parses the whole frame, but skips the children
of any element whose
data-rev is unchanged.
- Chunks at data-key is a glue layer that never parses the whole frame. It
scans the markup string for tags, splits it at
data-key elements, and
parses only the chunks whose data-rev changed, in one DOMParser call.
Elements that only changed their own attributes are patched in place.
Chunks at structural elements uses the same glue without help from the
renderer. It splits at data-key and id elements, SVG groups, HTML
landmarks and block elements, custom elements and significant ARIA roles, and
compares each chunk's markup with the last frame's instead of a digest.
Unkeyed chunks match by position.
Try the components #
Both components run live below through <qip-play svg-inline>, the current
replaceChildren path. Click one to focus it, then use the keyboard.
The orders grid sorts when you click a column header. Type to filter,
Backspace to remove a character and Escape to clear the filter. The Up and Down
arrows, Page Up, Page Down, Home and End move the cursor.
In the date range picker, click a check-in day, then hover to preview the stay
and click a check-out day. Arrow keys move between days, Page Up and Page Down
change month, Enter or Space chooses the focused day, and Escape clears the
selection.
Recorded results #
Mean milliseconds per frame from one run in headless Chrome, 20 rounds per
scenario. Shorter bars are faster. The chart is drawn by
name-group-value-to-svg-bars.wasm.
The React columns are timed from prepared trees, so React never parses markup. The other strategies parse the markup inside the timed region.
Run the benchmark #
Running the benchmark adds a chart of your own results below the tables.
Reading the results #
- Mean ms is the cost of one frame update: parse, DOM work, and the style
and layout that the page forces after each update. It excludes paint. A 60 Hz
frame has a budget of about 16.7 ms.
- Elements created shows how much of the tree each strategy rebuilds. The
two full-replacement strategies create every element on every frame.
- Elements moved counts existing elements reinserted at a new position.
Scrolling the grid or paging the date picker keeps the surviving keyed rows
and panels in order, so both libraries remove and add at the ends and move
nothing. When a filter reorders the visible rows, morphdom moves a few of
them; React's keyed diff avoids most of those moves.
- React app, useState approximates a stateful JSX component. An
<App>
holds the frame in useState, and an input event dispatched on the SVG
calls the setter: keydown for keyboard scenarios, click for sorting and
pointermove for hover. React schedules the update by that event's
priority, without flushSync, and the time includes waiting for the commit:
a microtask for keys and clicks, a scheduler task for pointer moves. The
elements come from trees prepared before timing, so React never parses
markup. React app, useState + memo rows also renders each keyed row,
day and month through a React.memo component compared by data-rev, so a
hover re-renders only the rows it touches.
- The chunk strategies parse whole chunks, so Elements created counts every
element in a changed day or row. The plain chart has no keys, so its chunks
are its SVG groups, and most of them change in every frame. Compare it with
the keyed chart to see what keying the same drawing is worth.
- Output compares each strategy's DOM with a fresh parse of the same frame,
ignoring attribute order and style syntax.
Each round starts from the frame before the scenario, untimed, so every
strategy patches the same first change.
The components are gui/data-grid-svg.wasm and
gui/date-range-picker-svg.wasm, plus the
chart's gui/openai-anthropic-arr-svg.wasm
and its keyed copy
gui/openai-anthropic-arr-keyed-svg.wasm.
The vendored modules are built by make -C benchmarks/svg-dom-update vendor.