SuperAPI AI Directory

perf

Design & creation

View repository

Updated 10-15 19:53

About this skill

Interface Performance

Fast UI is mostly work you do not do. Do not render nodes nobody can see, do not ask the browser to watch properties that never change, do not animate anything the compositor cannot own, do not fetch at request time what you could build once, and do not move the layout after the user has started reading it. Almost all jank is self-inflicted, and the fix is almost always deleting or deferring work rather than adding cleverness. This skill is entered by a measurement, not by a feeling: if a stopwatch, a profile or a Web Vitals number says it is slow, you are in the right place — if it merely feels wrong, the duration and the easing are motion's to fix and nothing here will help. Never optimise before profiling the actual device.

Adopt the project's existing performance tooling before adding any. If it already has a virtualizer, an image component, a bundle analyzer or a Vitals reporter, use that one — a second virtualization library is worse than an unvirtualized list, because now two things measure the same rows. Check the styling system too: a Tailwind project needs the arbitrary-value transition form, not a hand-written stylesheet beside it. If the framework ships a route-level image or font primitive, it already handles preloading, sizing and format negotiation better than a hand-rolled <img>.

Topic Reference
A number to hold the work to Open references/budgets.md when you need the target to measure against — frame time, Web Vitals thresholds, list sizes, image and bundle weights.
A symptom with no obvious cause Open references/diagnosis.md when something janks and you do not yet know why — it maps each symptom to the panel, the setting, and what the trace looks like when you have found it.

Core Principles

  1. Name the properties that transition; never transition: all. With all, every style change becomes a candidate animation — the browser tracks the whole style object, properties you never meant to tween start tweening, and the engine loses the optimisations a narrow, known list allows. Write transition-property: transform, opacity. Tailwind's bare transition compiles to transition-property: all; use transition-transform or transition-[opacity,filter]. Exception: none — a Tier 0 prohibition; the shorthand's convenience never covers its cost.

  2. Animate only what the compositor owns. transform, opacity, filter and clip-path skip layout and paint. Animating height, width, padding, margin, top or left runs the full pipeline on every frame. Expand panels with transform: scaleY() or a clip, or measure once and animate the transform. Exception: filter: blur() is compositor-owned but genuinely expensive — keep it under 20px and off frequently-animating elements, especially in Safari.

  3. Reserve will-change for a stutter you have observed. The browser creates the compositor layer when motion begins, and that just-in-time promotion occasionally eats the opening frame; will-change: transform requests it up front and fixes exactly that. It does nothing for paint- or layout-bound properties, and each layer holds real GPU memory. Exception: remove it after the animation on any element that exists in quantity — 200 permanently promoted rows cost more than the hitch they prevented.

  1. Virtualize above roughly 50 rows. Below that the DOM is cheaper than the bookkeeping; above it, scroll cost grows with total rows rather than visible ones. Use the project's virtualizer, give it a estimateSize that is close, and measure real rows rather than trusting the estimate. Exception: content that must be findable with the browser's own Ctrl+F, printable, or crawlable — paginate instead of virtualizing.

  2. Keep per-frame values out of the render cycle. A useState write per frame is a re-render storm at pointer speed. Drive frame values through a ref and write to the DOM directly inside requestAnimationFrame. Exception: a value that changes once per interaction rather than once per frame belongs in state, where it is easier to reason about.

  3. CSS custom properties inherit, so never animate one on a parent (Emil Kowalski). Setting --swipe-amount on a container recalculates styles for every descendant; in a drawer this starts dropping frames at around 20 items. Write element.style.transform on the one element that moves. Exception: a variable set once at the end of an interaction rather than per frame is fine anywhere.

  4. Framer Motion's x, y and scale shorthands are not hardware-accelerated (Emil Kowalski) — they run requestAnimationFrame on the main thread, so they drop frames precisely when the browser is busy loading. Use the full string: animate={{ transform: "translateX(100px)" }}. Vercel's dashboard tab animation dropped frames during page loads until it moved to CSS. The general rule follows: CSS for predetermined animation, JS for dynamic and interruptible animation. Exception: verify it in a profile on your own version before rewriting a working component — if the shorthand composites in your build, keep the readable form.

  5. Reserve the space before the content arrives. Layout shift is the one performance defect users experience as broken rather than slow. Set explicit dimensions or aspect-ratio on every image and video, reserve async regions with skeletons, use font-variant-numeric: tabular-nums on numbers that change, never change font weight on hover, and preload above-the-fold fonts so text does not reflow when they land. Exception: content whose height genuinely cannot be known — reserve a minimum and let it grow downward, never above the reading position.

  6. Stop work that is off-screen. Pause looping animations, video and polling when the element is not visible, via IntersectionObserver or content-visibility: auto on long off-screen sections. Preload only what is above the fold; everything below gets loading="lazy". Exception: content-visibility: auto breaks in-page Ctrl+F and anchor scrolling in some engines — keep it off prose.

  7. Ship less JavaScript before making JavaScript faster. Split at the route, import icons individually rather than the set, render static content at build time, and apply the replicate-vs-install test before adding a dependency (pick-library owns that call). A component that is 40 lines of CSS does not need a 30 kB library. Exception: correctness-critical dependencies — date/time, currency, internationalisation, sanitisation — where writing your own is the more expensive mistake.

  8. Theme switches must not animate. Flipping themes with transitions live makes every colour tween independently, which reads as a smear. Add a no-transitions class, swap the theme attribute, remove the class after a double requestAnimationFrame. Exception: a deliberate designed crossfade — one animation on one overlay, not every property on every node.

Smells and Fixes

Smell Fix
transition: all or Tailwind's bare transition Name the properties: transition-[transform,opacity]
height animated to expand a panel transform: scaleY(), a clip, or measure once and animate the transform
will-change on every card "for performance" Remove; add it back only where a profile showed a first-frame hitch
blur(40px) on a moving element Under 20px, or move the blur to a static parent
2,000-row table with choppy scroll Virtualize
useState inside a pointermove or scroll handler Ref plus direct DOM write inside requestAnimationFrame
setProperty('--x') on a list container per frame element.style.transform on the single moving element
<motion.div animate={{ x }}> in a busy view animate={{ transform: "translateX(100px)" }}, or CSS
<img> with no width/height or aspect-ratio Set both; the layout shift is the defect, not the load time
Fonts loaded without preload <link rel="preload" as="font" crossorigin>; check a cold, throttled load
Whole icon set imported for four icons Per-icon imports; re-read the analyzer output

Output Format

Report every finding with its measurement attached: what was measured → on what device and throttle → the number before → the cause in the trace → the fix → the number after. A finding without a before-and-after number is an opinion, and it will be argued with. Rank by user-visible cost: layout shift and input delay first, then dropped frames during interaction, then load weight.

Checklist

  • No transition: all and no bare Tailwind transition
  • Animations touch only transform, opacity, filter, clip-path; no blur above 20px on moving elements
  • will-change only where a profile showed a first-frame stutter, and removed afterwards
  • Lists past ~50 rows are virtualized with one virtualizer
  • No per-frame useState; no per-frame custom-property writes on a parent
  • Framer Motion shorthands checked in a profile, or replaced with the transform string
  • Every image and video has dimensions or aspect-ratio; async regions have skeletons
  • Changing numbers use tabular-nums; hover never changes font weight
  • Above-fold fonts and images preloaded; everything below is lazy; off-screen work paused
  • Static content generated at build time; bundle checked after adding any dependency
  • Theme switching disables transitions for the swap
  • Profiled on a mid-range device or with CPU throttling, not on the development machine