oficina / docs Public

Toolbar: write to the DOM only when the value changed

Paul saw a flash switching between two tabs on one document. Driven in
a real browser with one genuinely hidden tab, the content update was
innocent — the editable node was never replaced, one text node swapped,
visible 33ms after reveal. The flash was the toolbar: 23 DOM mutations
on reveal, 21 of them attribute writes on controls that were already on
screen. Recording each write's old value against its new one, 21 of 21
were redundant. Zero changed anything. Every one restated what was
already there: aria-pressed "false" -> "false", disabled "" -> "".

toolbar.mjs's sync looped every button and assigned button.disabled and
setAttribute('aria-pressed', ...) unconditionally, then unconditionally
enabled every select and every combobox and rewrote their values.
setAttribute with an identical value still invalidates style and still
records a mutation. And sync runs on EVERY ProseMirror state update, so
this was the cost of every keystroke too; the tab switch merely gathered
a page of it into one visible frame. The same measurement gave 12
mutations before the toolbar redesign and 23 after, because each control
added brought its own unconditional pair.

Two helpers, setAttr and setProp, write only when the value differs, and
every DOM write in the module goes through one of them. Two rather than
one because the kinds do not compare alike: an attribute is a string
read back with getAttribute, a property is whatever type it is and reads
back as itself. The resulting DOM is identical; only the redundant write
disappears. Nothing about why the writes exist has changed — the selects
are still rendered disabled server-side and enabled here, the combobox
is still looked up rather than captured at mount.

fixtures/toolbar.test.mjs holds the property that matters: after a state
update that changes nothing, the bar performs zero writes. It drives the
real plugin against a recording stand-in for the DOM that logs every
write WHETHER OR NOT IT CHANGES ANYTHING — recording the call rather
than the change is the whole seam, since a check comparing the bar
before and after would see nothing wrong. A second test holds the
counterweight, that everything a person can see change is still written,
so "write nothing, ever" does not pass. A third holds the rule against
the source, because the first two only drive the controls show.html
renders today and the failure mode is a control added later: nothing in
toolbar.mjs may write to the DOM outside the two helpers.

All three were watched failing first. Before the fix the recording fake
logged 37 writes on a no-op update, every one with from === to. Adding a
new unconditional setAttribute back into the loop fails the first and
third; deleting the aria-pressed write entirely fails the second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 47c908018c39a545b5edba5040a4e986117bfecd parent 1859534
4 files changed, +447 −22
  • .amadan/ci.d/10-gate +16 −0
  • fixtures/toolbar.mjs +48 −9
  • fixtures/toolbar.test.mjs +370 −0
  • internal/docs/static/{editor.d8d3d5d91b1c09c4.js => editor.4dc0a272328103ed.js} +13 −13

Intent

Attributed to this commit: 30.1 g CO₂e (3944336 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on worktree-toolbar-flash, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.

.amadan/ci.d/10-gate

Loading diff…

fixtures/toolbar.mjs

Loading diff…

fixtures/toolbar.test.mjs

Loading diff…

internal/docs/static/{editor.d8d3d5d91b1c09c4.js => editor.4dc0a272328103ed.js}

Loading diff…