Hazel reports · ^report(16) · 2026-10-15
Two fixes for a drag, measured side by side
On #2635, dragging a slider in the Livelit Playground's Full Body slide for a second and a half froze the page for about three seconds in ^report(15), and longer for a longer drag. Every value a drag passes through makes the page recalculate, and a drag passes through dozens. Two fixes were built on the way: keeping only the newest preview while one is on its way, and evaluating the livelit views in the evaluation worker instead of on the page. Measured one session at a time, earlier the same day, neither could be told apart from the drift between sessions. This report measures them again with the builds side by side, drag by drag, and with the head measured twice as a control.
Short answers. #2635 at 032fe3aca6 and two prototypes on it, in one headless Firefox 157, a tab each; ten rounds of one 1.5-second drag per tab, the order turned each round; the head in two tabs, so its difference from itself is the noise floor. The method is at the end.
- Keeping only the newest preview cuts the freeze nearly in half, with the whole program. Frozen per drag: the head 4.0 s, the fix 2.2 s; round by round the fix was lower in all 10 rounds, by 1.2 to 3.0 s (median -1.72 s), where the head against itself differed by +0.12 s, five rounds each way (Figures 1 and 2).
- It does it by sending fewer repaints, not faster ones. Of about 41.5 inputs in a drag, it sent 24.5 repaints and folded 17 into one already on its way; each repaint still froze the page about as long (the head 91 ms per input, the fix 90 ms per repaint, medians). A preview costs much less than a finished change, which
^report(15)put at about a second. The picture is final as soon after letting go as before (-0.03 s, within the noise; Figure 3). - In UX mode, the freeze is smaller and the effect hides in the noise. The head froze for about 1.5 s; the fix -0.39 s against it, but the head against itself -0.25 s, with single rounds up to two seconds apart.
- Views in the worker do not shorten the freeze, and in UX mode the picture comes later. Frozen: -0.03 s with the whole program and +0.13 s in UX mode, both within the noise. Letting go to the final picture in UX mode: +0.28 s, later in 8 of 10 rounds; the head against itself differed by +0.05 s in that run and -0.12 s in the other UX run. Likely real, but small, and from one run.
- Side by side is what made the difference visible. Measured a session at a time earlier in the day, alternating builds between sessions, the head froze 4.1 s and then 3.2 s, and the same coalescing 2.8 s and then 4.2 s: the sessions moved more than the fix did. Alternating drag by drag puts the drift on every build alike.
The freeze, round by round
A drag's moves arrive faster than Full Body's page can handle them: each preview of a dragged value is a page action, and the page recalculates for every action, which on Full Body checks the whole program for what can be reused. The fix (3eb4489124, local) keeps at most one such repaint on its way: a move that arrives while one is pending only updates the preview that repaint will show, and the next repaint may go once a frame has been painted. Fewer recalculations, so fewer freezes; each recalculation costs what it did.
These drags are longer than ^report(15)'s. Each crosses 190 to 250 of the slider's units, from near one end to near the other, where all but one of ^report(15)'s crossed 20 to 50; in the same second and a half the slider sends about 44 inputs instead of 6 to 15. ^report(15) found only 1.5 to 3 of a drag's changes could be folded, and expected the fix to matter only once each change was cheaper. On a long drag, with inputs arriving every 35 ms or so, it folds 17 of 41.5, and it matters now.
The final picture
Moving the views to the worker (4c9331d13c, local) takes their evaluation off the page, and ^report(15) counted about 0.2 to 0.5 s of page time saved per change. In a drag that does not show: the page still recalculates for every preview, and the views' work moves to the worker, which the final picture then waits for.
The numbers
| Mode | Build | Frozen (s) | Longest gap (ms) | Letting go to final picture (s) | Inputs | Frozen, minus the head, round by round | Final picture, minus the head |
|---|---|---|---|---|---|---|---|
| the whole program | #2635's head | 4.02 | 258 | 1.12 | 44 | — | — |
| the whole program | the head again (A/A) | 4.17 | 274 | 1.22 | 44.5 | +0.12 (5 lower, 5 higher) | +0.08 (3 lower, 7 higher) |
| the whole program | newest preview only | 2.19 | 234 | 1.08 | 41.5 | -1.72 (10 lower, 0 higher) | -0.03 (6 lower, 4 higher) |
| UX mode | #2635's head | 1.55 | 300 | 1.25 | 47.5 | — | — |
| UX mode | the head again (A/A) | 1.11 | 266 | 1.07 | 48 | -0.25 (7 lower, 3 higher) | -0.12 (9 lower, 1 higher) |
| UX mode | newest preview only | 1.04 | 284 | 1.12 | 49.5 | -0.39 (7 lower, 3 higher) | +0.04 (4 lower, 6 higher) |
| the whole program | #2635's head | 4.25 | 250 | 1.11 | 44.5 | — | — |
| the whole program | the head again (A/A) | 4.42 | 284 | 1.12 | 44 | +0.26 (3 lower, 7 higher) | -0.00 (5 lower, 5 higher) |
| the whole program | views in the worker | 4.11 | 224 | 1.15 | 45 | -0.03 (5 lower, 5 higher) | +0.07 (4 lower, 6 higher) |
| UX mode | #2635's head | 2.20 | 308 | 1.20 | 46 | — | — |
| UX mode | the head again (A/A) | 1.94 | 325 | 1.25 | 46 | +0.12 (3 lower, 7 higher) | +0.05 (4 lower, 6 higher) |
| UX mode | views in the worker | 2.03 | 250 | 1.58 | 46 | +0.13 (4 lower, 6 higher) | +0.28 (2 lower, 8 higher) |
Medians of ten drags per build after one warm-up each. The differences are round by round, each build against the head's drag in the same round.
What next
- Offer keeping only the newest preview (
3eb4489124) to #2635, and to #2597, where the same page loop runs. - Park views in the worker for drags: they do not shorten a drag's freeze, and they make UX mode's final picture later.
- Make each change cheaper, now that there are fewer of them:
^report(15)'s first step, skipping the whole-program reuse check for a dragged value's preview. - Measure UX mode with more rounds, or longer drags, to find out whether coalescing helps there too; ten rounds were not enough against its noise.
How this was put together
- Builds: #2635 at
032fe3aca6(a dev build); keeping only the newest preview,3eb4489124on it, with2db6ee4c30's counters of repaints sent and coalesced; views in the worker,4c9331d13con it (the prototype's third experiment: whole programs fold the livelit views into the worker's run). Both prototypes are local branches. - Interleaving: one headless Firefox 157 over WebDriver, one tab per build, all three loaded at once on Full Body: the head twice, served under two host names (two origins, so each tab keeps its own saved state), and the prototype from its own server. One warm-up drag per tab, then ten rounds; in each round every tab makes one drag, the order of the tabs turned round from round to round (and reversed every other round), the tab brought to the front before its drag. Two runs per prototype: the whole program on screen (C), and UX mode with only the slider and the picture shown (B).
- The drag and its measures: as
^report(15)'s: WebDriver pointer actions, down, 15 moves of 100 ms, up, to a value new to that tab, alternately near the top and near the bottom of the slider, so each drag crosses 190 to 250 of its units (all but one of^report(15)'s crossed 20 to 50); a requestAnimationFrame loop records every frame from the press until the picture differs from before the drag, polled every 50 ms after the release; frozen time is the sum of frame gaps over 50 ms. - The numbers, for programs too: every drag's readings are in this page's
<script type="application/json" id="report-data">element and in016/data/, with the driver (ab_drag.py) and this page's generator in016/drivers/.