8.9 KiB
Performance — Animation & Runtime
How to verify and quantify the theme's runtime (animation/interaction) performance.
The essence of an animation optimization is moving work off the main thread: from per-frame Layout/Paint onto the compositor (
transform/opacity/clip-path/scale). So the single most important question this guide answers is:"Does the main thread still run
Layoutevents while the animation plays?"
Where this fits
Frontend performance splits into two unrelated buckets — don't conflate them:
| Bucket | Concern | Relevant here? |
|---|---|---|
| Load | First paint, bundle size (LCP, CSS/JS bytes) | Only for main.css size |
| Runtime | Interaction & animation smoothness (INP, dropped frames) | ✅ This doc |
Two more axes the industry tracks:
- Lab vs Field — lab is your local, repeatable profiling (DevTools, Lighthouse). Field is real users (RUM). This doc is lab-only; for field data see Preventing regressions.
- Reality check — LuCI's assets ship from the router, but rendering happens in the user's browser. The bottleneck is the client GPU/CPU. Most users open LuCI on a phone, so always profile under CPU throttle, never on a bare fast desktop.
0. Setup (one-time)
- Run the dev server (
cd .dev && pnpm dev) and open a logged-in page in Chrome/Edge, e.g.http://localhost:5173/cgi-bin/luci/admin/status/overview. - Open DevTools (
F12/Cmd+Opt+I). - Have two panels ready:
- Rendering:
Cmd+Shift+P→ typeShow Rendering. - Performance: the top tab.
- Rendering:
- Confirm you're serving the optimized CSS: Network → reload → click
main.css→ Response → searchprefers-reduced-motion. If present, it's the new build.
1. Core evidence — Performance recording (the Layout track)
This is where the real numbers come from. Generic recipe, used for every animation:
- Performance panel ⚙️ → CPU: 4× slowdown (emulates a low-end phone/tablet admin device).
- ● Record → trigger the animation once → let it finish → Stop.
- Read the timeline:
- Purple
Layout/Recalculate Stylebars = reflow. Fewer is better. - FPS green bar collapsing = dropped frames.
- Summary → Rendering time at the bottom.
- Purple
Verdict rule: watch the
Layoutrow. The ideal result is that after the first frame, the entire animation produces no newLayoutevents — it runs purely on the compositor.
Component → technique → what to check
| Component | Trigger | Technique | Pass criteria |
|---|---|---|---|
| Desktop mega-menu | Hover a top nav item with a submenu (e.g. System) | clip-path (not height) |
No continuous Layout bars while expanding (old height-anim reflowed every frame → should be ≈0) |
| Mobile submenu | Device mode (Cmd+Shift+M) → hamburger → tap a parent item |
grid-template-rows (not JS scrollHeight) |
No Layout bars, and no single forced-reflow spike on expand (old code read scrollHeight) |
Main view (#view) hover |
Desktop width, move cursor in/out of the main card repeatedly | hover shadow repaint removed | No Paint flashing during the recording (old hover shadow repainted the whole block) |
| Tooltip | Hover then mouse-out of a [data-tooltip] element |
scale + opacity transition |
scale and opacity fade together on hide — no instant size snap |
| Theme switch | Footer theme toggle | View Transitions API (not body transition-all) |
No body-wide repaint storm on the Main track |
2. Fastest visual evidence — Paint flashing (no recording)
- Rendering panel → enable Paint flashing.
- Trigger each animation; repainted regions flash green.
| Trigger | Pass criteria |
|---|---|
| Open/close mega-menu | The frosted panel should not flash green as a solid block continuously (ideally only a faint flash at the reveal edge) |
| Mobile drawer slide in/out | Drawer body doesn't flash green continuously (transform slide is repaint-free) |
| Tooltip appear | Only the small tooltip region flashes once |
| Toggle floating toolbar | Button area doesn't flash green continuously |
⚠️ Honest caveat: elements with
backdrop-blur(mega-menu panel, modal scrim) will show some green flashing while animating — that's the inherent cost of a blur layer, not a regression. Judge the reflow-class animations (height / shadow) on whether they still flash, not whether blur reaches zero flash.
3. Accessibility & fallback — prefers-reduced-motion
- Rendering panel → Emulate CSS media feature prefers-reduced-motion →
reduce. - Trigger animations:
| Check | Pass criteria |
|---|---|
| Mega-menu / drawer / tooltip | Snap into place, no transition |
| Theme switch | Instant, no crossfade |
| Page navigation | #view has no entry animation |
| Functionality | All menus/modals still open and close — nothing stuck hidden |
Reset to No emulation when done.
4. View Transitions — theme switch
- Chrome/Edge (supports View Transitions): click the footer theme toggle (device / light / dark).
- Pass: the whole page crossfades smoothly (every element transitions together).
- No first-paint flash: reload the page.
- Pass: no theme crossfade on load (the first set is guarded by
prev !== null).
- Pass: no theme crossfade on load (the first set is guarded by
- Graceful degradation: switch theme in Firefox / Safari.
- Pass: instant switch, no console errors.
5. Behavior regression (animation rework didn't break function)
- Mega-menu: hover opens, leave closes; sweeping between menus closes the old and opens the new with no residue.
- Mega-menu height: submenu content fully visible, not clipped (
--mega-menu-heightcomputed correctly). - Mobile drawer: closes via
Esc/ scrim tap / close button; scrim fades out on close. - Mobile submenu: opening B auto-closes A; reopening the drawer shows all submenus collapsed.
- Modal (e.g. Save & Apply): scrim still blurs the background; modal content stays sharp.
- Tooltip: appears on hover, disappears on leave, ~150 ms, feels responsive.
6. Before/after A/B comparison (real numbers)
Absolute numbers on a fast machine prove nothing — the delta vs master is the result.
cd /path/to/luci-theme-aurora
git stash # if you have uncommitted work
git checkout master
cd .dev && pnpm build # old assets
# Hard-refresh the browser (Cmd+Shift+R), record the mega-menu open (§1),
# note the Layout event count / Rendering time.
cd .. && git checkout perf/compositor-animations
cd .dev && pnpm build
# Record again — Layout count should drop noticeably.
The dev server re-proxies the new build automatically; hard-refresh each time to clear cache.
Pass criteria summary
| Signal | Tool | Pass criteria |
|---|---|---|
| Main-thread reflow during animation | Performance → Main track | Near-zero Layout / Recalculate Style after frame 1 |
| Dropped frames | Performance → Frames / FPS meter | No frame > 16.7 ms; steady ~60 fps under 4× throttle |
| Repaint area | Rendering → Paint flashing | Minimal green on reflow-class animations |
| Compositor offload | Rendering → Layer borders | Animated elements promoted to their own layer |
| Interaction latency (INP) | Lighthouse / Performance | No regression; improved where scrollHeight was removed |
Preventing regressions
Verifying once isn't enough — keep it from drifting back:
- CSS size budget: gate
main.csssize in CI (e.g.size-limit) so a PR that bloats the bundle fails. - Field data (optional): ship the
web-vitalslibrary to report INP/LCP/CLS from real users, and watch the P75 (not the average). - Rule of thumb for new CSS: animate only
transform/opacity/clip-path/scale. Never animateheight/width/top/box-shadow/background-color(they reflow or repaint). Avoid blankettransition-all— list explicit properties. - Accepted exception —
.cbi-progressbar: the inner bar'swidthis set via inline style by LuCI core'sProgressbarwidget, so atransform: scaleX()swap would need a JS observer to mirror that value into a custom property (plus RTL-awaretransform-origin). Given the bar updates infrequently (firmware/package install progress, not a 60fps animation), the single explicittransition-[width]is left as-is rather than adding that infrastructure.
The 3 fastest checks (when short on time)
- §1 — record a mega-menu open → is the
Layoutrow clear? - §2 — Paint flashing → does the mega-menu / mobile drawer still flash as a solid block?
- §3 — emulate
reduce→ are animations fully off and everything still functional?