NotesExperiment

283ms to open one view: two reasons the UX/UI gallery stuttered

In a gallery with 400 animated elements on one page, the reels view and the prompt dialog stuttered. How I found the causes with a Chrome performance trace and cut 283ms to 33–50ms.

Cards in the UX/UI gallery, with a flow chart, bar chart and bubble chart in motion

Lucian UX/UI Prompt is a gallery where you can try 400 interface elements, such as buttons, charts and forms, on a single page. Each card holds a small working demo that starts moving on its own when it scrolls into view.

The gallery has two more views. The reels view lets you flip through elements one at a time at full size, and the prompt dialog shows an element's prompt and code. On October 12, I noticed the screen hitched for a moment when either one opened. If you can feel it, it is usually over 100ms.

Measure first

Before fixing anything by feel, I recorded the moment the reels view opened with a Chrome DevTools performance trace. I took the measurements with a script that repeats the same actions in headless Chrome. It draws in software without a GPU, so the numbers come out larger than on a real device, but it is good enough for comparing before and after under the same conditions.

ActionLongest frame
Open reels (first time)283ms
Close reels167ms
Next slide in reels117ms
Open prompt dialog75ms

To hold 60fps, each frame has to finish within 16.7ms. At 283ms, the screen froze for nearly 17 frames.

Cause 1: opening the reels prepared every slide at once

The trace showed a 172ms long task at the moment the reels view opened. Inside it, style recalculation (Update Layout Tree) took 94ms and event handling 103ms, and all of it started from one function that decides the current slide.

Following the code, I found that opening the reels view built a frame for every slide and attached a demo to each one. The user sees one slide, but the browser was preparing hundreds. On top of that, the cards of the original gallery behind the reels kept animating.

I changed three things.

  • Fill only the slides near the visible one. Slides start as empty frames, and only the two slides before and after the current one get content. Slides four or more away are emptied.
  • Unload the gallery behind a moment later. The demos in the gallery behind are unloaded after the reels' opening transition ends (700ms), so heavy work does not overlap the transition.
  • Pause background animations on slides that are not visible.
Pause background animation on slides other than the current one
.g-slide:not(.is-on) .g-slide-bg { animation-play-state: paused; }
ActionBeforeAfter
Open reels (first time)283ms33–50ms
Close reels167ms17ms
Next slide in reels117ms17ms
Animations running at once2027

The number of animations running at once dropped the most. 195 things nobody could see had been moving the whole time.

Cause 2: the blur behind the prompt dialog

When the prompt dialog opens, it covers the page behind it with a blur. In CSS, that is backdrop-filter: blur(10px) on the dialog's backdrop (::backdrop). It looks nice, but dozens of demos were moving behind it.

To find which one was the cause, I changed one condition at a time.

ConditionLongest frameFrames over 50ms
As is (blur + animations behind)75ms11
Only the animations behind paused75ms6
Only the blur removed34ms0

Turning off the blur fixed it right away. The trace also showed almost no JavaScript; most of the time went to the graphics work of compositing the screen. A blur has to read every pixel behind it and mix them again on every frame. When the page behind is moving, that work repeats without end.

Instead of the blur, I covered the page with its background color at 84% opacity. The page behind is hidden well enough that the focus on the dialog feels almost the same.

A translucent cover instead of a blur
/* before */
.g-dlg::backdrop { backdrop-filter: blur(10px) saturate(170%); }
/* after */
.g-dlg::backdrop { background: color-mix(in srgb, var(--ux-page) 84%, transparent); }

A trap: :has() was slow

While the dialog was open, I also wanted to pause the demos behind it. At first I did it cleanly with CSS alone.

First attempt
body:has(dialog[open]) .g-slide-bg { animation-play-state: paused; }

But with this rule in place, opening the dialog took close to 100ms again. :has() selects a parent based on the state of its children, so every time the dialog opens or closes, the browser rechecks styles across the whole document. On a page with thousands of elements, that cost is large.

So I changed the code that opens and closes the dialog to add and remove a class on body directly.

Signal the state with a class (abridged)
dialog.addEventListener('close', () => document.body.classList.remove('has-dlg'));
// on open: document.body.classList.add('has-dlg'); dialog.showModal();
css
body.has-dlg .g-slide-bg { animation-play-state: paused; }

The final result: 33–42ms to open the dialog, 33ms to close it, and no frames over 50ms.

Summary

ProblemCauseFix
Opening reels took 283msEvery slide prepared, even unseen onesFill only two slides on each side; unload the gallery behind after the transition
Prompt dialog took 75msA blur over a moving pageAn 84% translucent cover instead of a blur
Dialog slowed back to 100ms:has() applied across the whole documentToggle a class on body

All three problems came down to the same thing: work was happening where nobody could see it. Slides off screen, demos moving behind a cover, a selector that rescanned everything on each change. Deciding what not to draw paid off more than deciding what to draw.

The numbers are from software rendering in headless Chrome, so on a real device with a GPU both before and after come out smaller. In ratio terms, opening the reels got more than 5 times faster and opening the dialog about 2 times faster.

Tool in this note

Lucian UX/UI Prompt

Try the interface. Take the prompt.

Learn more

Lucian · runs lucianLabs

I build the tools I need for working with AI agents and write down the numbers and mistakes behind them. If something here is wrong, let me know.