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.

  1. 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).
  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).
  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.
  4. 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.
  5. 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.
Frozen during one 1.5-second drag (s): median of 10, with fastest to slowest0 s1 s2 s3 s4 s5 sThe whole program: the run with newest preview only#2635's head4.02the head again (A/A)4.17newest preview only2.19The whole program: the run with views in the worker#2635's head4.25the head again (A/A)4.42views in the worker4.11UX mode: the run with newest preview only#2635's head1.55the head again (A/A)1.11newest preview only1.04UX mode: the run with views in the worker#2635's head2.20the head again (A/A)1.94views in the worker2.03
Figure 1. Frozen time (frame gaps over 50 ms) in one 1.5-second drag, each build's median of ten with its fastest and slowest, in the two runs per mode. The head appears twice in each run (the second, faded, is the A/A copy). Numbers in the table.

The freeze, round by round

Frozen during a drag: each build minus the head, round by round-3 s-2.5 s-2 s-1.5 s-1 s-0.5 s+0 s+0.5 s+1 s+1.5 sthe whole program: the head again (A/A)in the newest preview only run+0.12 sthe whole program: newest preview only-1.72 sthe whole program: the head again (A/A)in the views in the worker run+0.26 sthe whole program: views in the worker-0.03 sUX mode: the head again (A/A)in the newest preview only run-0.25 sUX mode: newest preview only-0.39 sUX mode: the head again (A/A)in the views in the worker run+0.12 sUX mode: views in the worker+0.13 seach dot one round: the build's drag minus the head's drag in that round; the black bar the median; left of the dashed line is better
Figure 2. Each dot is one round: that build's frozen time minus the head's in the same round. The A/A rows (the head against itself, faded) show how far two copies of the same build land apart. With the whole program, keeping only the newest preview is left of zero in every round; nothing else is clearly off zero.

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

From letting go to the final picture: each build minus the head-1 s-0.5 s+0 s+0.5 s+1 sthe whole program: the head again (A/A)in the newest preview only run+0.08 sthe whole program: newest preview only-0.03 sthe whole program: the head again (A/A)in the views in the worker run-0.00 sthe whole program: views in the worker+0.07 sUX mode: the head again (A/A)in the newest preview only run-0.12 sUX mode: newest preview only+0.04 sUX mode: the head again (A/A)in the views in the worker run+0.05 sUX mode: views in the worker+0.28 seach dot one round: the build's drag minus the head's drag in that round; the black bar the median; left of the dashed line is better
Figure 3. Letting go to the final picture, each build minus the head, round by round. Only views in the worker in UX mode lean off zero: later by about a quarter of a second in eight of ten rounds, with one round far each way.

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

ModeBuildFrozen (s)Longest gap (ms)Letting go to final picture (s)InputsFrozen, minus the head, round by roundFinal picture, minus the head
the whole program#2635's head4.022581.1244——
the whole programthe head again (A/A)4.172741.2244.5+0.12 (5 lower, 5 higher)+0.08 (3 lower, 7 higher)
the whole programnewest preview only2.192341.0841.5-1.72 (10 lower, 0 higher)-0.03 (6 lower, 4 higher)
UX mode#2635's head1.553001.2547.5——
UX modethe head again (A/A)1.112661.0748-0.25 (7 lower, 3 higher)-0.12 (9 lower, 1 higher)
UX modenewest preview only1.042841.1249.5-0.39 (7 lower, 3 higher)+0.04 (4 lower, 6 higher)
the whole program#2635's head4.252501.1144.5——
the whole programthe head again (A/A)4.422841.1244+0.26 (3 lower, 7 higher)-0.00 (5 lower, 5 higher)
the whole programviews in the worker4.112241.1545-0.03 (5 lower, 5 higher)+0.07 (4 lower, 6 higher)
UX mode#2635's head2.203081.2046——
UX modethe head again (A/A)1.943251.2546+0.12 (3 lower, 7 higher)+0.05 (4 lower, 6 higher)
UX modeviews in the worker2.032501.5846+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

  1. Offer keeping only the newest preview (3eb4489124) to #2635, and to #2597, where the same page loop runs.
  2. Park views in the worker for drags: they do not shorten a drag's freeze, and they make UX mode's final picture later.
  3. 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.
  4. 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