From harness-claude
Optimizes React SPA client-side rendering: profile with DevTools, prevent unnecessary re-renders via memoization, implement skeleton screens, leverage concurrent features for responsive UIs.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Master client-side rendering performance — SPA rendering optimization, reducing unnecessary re-renders, skeleton screen patterns, progressive rendering strategies, virtual DOM efficiency, React performance profiling, and concurrent rendering features for responsive user interfaces.
Renders React entirely client-side for interactive SPAs like dashboards, admin tools, and prototypes where SEO is unnecessary. Covers Vite setup, React Router, and performance mitigations.
Optimizes React apps using memoization (React.memo, useMemo, useCallback), code splitting, virtualization, and concurrent rendering. Use for slow components, bundle size reduction, large datasets, and re-render prevention.
Optimizes React performance using React.memo for component memoization, custom prop comparisons, and useMemo for expensive computations like filtering and sorting. Use for preventing unnecessary re-renders.
Share bugs, ideas, or general feedback.
Master client-side rendering performance — SPA rendering optimization, reducing unnecessary re-renders, skeleton screen patterns, progressive rendering strategies, virtual DOM efficiency, React performance profiling, and concurrent rendering features for responsive user interfaces.
Profile rendering with React DevTools Profiler. Identify which components render, why, and how long they take:
1. Open React DevTools → Profiler tab
2. Click "Record" → interact with the application → "Stop"
3. Examine the flame chart:
- Gray components: did not render
- Colored components: rendered (warmer = slower)
- "Why did this render?" shows the trigger
4. Look for:
- Components rendering on unrelated state changes
- Large subtrees re-rendering for small changes
- Components rendering >16ms (frame budget)
Prevent unnecessary re-renders with memoization. Use React.memo, useMemo, and useCallback strategically:
// React.memo: skip re-render when props are unchanged
const ExpensiveList = React.memo(function ExpensiveList({
items,
onItemClick,
}: {
items: Item[];
onItemClick: (id: string) => void;
}) {
return (
<ul>
{items.map(item => (
<ListItem key={item.id} item={item} onClick={onItemClick} />
))}
</ul>
);
});
// Parent: stabilize callback reference
function Dashboard() {
const [items, setItems] = useState<Item[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
// useCallback: stable reference across renders
const handleItemClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
return (
<>
<Sidebar selectedId={selectedId} />
<ExpensiveList items={items} onItemClick={handleItemClick} />
</>
);
}
Implement skeleton screens for data-loading states. Skeletons reduce perceived load time by 15-30%:
function ProductGrid() {
const { data, isLoading } = useProducts();
if (isLoading) {
return (
<div className="grid">
{Array.from({ length: 12 }, (_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>
);
}
return (
<div className="grid">
{data.map(product => <ProductCard key={product.id} product={product} />)}
</div>
);
}
Use a CSS shimmer animation (background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite) on .skeleton elements with explicit dimensions matching the loaded content to prevent layout shift.
Use concurrent rendering for responsive interactions. React 18 concurrent features prevent expensive renders from blocking user input:
import { useTransition, useDeferredValue } from 'react';
// useTransition: mark state updates as non-urgent
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
const [isPending, startTransition] = useTransition();
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // urgent: update input immediately
startTransition(() => {
// non-urgent: can be interrupted by user input
const filtered = filterResults(allData, value);
setResults(filtered);
});
}
return (
<>
<input value={query} onChange={handleSearch} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
);
}
// useDeferredValue: defer expensive re-renders
function FilteredList({ filter }: { filter: string }) {
const deferredFilter = useDeferredValue(filter);
return (
<div style={{ opacity: filter !== deferredFilter ? 0.7 : 1 }}>
<ExpensiveList filter={deferredFilter} />
</div>
);
}
Optimize list rendering. Large lists are the most common CSR performance problem:
// Key stability: use stable IDs, not array index
// Bad: items.map((item, index) => <Item key={index} ... />)
// Good: items.map(item => <Item key={item.id} ... />)
// Avoid creating new objects/arrays in render
// Bad: <List items={data.filter(d => d.active)} />
// Good:
const activeItems = useMemo(
() => data.filter(d => d.active),
[data]
);
return <List items={activeItems} />;
// For very long lists (1000+), use virtualization
// See: perf-lazy-loading skill for virtual scrolling patterns
Batch state updates for fewer renders. React 18 automatically batches state updates in all contexts:
// React 18: all state updates are batched automatically
// This triggers ONE re-render, not three:
async function handleSubmit() {
const data = await fetchData();
setItems(data.items); // batched
setTotal(data.total); // batched
setLoading(false); // batched → single re-render
}
// When you need to force a synchronous update (rare):
import { flushSync } from 'react-dom';
flushSync(() => {
setMeasurement(value); // renders immediately
});
// DOM is updated here — safe to measure
const height = ref.current.offsetHeight;
Profile with the Performance panel. Beyond React DevTools, the Chrome Performance panel shows the full picture:
1. Performance tab → Record → interact → Stop
2. Look for:
- Long Tasks (>50ms) in the Main thread
- Scripting vs Rendering vs Painting breakdown
- React commit phases (look for "React" in the call stack)
3. Common findings:
- Large component trees re-rendering: many short React frames
- Single expensive computation: one long scripting block
- Layout thrashing: alternating "Recalculate Style" and "Layout"
React's reconciliation algorithm diffs the previous and next virtual DOM trees to determine minimal DOM updates. The diffing itself is O(n) where n is the number of elements. For 1000 list items, the diff cost is ~1-5ms. The expensive part is DOM mutation: inserting, moving, or removing real DOM nodes costs ~0.1-0.5ms each. This is why stable keys are critical — they help React match elements across renders and minimize DOM mutations.
Figma uses windowed rendering plus React.memo on file cards with Zustand selector-based subscriptions, so selecting a file does not re-render the grid. The search input uses useDeferredValue to keep typing responsive. Result: 60fps scrolling through 10,000+ files with <50ms INP.
Linear achieves instant-feeling interactions via: (1) optimistic updates before server confirmation, (2) fine-grained state subscriptions so status changes re-render only that row, (3) CSS transitions instead of React-driven animations, (4) skeleton screens matching loaded layout. Result: <50ms INP for status changes and zero layout shift.
Premature memoization. Only memoize components that receive complex props and render frequently, are expensive (>5ms), or sit below frequently-changing state. Profile first, memoize second.
State in the wrong component. Lifting state too high causes entire subtrees to re-render. Keep state as close to where it is used as possible.
Creating objects in JSX props. <Child style={{ color: 'red' }} /> creates a new object every render, defeating React.memo. Extract constants or use useMemo.
Synchronous heavy computation during render. Move heavy computation (sorting, filtering 10K+ items) to a Web Worker or use useDeferredValue.