Interface: Performance
Rule
Principles
- MUST: Measure reliably (disable extensions that skew runtime)
- MUST: Track re-renders (React DevTools/React Scan)
- MUST: Batch layout reads/writes — avoid reflows/repaints
- MUST: Mutations (
POST/PATCH/DELETE) < 500ms - MUST: Virtualize large lists (
@tanstack/react-virtualorvirtua) - MUST: Preload above-fold images; lazy-load rest
- SHOULD: Test iOS Low Power Mode and macOS Safari
- SHOULD: Prefer uncontrolled inputs; make controlled loops cheap
CSS
- SHOULD: Avoid large
blur()values (GPU-heavy) - SHOULD: Replace blurred rectangles with radial gradients
- SHOULD:
transform: translateZ(0)sparingly for GPU layer promotion - SHOULD: Toggle
will-changeonly during scroll, then remove
CSS Variables
- NEVER: Animate global CSS variables — triggers style recalc on ALL descendants (F-Tier)
- CSS variables ALWAYS trigger paint, even inside
opacity: var(--x) - If unavoidable, use
@property { inherits: false }to prevent cascade
Thrashing (F-Tier)
- NEVER: Interleave DOM reads and writes (read-write-read-write)
- MUST: Batch all reads, then all writes
// Bad: Thrashing
element.style.width = "100px"
const width = element.offsetWidth // Forces layout
element.style.width = width * 2 + "px"
// Good: Batched
const width = element.offsetWidth // Read first
element.style.width = width * 2 + "px" // Then write
Theme Switching
- MUST: Disable transitions during theme change (see
animation.md)
Hydration & Refresh
- MUST: No flash of wrong content on page refresh for interactive components (tabs, toggles, accordions, theme)
- MUST: Persist client state in
localStorage/sessionStorageand read before first render - MUST: Set initial state server-side or use CSS to prevent flash:
// Read persisted state before render to avoid flash
const [activeTab, setActiveTab] = useState(() => {
if (typeof window === 'undefined') return 'default';
return localStorage.getItem('activeTab') ?? 'default';
});
/* CSS-only: hide content until JS hydrates to prevent flash */
[data-hydrated="false"] .interactive-content {
visibility: hidden;
}
- SHOULD: Use proper SSR hydration — match server/client initial state
Video & Media
- MUST: Pause/unmount off-screen videos (especially iOS)
- MUST:
muted playsinlinefor iOS autoplay:
<video autoplay loop muted playsinline>
<source src="video.mp4" type="video/mp4">
</video>
React
- SHOULD: Refs for real-time DOM updates that bypass render (mouse position, scroll)
- SHOULD: Detect/adapt to device capabilities and network conditions
Motion/Framer Motion
Motion uses WAAPI (S-Tier) for most animations. However, "independent transforms" (x, y, rotate, scale) use a main-thread approach (A-Tier).
// S-Tier: WAAPI, compositor thread
<motion.div animate={{ transform: "translateX(100px)" }} />
<motion.div animate={{ opacity: 1 }} />
// A-Tier: Main thread (independent transforms)
<motion.div animate={{ x: 100 }} />
For performance-critical transforms, prefer string syntax to ensure WAAPI.
Long Lists Without Virtualization
When full virtualization (@tanstack/react-virtual) isn't feasible, use CSS content-visibility:
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px; /* estimated height */
}
This lets the browser skip rendering off-screen items. Simpler than JS virtualization, works for moderate lists (100-500 items).
Hydration Mismatches
For content that legitimately differs between server and client (timestamps, locale-dependent text), suppress the warning:
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>