| name | frontend-review-performance |
| description | Use when reviewing React rendering performance โ profiler-first diagnosis, memo/useCallback/useMemo correctness, virtual scroll, useTransition/useDeferredValue, and canvas/WebGL separation for data-heavy UIs. Covers checklist 24-rendering-performance.md. |
Frontend Review โ Rendering Performance
You are reviewing the rendering performance of a React frontend. The most common AI-generated problems are: applying memo / useCallback / useMemo everywhere without measuring (or never at all), missing virtual scroll on large lists, and Context changes re-rendering unrelated components.
Procedure
- Check
package.json for performance-related packages (@tanstack/react-virtual, react-window, @welldone-software/why-did-you-render, etc.).
- Grep for existing memo usage:
grep -rn "React\.memo\|useMemo\|useCallback" src/ --include='*.tsx' --include='*.ts' | wc -l
grep -rn "useVirtualizer\|FixedSizeList\|VariableSizeList" src/ --include='*.tsx' | wc -l
grep -rn "useTransition\|useDeferredValue\|startTransition" src/ --include='*.tsx' | wc -l
- Find the largest list-rendering components (look for
.map( on arrays with no size guard).
- Look for Context providers that change frequently and might cause wide re-renders.
- For
iot-ops / map / chart apps: check whether heavy rendering is in React state or in a canvas/WebGL ref.
Profiler-First Principle
Do not recommend memo / useCallback / useMemo without first profiling. Premature memoization adds cognitive overhead and can slow things down (each hook has a cost).
When writing the report, prefix every optimization recommendation with: "After profiling confirms X re-renders per interaction, consider Y."
Memoization Correctness
When memoization IS present (or being recommended), check for these common mistakes:
React.memo
- Is
React.memo applied to components that receive stable props from their parents?
- Is the parent passing new object/array/function references on every render (negating memo)?
<List items={data.filter(x => x.active)} />
const activeItems = useMemo(() => data.filter(x => x.active), [data]);
<List items={activeItems} />
useCallback
- Is
useCallback used when passing callbacks to memo-wrapped children?
- Are dependency arrays accurate (no missing or unnecessary deps)?
<Button onClick={() => handleDelete(id)} />
const handleDeleteClick = useCallback(() => handleDelete(id), [id, handleDelete]);
<Button onClick={handleDeleteClick} />
useMemo
- Is
useMemo applied to expensive computations (filter/sort/aggregate on large arrays), not trivial ones (string concat, boolean check)?
- Are dependency arrays correct?
Virtual Scroll
For lists with 100+ items, virtual scroll is almost always necessary for acceptable performance.
Recommended: @tanstack/react-virtual (works with any layout, no CSS constraints).
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
{rowVirtualizer.getVirtualItems().map(vItem => (
<div key={vItem.key} style={{ position: 'absolute', top: vItem.start, height: vItem.size }}>
<ListItem item={items[vItem.index]} />
</div>
))}
</div>
</div>
);
Flag any list that maps over an array > 100 items without virtual scroll.
Concurrent Features (React 18+)
useTransition โ wrap heavy non-urgent state updates so the UI stays responsive:
const [isPending, startTransition] = useTransition();
const handleFilterChange = (q: string) => {
startTransition(() => setFilterQuery(q));
};
useDeferredValue โ defer a value that drives expensive rendering:
const deferredQuery = useDeferredValue(filterQuery);
const filtered = useMemo(() => items.filter(i => i.name.includes(deferredQuery)), [deferredQuery, items]);
Flag heavy filter/sort operations that block the main thread on every keystroke โ these are candidates for useTransition.
Canvas / WebGL Separation (iot-ops / map / chart apps)
For data-dense UIs (real-time dashboards, map overlays, charting), React state is the wrong tool for per-frame updates.
Check whether:
- High-frequency data (sensor readings, map tile updates, chart data) bypasses React state and goes directly to canvas/WebGL via
useRef.
- React only controls the layout shell and control panel; the canvas/WebGL layer handles rendering independently.
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const renderer = new WebGLRenderer(canvasRef.current!);
const unsub = sensorStream.subscribe(data => renderer.update(data));
return unsub;
}, []);
Output
Write <client-repo>/.frontend-review/report/latest/md/performance-review.md with:
- Profiling recommendation: what to measure first and how (React DevTools Profiler, why-did-you-render)
- Memoization gaps / misuse: file:line references for each finding
- Virtual scroll candidates: component name, estimated list size
- Concurrent feature opportunities: interactions that block the thread
- Canvas/WebGL assessment (if applicable): is high-frequency data bypassing React state?
- Recommended PRs: one optimization per PR, profiling benchmark in PR description
Keep under 200 lines. Recommendations without profiling evidence must be explicitly flagged as "unconfirmed โ profile first."
Boundaries
- Do NOT run profiling sessions โ describe what to measure and how.
- Do NOT propose optimization without a measurement plan.
- Do NOT touch source files in the client repo.
- State management architecture (store design, selector granularity) is covered by
frontend-review-state.
Reference
- Checklist:
24-rendering-performance.md, 23-state-management.md, C2-lighthouse.md
- Tools: React DevTools Profiler,
@welldone-software/why-did-you-render, @tanstack/react-virtual