Interface: Performance
Rule
Principles
- MUSTMeasure reliably (disable extensions that skew runtime)
- MUSTTrack re-renders (React DevTools/React Scan)
- MUSTBatch layout reads/writes — avoid reflows/repaints
- MUSTMutations (
POST/PATCH/DELETE) < 500ms - MUSTVirtualize large lists (
@tanstack/react-virtualorvirtua) - MUSTPreload above-fold images; lazy-load rest
- SHOULDTest iOS Low Power Mode and macOS Safari
- SHOULDPrefer uncontrolled inputs; make controlled loops cheap
CSS
- SHOULDAvoid large
blur()values (GPU-heavy) - SHOULDReplace blurred rectangles with radial gradients
- SHOULD
transform: translateZ(0)sparingly for GPU layer promotion - SHOULDToggle
will-changeonly during scroll, then remove
CSS Variables
- NEVERAnimate 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)
- NEVERInterleave DOM reads and writes (read-write-read-write)
- MUSTBatch 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
- MUSTDisable transitions during theme change (see
animation.md)
Hydration & Refresh
- MUSTNo flash of wrong content on page refresh for interactive components (tabs, toggles, accordions, theme)
- MUSTPersist client state in
localStorage/sessionStorageand read before first render - MUSTSet 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;
}
- SHOULDUse proper SSR hydration — match server/client initial state
Video & Media
- MUSTPause/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
- SHOULDRefs for real-time DOM updates that bypass render (mouse position, scroll)
- SHOULDDetect/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>