op-packages/luci-theme-aurora/.dev/docs/PERFORMANCE.md
github-actions[bot] 2751752d86 🌴 Sync 2026-06-12 00:46:55
2026-06-12 00:46:55 +08:00

183 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `Layout` events 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](#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)
1. 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`.
2. Open DevTools (`F12` / `Cmd+Opt+I`).
3. Have two panels ready:
- **Rendering**: `Cmd+Shift+P` → type `Show Rendering`.
- **Performance**: the top tab.
4. Confirm you're serving the optimized CSS: **Network** → reload → click `main.css`
**Response** → search `prefers-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:
1. Performance panel ⚙️ → **CPU: 4× slowdown** (emulates a low-end phone/tablet admin device).
2. ● Record → trigger the animation once → let it finish → Stop.
3. Read the timeline:
- **Purple `Layout` / `Recalculate Style` bars** = reflow. Fewer is better.
- **FPS green bar** collapsing = dropped frames.
- **Summary → Rendering** time at the bottom.
> **Verdict rule:** watch the `Layout` row. The ideal result is that **after the first frame,
> the entire animation produces no new `Layout` events** — 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)
1. Rendering panel → enable **Paint flashing**.
2. 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`
1. Rendering panel → **Emulate CSS media feature prefers-reduced-motion**`reduce`.
2. 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
1. **Chrome/Edge** (supports View Transitions): click the footer theme toggle (device / light / dark).
- Pass: the **whole page** crossfades smoothly (every element transitions together).
2. **No first-paint flash:** reload the page.
- Pass: **no** theme crossfade on load (the first set is guarded by `prev !== null`).
3. **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-height` computed 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.**
```bash
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.css` size in CI (e.g. [`size-limit`](https://github.com/ai/size-limit))
so a PR that bloats the bundle fails.
- **Field data (optional)**: ship the [`web-vitals`](https://github.com/GoogleChrome/web-vitals)
library 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 animate `height` / `width` / `top` / `box-shadow` / `background-color` (they reflow or
repaint). Avoid blanket `transition-all` — list explicit properties.
- **Accepted exception — `.cbi-progressbar`**: the inner bar's `width` is set via inline style
by LuCI core's `Progressbar` widget, so a `transform: scaleX()` swap would need a JS observer
to mirror that value into a custom property (plus RTL-aware `transform-origin`). Given the bar
updates infrequently (firmware/package install progress, not a 60fps animation), the single
explicit `transition-[width]` is left as-is rather than adding that infrastructure.
---
## The 3 fastest checks (when short on time)
1. **§1** — record a mega-menu open → is the `Layout` row clear?
2. **§2** — Paint flashing → does the mega-menu / mobile drawer still flash as a solid block?
3. **§3** — emulate `reduce` → are animations fully off *and* everything still functional?