React Performance Optimization
1. React.memo for Component Memoization
| Concept | Description | When to Use | Performance Impact |
|---|---|---|---|
| React.memo | HOC that memoizes component result | Expensive renders, pure components | Skips re-render if props unchanged |
| Shallow comparison | Default props comparison (===) | Primitive props | Fast, works for most cases |
| Custom comparison | Custom arePropsEqual function | Complex props, deep equality | Slower, use judiciously |
| When NOT to use | Simple components, always different props | Overhead > benefit | Can hurt performance |
| Props Type | Memo Behavior | Solution |
|---|---|---|
| Primitives | Compared by value | Works perfectly |
| Objects | Compared by reference | useMemo to stabilize |
| Arrays | Compared by reference | useMemo to stabilize |
| Functions | Compared by reference | useCallback to stabilize |
| Children prop | New reference each render | Extract to separate component |
Example: React.memo usage patterns
// Basic React.memo usage
const ExpensiveComponent = React.memo(({ data }) => {
console.log('Rendering ExpensiveComponent');
// Expensive computation or large render tree
const processedData = data.map(item => {
// Complex processing
return item.value * 2;
});
return (
<div>
{processedData.map(val => (
<div key={val}>{val}</div>
))}
</div>
);
});
// Parent component
const Parent = () => {
const [count, setCount] = useState(0);
const [data] = useState([1, 2, 3, 4, 5]);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
{/* ExpensiveComponent won't re-render when count changes */}
<ExpensiveComponent data={data} />
</div>
);
};
// ❌ BAD: memo doesn't help with new object references
const BadMemo = React.memo(({ config }) => {
return <div>{config.name}</div>;
});
const ParentBad = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
{/* Re-renders every time - new object! */}
<BadMemo config={{ name: 'Test' }} />
</div>
);
};
// ✅ GOOD: stabilize object with useMemo
const GoodMemo = React.memo(({ config }) => {
return <div>{config.name}</div>;
});
const ParentGood = () => {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ name: 'Test' }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
{/* Only renders once! */}
<GoodMemo config={config} />
</div>
);
};
// Custom comparison function
const UserCard = React.memo(
({ user, onEdit }) => {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
},
(prevProps, nextProps) => {
// Custom comparison: only re-render if user.id or user.name changed
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name
);
// Return true to skip render, false to render
}
);
// When NOT to use React.memo
const SimpleComponent = ({ text }) => {
return <div>{text}</div>;
};
// No need for memo - component is very simple
const AlwaysChanging = ({ timestamp }) => {
return <div>{timestamp}</div>;
};
// No need for memo - timestamp always changes
// Memoizing list items
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
console.log('Rendering todo:', todo.id);
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
const TodoList = () => {
const [todos, setTodos] = useState([]);
// Stabilize callback functions
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
};
Note: React.memo only checks props changes. It doesn't prevent re-renders from state/context
changes within the component. Use it for expensive components with stable props.
2. useMemo Hook for Expensive Calculations
| Use Case | Without useMemo | With useMemo | Benefit |
|---|---|---|---|
| Expensive calculation | Runs every render | Cached until deps change | Saves computation time |
| Array filtering/sorting | Creates new array every render | Reuses array if deps unchanged | Prevents unnecessary work |
| Object creation | New reference every render | Stable reference | Prevents child re-renders |
| Derived state | Computed on each render | Computed only when needed | Performance optimization |
| When to Use | When NOT to Use |
|---|---|
| Expensive calculations (loops, recursion) | Simple calculations (arithmetic) |
| Processing large arrays/objects | Small data sets (< 100 items) |
| Stabilizing references for child props | Values that always change |
| Complex transformations/filtering | Trivial operations |
Example: useMemo patterns
// Expensive calculation
const ProductList = ({ products, filter }) => {
// Without useMemo: runs on EVERY render (bad!)
// const filtered = products.filter(p => p.category === filter);
// With useMemo: only runs when products or filter changes
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(p => p.category === filter);
}, [products, filter]);
return (
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
};
// Complex derived state
const Dashboard = ({ data }) => {
const statistics = useMemo(() => {
console.log('Computing statistics...');
const total = data.reduce((sum, item) => sum + item.value, 0);
const average = total / data.length;
const max = Math.max(...data.map(item => item.value));
const min = Math.min(...data.map(item => item.value));
return { total, average, max, min };
}, [data]);
return (
<div>
<p>Total: {statistics.total}</p>
<p>Average: {statistics.average}</p>
<p>Max: {statistics.max}</p>
<p>Min: {statistics.min}</p>
</div>
);
};
// Sorting large lists
const SortableTable = ({ data, sortBy, sortOrder }) => {
const sortedData = useMemo(() => {
console.log('Sorting data...');
return [...data].sort((a, b) => {
const aVal = a[sortBy];
const bVal = b[sortBy];
if (typeof aVal === 'string') {
return sortOrder === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
}
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
});
}, [data, sortBy, sortOrder]);
return (
<table>
{sortedData.map(row => (
<tr key={row.id}>
<td>{row.name}</td>
</tr>
))}
</table>
);
};
// Stabilizing object references
const Parent = () => {
const [count, setCount] = useState(0);
// ❌ BAD: new object every render
// const config = { theme: 'dark', lang: 'en' };
// ✅ GOOD: stable reference
const config = useMemo(
() => ({ theme: 'dark', lang: 'en' }),
[]
);
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<MemoizedChild config={config} />
</div>
);
};
// When NOT to use useMemo
const Bad = ({ a, b }) => {
// ❌ Overkill - simple calculation
const sum = useMemo(() => a + b, [a, b]);
// ✅ Just do it directly
const betterSum = a + b;
return <div>{sum}</div>;
};
// Search and filter with multiple criteria
const AdvancedSearch = ({ items, search, filters }) => {
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item => {
// Text search
if (search && !item.name.toLowerCase().includes(search.toLowerCase())) {
return false;
}
// Category filter
if (filters.category && item.category !== filters.category) {
return false;
}
// Price range
if (item.price < filters.minPrice || item.price > filters.maxPrice) {
return false;
}
return true;
});
}, [items, search, filters]);
const sortedItems = useMemo(() => {
console.log('Sorting items...');
return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredItems]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.name} - ${item.price}</li>
))}
</ul>
);
};
// Recursive computation
const TreeView = ({ node }) => {
const descendantCount = useMemo(() => {
const countDescendants = (n) => {
if (!n.children) return 0;
return n.children.length +
n.children.reduce((sum, child) => sum + countDescendants(child), 0);
};
return countDescendants(node);
}, [node]);
return <div>Descendants: {descendantCount}</div>;
};
Warning: Don't overuse useMemo! It has overhead (memory + comparison). Profile first, optimize
later. Most calculations are fast enough without memoization.
3. useCallback Hook for Function Memoization
| Scenario | Without useCallback | With useCallback | Impact |
|---|---|---|---|
| Function prop to memo component | New function = child re-renders | Stable function = no re-render | Prevents unnecessary renders |
| Function in useEffect deps | Effect runs every render | Effect runs only when needed | Reduces side effects |
| Event handlers | New function every render | Stable reference | Slight memory improvement |
| When to Use useCallback | When NOT to Use |
|---|---|
| Function passed to memoized child | Simple event handlers not passed down |
| Function in useEffect dependencies | Function only used once |
| Function passed to many children | Non-memoized children |
| Optimization after profiling | Premature optimization |
Example: useCallback patterns
// Basic useCallback usage
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ❌ BAD: new function every render
// const handleClick = () => {
// console.log(count);
// };
// ✅ GOOD: stable function reference
const handleClick = useCallback(() => {
console.log(count);
}, [count]); // Re-created only when count changes
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>{count}</button>
{/* Child won't re-render when text changes */}
<MemoizedChild onClick={handleClick} />
</div>
);
};
const MemoizedChild = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
// Function in useEffect dependencies
const DataFetcher = ({ userId }) => {
const [data, setData] = useState(null);
// Stable fetchData function
const fetchData = useCallback(async () => {
const response = await fetch(\`/api/users/\${userId}\`);
const result = await response.json();
setData(result);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]); // Only runs when fetchData changes (i.e., when userId changes)
return <div>{data?.name}</div>;
};
// List operations with callbacks
const TodoList = () => {
const [todos, setTodos] = useState([]);
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []); // Empty deps - uses functional update
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const handleEdit = useCallback((id, newText) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</ul>
);
};
// When NOT to use useCallback
const SimpleComponent = () => {
const [count, setCount] = useState(0);
// ❌ Unnecessary - not passed to memoized child
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// ✅ Just use inline
return (
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
);
};
// Combining useCallback with other hooks
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const search = useCallback(async (searchQuery) => {
if (!searchQuery) {
setResults([]);
return;
}
const response = await fetch(\`/api/search?q=\${searchQuery}\`);
const data = await response.json();
setResults(data);
}, []);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
search(query);
}, 300);
return () => clearTimeout(timer);
}, [query, search]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// useCallback with parameters from closure
const ItemList = ({ items, onUpdate }) => {
const [filter, setFilter] = useState('');
// ❌ BAD: depends on filter but not in deps
// const handleUpdate = useCallback((id, value) => {
// if (filter === 'active') {
// onUpdate(id, value);
// }
// }, []); // Missing filter!
// ✅ GOOD: includes all dependencies
const handleUpdate = useCallback((id, value) => {
if (filter === 'active') {
onUpdate(id, value);
}
}, [filter, onUpdate]);
return (
<ul>
{items.map(item => (
<Item key={item.id} item={item} onUpdate={handleUpdate} />
))}
</ul>
);
};
Note: useCallback returns memoized function, useMemo returns memoized value.
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
4. Code Splitting with React.lazy and Suspense
| Technique | Description | Bundle Impact | Use Case |
|---|---|---|---|
| React.lazy | Dynamic import for components | Separate chunk per lazy component | Route-based splitting |
| Suspense | Loading fallback for lazy components | No impact | Show loading state |
| Route-based | Split by routes/pages | One chunk per route | Most common pattern |
| Component-based | Split heavy components | Smaller chunks | Modals, tabs, charts |
| Library splitting | Separate vendor chunks | Better caching | Large dependencies |
Example: Code splitting patterns
// Basic React.lazy usage
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const App = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
};
// Route-based code splitting
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const AppRouter = () => {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
// Conditional lazy loading
const AdminPanel = lazy(() => import('./AdminPanel'));
const App = ({ user }) => {
return (
<div>
{user.isAdmin && (
<Suspense fallback={<div>Loading admin panel...</div>}>
<AdminPanel />
</Suspense>
)}
</div>
);
};
// Named exports with lazy
const LazyComponent = lazy(() =>
import('./MyComponent').then(module => ({
default: module.MyNamedComponent
}))
);
// Preloading lazy components
const HeavyChart = lazy(() => import('./HeavyChart'));
// Preload function
const preloadChart = () => {
import('./HeavyChart');
};
const Dashboard = () => {
const [showChart, setShowChart] = useState(false);
return (
<div>
{/* Preload on hover */}
<button
onClick={() => setShowChart(true)}
onMouseEnter={preloadChart}
>
Show Chart
</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
};
// Modal lazy loading
const Modal = lazy(() => import('./Modal'));
const App = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<Suspense fallback={null}>
<Modal onClose={() => setIsOpen(false)} />
</Suspense>
)}
</div>
);
};
// Tab-based splitting
const Tab1 = lazy(() => import('./tabs/Tab1'));
const Tab2 = lazy(() => import('./tabs/Tab2'));
const Tab3 = lazy(() => import('./tabs/Tab3'));
const TabbedInterface = () => {
const [activeTab, setActiveTab] = useState('tab1');
return (
<div>
<div>
<button onClick={() => setActiveTab('tab1')}>Tab 1</button>
<button onClick={() => setActiveTab('tab2')}>Tab 2</button>
<button onClick={() => setActiveTab('tab3')}>Tab 3</button>
</div>
<Suspense fallback={<div>Loading tab...</div>}>
{activeTab === 'tab1' && <Tab1 />}
{activeTab === 'tab2' && <Tab2 />}
{activeTab === 'tab3' && <Tab3 />}
</Suspense>
</div>
);
};
// Error boundary with lazy loading
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Failed to load component</div>;
}
return this.props.children;
}
}
const AppWithErrorHandling = () => {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
};
// Webpack magic comments for chunk naming
const Dashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './Dashboard')
);
const AdminPanel = lazy(() =>
import(/* webpackChunkName: "admin" */ './AdminPanel')
);
// Prefetching/preloading
const Settings = lazy(() =>
import(
/* webpackChunkName: "settings" */
/* webpackPrefetch: true */
'./Settings'
)
);
Note: Code splitting works best for route-based splits and heavy components (charts, editors,
admin panels). Don't split every component - bundle overhead can outweigh benefits for small components.
5. Bundle Size Analysis and Optimization
| Tool | Purpose | Install/Usage |
|---|---|---|
| webpack-bundle-analyzer | Visualize bundle contents | npm i -D webpack-bundle-analyzer |
| source-map-explorer | Analyze source maps | npm i -D source-map-explorer |
| Bundle Buddy | Find duplicate dependencies | Upload webpack stats.json |
| Lighthouse | Performance audit | Chrome DevTools |
| Optimization | Technique | Impact |
|---|---|---|
| Tree shaking | Remove unused exports | Smaller bundle (10-30%) |
| Minification | Compress code | 40-60% size reduction |
| Compression (gzip/brotli) | Server-side compression | 60-80% transfer size reduction |
| Dynamic imports | Load code on demand | Faster initial load |
| Remove unused deps | Audit package.json | Variable savings |
| Lighter alternatives | Replace heavy libraries | Significant size reduction |
Example: Bundle optimization techniques
// package.json - analyze bundle
{
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"build:analyze": "npm run build && npm run analyze"
}
}
// webpack.config.js - bundle analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};
// Tree shaking - import only what you need
// ❌ BAD: imports entire library
import _ from 'lodash';
const result = _.debounce(fn, 300);
// ✅ GOOD: import specific function
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// Or use lodash-es for better tree shaking
import { debounce } from 'lodash-es';
// Replace heavy libraries with lighter alternatives
// ❌ Moment.js (large)
import moment from 'moment';
// ✅ date-fns (smaller, tree-shakeable)
import { format, parseISO } from 'date-fns';
// ✅ dayjs (smallest)
import dayjs from 'dayjs';
// Remove unused dependencies
// Run: npm ls
// Check: npx depcheck
// Dynamic imports for heavy dependencies
const loadChartLibrary = async () => {
const Chart = await import('chart.js');
return Chart.default;
};
// Optimize images
// Use next/image or similar
import Image from 'next/image';
<Image
src="/photo.jpg"
width={500}
height={300}
loading="lazy"
alt="Photo"
/>
// Code splitting by route
const routes = [
{
path: '/',
component: lazy(() => import('./pages/Home'))
},
{
path: '/dashboard',
component: lazy(() => import('./pages/Dashboard'))
}
];
// Vendor chunk separation (webpack)
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
// Preload critical resources
<link rel="preload" as="script" href="/main.js" />
<link rel="prefetch" href="/secondary.js" />
// Compression middleware (Express)
const compression = require('compression');
app.use(compression());
// Service worker for caching
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// Remove console logs in production
// babel-plugin-transform-remove-console
{
"plugins": [
["transform-remove-console", { "exclude": ["error", "warn"] }]
]
}
Warning: Always analyze your bundle before optimizing. Focus on the biggest chunks first.
Remember: premature optimization is the root of all evil!
6. Re-render Optimization and React Profiler
| Tool | Purpose | How to Use |
|---|---|---|
| React DevTools Profiler | Record and analyze renders | Click "Profiler" tab, record session |
| Highlight Updates | Visualize re-renders | Settings → "Highlight updates" |
| Component Stack | See render cause | Click component in Profiler |
| Why Did You Render | Debug unnecessary renders | npm package with detailed logs |
| Common Issue | Symptom | Solution |
|---|---|---|
| Inline functions | Child re-renders unnecessarily | useCallback + React.memo |
| Inline objects | Props always different | useMemo for stable reference |
| Context changes | All consumers re-render | Split contexts, memoize value |
| State too high | Entire tree re-renders | Move state closer to usage |
| Large lists | Slow rendering | Virtualization, pagination |
| No keys/bad keys | Wrong items update | Stable unique keys |
Example: Debugging and fixing re-renders
// Install why-did-you-render
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// Use in component
const MyComponent = ({ value }) => {
return <div>{value}</div>;
};
MyComponent.whyDidYouRender = true;
// Finding render causes with useEffect
const DebugComponent = ({ prop1, prop2, prop3 }) => {
useEffect(() => {
console.log('Component rendered');
console.log('prop1:', prop1);
console.log('prop2:', prop2);
console.log('prop3:', prop3);
});
return <div>Debug</div>;
};
// Custom hook to track prop changes
const useWhyDidYouUpdate = (name, props) => {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changedProps).length > 0) {
console.log('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
};
// Usage
const MyComponent = (props) => {
useWhyDidYouUpdate('MyComponent', props);
return <div>{props.value}</div>;
};
// Fix: Inline object props
// ❌ BAD
const Parent = () => {
return <Child style={{ color: 'red' }} />; // New object every render
};
// ✅ GOOD
const Parent = () => {
const style = useMemo(() => ({ color: 'red' }), []);
return <Child style={style} />;
};
// Fix: Context re-renders
// ❌ BAD: value changes every render
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
// ✅ GOOD: stable value reference
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, setTheme }),
[theme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
// Fix: State colocation
// ❌ BAD: state too high
const App = () => {
const [formData, setFormData] = useState({});
return (
<div>
<Header /> {/* Re-renders on formData change */}
<Sidebar /> {/* Re-renders on formData change */}
<Form data={formData} onChange={setFormData} />
</div>
);
};
// ✅ GOOD: state colocated
const App = () => {
return (
<div>
<Header />
<Sidebar />
<FormContainer /> {/* State inside here */}
</div>
);
};
const FormContainer = () => {
const [formData, setFormData] = useState({});
return <Form data={formData} onChange={setFormData} />;
};
// Profiling with React.Profiler API
const App = () => {
const onRenderCallback = (
id, // component name
phase, // "mount" or "update"
actualDuration, // time spent rendering
baseDuration, // estimated time without memoization
startTime, // when started
commitTime, // when committed
interactions // Set of interactions
) => {
console.log(\`\${id} took \${actualDuration}ms to render\`);
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyApp />
</Profiler>
);
};