
React Performance
- 107 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with frontend development tasks during AI-assisted development.
About
react-performance is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted coding.
- react-performance
- Frontend Development
- AI-coding skill
React Performance by the numbers
- 107 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,039 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill react-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 107 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with frontend development tasks during AI-assisted development.
Files
React Performance
Overview
Dedicated performance optimization skill for React applications. Covers the full spectrum from build-time optimizations (code splitting, barrel file avoidance) through runtime techniques (memoization, transitions, content-visibility) to diagnostic tooling (React DevTools Profiler, bundle analyzers).
When to use: Reducing Time to Interactive, shrinking bundle size, eliminating re-renders, profiling slow components, optimizing large lists, lazy loading heavy dependencies, auditing React app performance.
When NOT to use: General React component patterns (use react-patterns skill), framework-specific optimizations like Next.js caching (use framework skill), non-React performance (network, database, CDN).
Quick Reference
| Category | Technique | Key Points |
|---|---|---|
| Compiler | React Compiler | Automatic memoization at build time; eliminates manual memo/useMemo/useCallback |
| Memoization | React.memo(Component) | Wrap components receiving stable primitive props from frequently re-rendering parents |
| Memoization | useMemo(fn, deps) | Expensive computations only: sorting, filtering, Set/Map construction |
| Memoization | useCallback(fn, deps) | Only when passed to memoized children; use functional setState for stable refs |
| Splitting | React.lazy(() => import()) | Lazy-load heavy components with <Suspense> fallback |
| Splitting | Preload on intent | Trigger import() on hover/focus for perceived speed |
| Bundle | Direct imports | Avoid barrel files; import from specific paths to reduce module count |
| Bundle | Defer third-party | Load analytics, logging after hydration |
| Re-renders | startTransition | Mark non-urgent updates (search, scroll tracking) as interruptible |
| Re-renders | Functional setState | setState(prev => ...) eliminates state dependencies from callbacks |
| Re-renders | Derived state | Subscribe to booleans, not continuous values; compute during render |
| Re-renders | Defer state reads | Read dynamic state (searchParams) inside callbacks, not at render |
| Rendering | content-visibility: auto | Skip layout/paint for off-screen items in long lists |
| Rendering | Hoist static JSX | Extract constant elements outside component functions |
| Profiling | React DevTools Profiler | Record renders, identify slow components, flamegraph analysis |
| Profiling | Bundle analyzer | Visualize chunk sizes, find oversized dependencies |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Wrapping everything in useMemo/useCallback | Trust React Compiler first; only memoize expensive computations or memoized-child callbacks |
Memoizing cheap operations like value * 2 | Skip memo for simple primitives; overhead exceeds recomputation cost |
Importing from barrel files (lucide-react, @mui/material) | Import directly from specific paths or use optimizePackageImports |
| Loading analytics/tracking in the initial bundle | Defer with lazy + mounted state to load after hydration |
| Subscribing to continuous values (window width) for boolean checks | Use useMediaQuery or derived boolean to re-render only on threshold change |
| Referencing state in useCallback dependency array | Use functional setState setState(prev => ...) for stable callbacks |
Using useEffect to derive state from props | Compute derived values during render; effects add an extra render cycle |
| Creating new object literals as props on every render | Hoist static objects outside component; use useMemo for dynamic objects |
| Profiling in development mode | Always profile production builds; dev mode includes extra warnings that skew results |
Delegation
- Profile and diagnose performance bottlenecks: Use
Exploreagent to run React DevTools Profiler, analyze bundle composition, and trace re-render cascades - Apply performance optimizations to existing code: Use
Taskagent to implement code splitting, add memoization boundaries, and optimize rendering - Plan performance improvement strategy: Use
Planagent to prioritize optimizations by impact (waterfalls > bundle > re-renders) and create an optimization roadmap
If the react-patterns skill is available, delegate general component architecture and React 19 API questions to it.Otherwise, recommend: npx skills add oakoss/agent-skills --skill react-patternsReferences
- Rendering optimization: memo, useMemo, useCallback, compiler, re-render elimination
- Code splitting: React.lazy, Suspense, dynamic imports, bundle optimization
- Profiling and debugging: DevTools, performance measurement, common bottlenecks
Code Splitting
React.lazy with Suspense
Lazy-load heavy components that are not needed on initial render:
import { lazy, Suspense } from 'react';
const MonacoEditor = lazy(() =>
import('./monaco-editor').then((m) => ({ default: m.MonacoEditor })),
);
function CodePanel({ code }: { code: string }) {
return (
<Suspense fallback={<div>Loading editor...</div>}>
<MonacoEditor value={code} />
</Suspense>
);
}The .then() wrapper is needed when the module does not have a default export.
Route-Based Splitting
Split at route boundaries for the most impactful code splitting:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/dashboard'));
const Settings = lazy(() => import('./pages/settings'));
const Analytics = lazy(() => import('./pages/analytics'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}Each route loads its own chunk. Users only download code for the page they visit.
Preload on User Intent
Trigger import() on hover or focus to preload chunks before the user clicks:
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor');
}
};
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
);
}The browser caches the import, so the component loads instantly when activated.
Preload on Feature Flag
Start loading modules when a feature is enabled, not when the UI needs them:
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then((mod) => mod.init());
}
}, [flags.editorEnabled]);
return (
<FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>
);
}Conditional Module Loading
Load large data modules only when a feature is activated:
function AnimationPlayer({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then((mod) => setFrames(mod.frames))
.catch(() => setEnabled(false));
}
}, [enabled, frames, setEnabled]);
if (!frames) return <Skeleton />;
return <Canvas frames={frames} />;
}The typeof window !== 'undefined' check prevents bundling the module for SSR.
Avoid Barrel File Imports
Barrel files (index files that re-export everything) force bundlers to load thousands of unused modules:
import Check from 'lucide-react/dist/esm/icons/check';
import X from 'lucide-react/dist/esm/icons/x';
import Menu from 'lucide-react/dist/esm/icons/menu';Libraries commonly affected: lucide-react, @mui/material, @mui/icons-material, @tabler/icons-react, react-icons, lodash, date-fns.
Direct imports provide 15-70% faster dev boot, 28% faster builds, and 40% faster cold starts.
Next.js optimizePackageImports
Next.js 13.5+ can automatically transform barrel imports at build time:
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material'],
},
};With this configured, ergonomic barrel imports are safe:
import { Check, X, Menu } from 'lucide-react';Defer Third-Party Libraries
Load analytics, logging, and tracking after hydration to keep the critical path lean:
import { useEffect, useState, lazy, Suspense } from 'react';
const Analytics = lazy(() =>
import('@vercel/analytics/react').then((m) => ({ default: m.Analytics })),
);
export default function App({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div>
{children}
{mounted ? (
<Suspense fallback={null}>
<Analytics />
</Suspense>
) : null}
</div>
);
}Suspense Boundaries for Streaming
Place Suspense boundaries around async Server Components to stream content progressively:
function Page() {
return (
<div>
<Sidebar />
<Header />
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<Footer />
</div>
);
}
async function DataDisplay() {
const data = await fetchData();
return <div>{data.content}</div>;
}Static content renders immediately while data-dependent sections stream in as they resolve.
Bundle Analysis Checklist
When auditing bundle size, check for these common issues:
1. Barrel file imports -- switch to direct imports or configure optimizePackageImports 2. Undeferred third-party scripts -- analytics, error tracking, chat widgets 3. Large components in initial bundle -- code editors, charts, maps, PDF viewers 4. Duplicate dependencies -- multiple versions of the same library 5. Unused exports -- dead code that bundlers cannot tree-shake due to side effects 6. Polyfills for modern browsers -- unnecessary compatibility code
Profiling and Debugging
React DevTools Profiler
The Profiler tab in React DevTools records component renders and measures their duration.
Recording a Profile
1. Open React DevTools and switch to the Profiler tab 2. Click Record (blue circle) 3. Perform the interaction you want to measure 4. Click Stop to end recording
Reading the Flamegraph
The flamegraph shows the component tree for each commit (render):
- Gray bars -- components that did not render in this commit
- Colored bars -- components that rendered; color intensity indicates render duration
- Width -- relative render time compared to siblings
- Hover -- shows exact render time and reason for re-render
Identifying Slow Components
Sort by render duration to find the most expensive components:
Profiler > Ranked chart > Sort by self timeComponents with high self time are doing expensive work during render. Common causes:
- Large list rendering without virtualization
- Expensive computations not wrapped in
useMemo - Creating new objects/arrays on every render
- Calling expensive functions during render instead of memoizing results
Identifying Unnecessary Re-renders
Enable "Record why each component rendered" in Profiler settings:
Profiler > Settings (gear icon) > Record why each component renderedCommon re-render reasons:
- Props changed -- parent passes new object references; check if the values actually changed
- State changed -- expected re-render; verify the state change is necessary
- Context changed -- context value creates new reference; split contexts or memoize the value
- Parent rendered -- wrap child in
React.memoif its props are stable
Highlight Updates
Enable visual re-render indicators to spot unnecessary renders in real time:
React DevTools > Settings > General > Highlight updates when components renderComponents flash colored borders when they re-render. Frequent flashing on components that should be static indicates optimization opportunities.
React Compiler DevTools
When React Compiler is enabled, DevTools shows optimization status:
- "Memo" badge on components -- the compiler successfully memoized the component
- No badge -- the compiler skipped optimization; check for Rules of React violations
Use the react-compiler-healthcheck CLI to audit your codebase:
npx react-compiler-healthcheck@latestThis reports how many components the compiler can optimize and which ones it skips.
Production Profiling
Development mode includes extra warnings, strict mode double-renders, and debugging aids that distort performance measurements. Always profile production builds.
Next.js Production Profiling
next build --profileThis creates a production build with profiling enabled. Then use React DevTools Profiler as normal.
Vite/CRA Production Profiling
Add the profiling bundle alias to the production build:
// vite.config.ts
export default defineConfig({
resolve: {
alias: {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
},
},
});Build and serve the production bundle, then profile with React DevTools.
Bundle Analysis
Visualize what is in your JavaScript bundles to find optimization targets.
Next.js Bundle Analyzer
pnpm add -D @next/bundle-analyzer// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});ANALYZE=true next buildOpens an interactive treemap showing chunk composition. Look for:
- Unexpectedly large dependencies
- Duplicate packages (multiple versions)
- Libraries that should be code-split
Vite Bundle Analysis
pnpm add -D rollup-plugin-visualizer// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [visualizer({ open: true, gzipSize: true })],
});Source Map Explorer
Framework-agnostic tool that works with any source maps:
npx source-map-explorer dist/assets/*.jsPerformance Measurement in Code
Use the React Profiler component for programmatic performance tracking:
import { Profiler, type ProfilerOnRenderCallback } from 'react';
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
) => {
if (actualDuration > 16) {
console.warn(`Slow render: ${id} took ${actualDuration.toFixed(1)}ms`);
}
};
function App() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}The 16ms threshold corresponds to 60fps. Components exceeding this cause visible jank.
Common Bottleneck Patterns
Large List Without Virtualization
Rendering thousands of DOM nodes causes slow initial render and expensive re-renders:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: `${virtualItem.size}px`,
width: '100%',
}}
>
<ItemRow item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}Virtualization renders only visible items. For 10,000 items, only ~20 DOM nodes exist at any time.
Context Re-render Cascade
A single context value change re-renders all consumers:
const ThemeContext = createContext<Theme>(defaultTheme);
const UserContext = createContext<User | null>(null);
function AppProviders({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [user, setUser] = useState<User | null>(null);
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>{children}</UserContext.Provider>
</ThemeContext.Provider>
);
}Split contexts by update frequency. Components consuming ThemeContext do not re-render when user changes.
Expensive Render in Unrelated Update
State colocation prevents unrelated components from re-rendering:
function SearchSection() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
return (
<div>
<SearchInput query={query} onChange={setQuery} />
<SearchResults results={results} />
</div>
);
}
function Page() {
return (
<div>
<SearchSection />
<ExpensiveChart />
</div>
);
}By colocating query state inside SearchSection, typing in the search input does not re-render ExpensiveChart.
Optimization Priority
When optimizing a React application, prioritize by impact:
1. Eliminate data fetching waterfalls -- parallel fetches, Suspense streaming (CRITICAL) 2. Reduce bundle size -- code splitting, direct imports, deferred third-party (CRITICAL) 3. Fix re-render cascades -- context splitting, state colocation, memoization (MEDIUM) 4. Optimize rendering -- virtualization, content-visibility, transitions (MEDIUM) 5. JavaScript micro-optimizations -- loop combining, Set/Map lookups (LOW)
Start from the top. Lower-priority optimizations rarely matter if higher-priority issues exist.
Rendering Optimization
React Compiler (Preferred)
React Compiler automatically applies memoization equivalent to useMemo, useCallback, and React.memo at build time. Code that follows the Rules of React gets optimized without manual intervention.
When the compiler is enabled, remove manual memoization unless the compiler explicitly skips a component (check React DevTools for the "Memo" badge).
function ExpensiveComponent({ data, onClick }: Props) {
const processedData = expensiveProcessing(data);
const handleClick = (item: Item) => {
onClick(item.id);
};
return (
<div>
{processedData.map((item) => (
<Item key={item.id} onClick={() => handleClick(item)} />
))}
</div>
);
}The compiler optimizes this automatically. No useMemo, useCallback, or memo needed.
When to Use Manual Memoization
Without React Compiler, or when the compiler skips a component, apply memoization selectively.
React.memo -- Component-Level Memoization
Wrap components that receive stable props from frequently re-rendering parents:
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />;
return (
<div>
<UserAvatar user={user} />
</div>
);
}Extracting into a memoized component enables early returns before expensive computation.
useMemo -- Expensive Computations Only
const doubled = value * 2;
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.price - b.price),
[items],
);
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);Skip useMemo for cheap operations (arithmetic, string concatenation). Use it for sorting, filtering, or building lookup structures from arrays.
useCallback -- Memoized Child Consumers Only
const handleClick = useCallback(() => doSomething(id), [id]);
return <MemoizedChild onClick={handleClick} />;Skip useCallback when the consumer is not memoized -- the overhead exceeds the benefit.
Functional setState for Stable Callbacks
Use the functional update form to eliminate state dependencies from callbacks:
function TodoList() {
const [items, setItems] = useState(initialItems);
const addItems = useCallback((newItems: Item[]) => {
setItems((curr) => [...curr, ...newItems]);
}, []);
const removeItem = useCallback((id: string) => {
setItems((curr) => curr.filter((item) => item.id !== id));
}, []);
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}Callbacks with empty dependency arrays are never recreated, preventing unnecessary child re-renders.
Defer State Reads
Avoid subscribing to dynamic state that is only used inside callbacks:
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search);
const ref = params.get('ref');
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}Reading searchParams on demand instead of via useSearchParams() avoids re-renders when URL changes.
Subscribe to Derived State
Subscribe to derived booleans instead of continuous values:
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)');
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
}This re-renders only when the boolean changes, not on every pixel of window resize.
Derive State During Render
Compute derived values during render instead of syncing with effects:
function FilteredList({ items, filter }: Props) {
const filtered = items.filter((item) => item.category === filter);
return (
<ul>
{filtered.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Using useEffect to sync derived state adds an extra render cycle and is unnecessary.
startTransition for Non-Urgent Updates
Mark frequent, non-urgent state updates as transitions to keep the UI responsive:
import { startTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
const handleChange = (value: string) => {
setQuery(value);
startTransition(() => {
setResults(search(value));
});
};
return (
<>
<input value={query} onChange={(e) => handleChange(e.target.value)} />
<ResultsList results={results} />
</>
);
}The input stays responsive while search results update in the background.
Stable Object References
Avoid creating new object or array literals as props on every render:
const style = { color: 'red' };
function Parent() {
return <Child style={style} />;
}For dynamic values, use useMemo:
function Parent({ color }: { color: string }) {
const style = useMemo(() => ({ color }), [color]);
return <MemoizedChild style={style} />;
}content-visibility for Long Lists
Use CSS content-visibility to skip layout and paint for off-screen items:
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map((msg) => (
<div key={msg.id} className="list-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
);
}For 1000 items, the browser skips layout/paint for ~990 off-screen items.
Hoist Static JSX
Extract constant JSX elements outside component functions to avoid recreation:
const emptyState = <div className="empty">No items found</div>;
function ItemList({ items }: { items: Item[] }) {
if (items.length === 0) return emptyState;
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Static JSX hoisted outside the component is created once and reused across renders.