React Memo and Lazy Loading
Optimize component rendering with React.memo, lazy loading, and code splitting strategies.
TL;DR
- 01Use React.memo to skip re-renders when props haven't changed.
- 02Use lazy loading to split code and load components on demand.
- 03Combine both for significant performance improvements.
Tips
- 01Unlike regular Suspense data-fetching failures, a failed dynamic import() throws a module load error — always pair React.lazy with an ErrorBoundary in production.
Warnings
- 01React.memo only does shallow comparison, so passing new objects or arrays as props defeats memoization — use useMemo or useCallback to keep props stable.
React.memo
- Memoize a component to skip re-renders when props are the same.
const UserCard = React.memo(({ user }) => ( <div>{user.name}</div> )); - React.memo compares new and old props using shallow equality.
- Only skip re-renders if props haven't changed.
// Parent re-renders but UserCard doesn't if user prop is the same const Parent = () => { const [count, setCount] = useState(0); const user = useMemo(() => ({ id: 1, name: "Alice" }), []); return ( <> <UserCard user={user} /> <button onClick={() => setCount(count + 1)}> Count: {count} </button> </> ); }; - Use custom comparison for complex props.
const UserCard = React.memo( ({ user }) => <div>{user.name}</div>, (prevProps, nextProps) => { return prevProps.user.id === nextProps.user.id; } ); - Memoization has overhead, so only use when re-renders are expensive.
Lazy Loading
Use
React.lazyto split code and load components on demand.import { lazy, Suspense } from "react"; const HeavyComponent = lazy(() => import("./HeavyComponent")); export default function App() { return ( <Suspense fallback={<p>Loading...</p>}> <HeavyComponent /> </Suspense> ); }The component code is not included in the main bundle — it fetches only when first rendered.
Wrap lazy components in an
ErrorBoundaryto handle chunk load failures gracefully.<ErrorBoundary fallback={<p>Failed to load — check your connection.</p>}> <Suspense fallback={<p>Loading...</p>}> <HeavyComponent /> </Suspense> </ErrorBoundary>Load conditional UI only when needed — modals, settings panels, and charts are ideal candidates.
const SettingsPanel = lazy(() => import("./SettingsPanel")); {showSettings && ( <Suspense fallback={<p>Loading settings...</p>}> <SettingsPanel /> </Suspense> )}Works great for route-based code splitting in React Router or any SPA router.
Route-Based Code Splitting
- Split code by route to load pages on demand in Next.js or React Router.
import { lazy } from "react"; const Dashboard = lazy(() => import("./pages/Dashboard")); const Settings = lazy(() => import("./pages/Settings")); const Profile = lazy(() => import("./pages/Profile")); - Each route is a separate chunk loaded when the user navigates.
- Reduces the initial bundle size significantly.
<Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/profile" element={<Profile />} /> </Routes> - Most effective optimization for large applications.
- Combine with Suspense for loading states while pages load.
Component Splitting
- Split large components into smaller memoized pieces.
const Header = React.memo(() => <header>...</header>); const Content = React.memo(({ data }) => <main>{data}</main>); const Footer = React.memo(() => <footer>...</footer>); export default function Page({ data }) { return ( <> <Header /> <Content data={data} /> <Footer /> </> ); } - Memoized children don't re-render when parent re-renders.
- Only effective if children receive stable props.
- Combine with useCallback to keep function props stable.
const handleClick = useCallback(() => { // Handle click }, []); <Button onClick={handleClick} />
Performance Monitoring
- Use Profiler API to measure component rendering time.
import { Profiler } from "react"; <Profiler id="dashboard" onRender={onRender}> <Dashboard /> </Profiler> function onRender(id, phase, actualDuration) { console.log(`${id} (${phase}) took ${actualDuration}ms`); } - Use React DevTools Profiler to identify slow components.
- Measure before and after optimization to confirm improvements.
- Only optimize the slow components, not everything.
// Profile to find slow components first // Then apply memo, lazy, or splitting strategically - Avoid premature optimization — measure real performance first.
FAQ
Use React.memo to prevent a component from re-rendering when its props haven't changed. Use useMemo inside a component to memoize expensive computed values. They solve different problems and are often used together.
Wrap your import with React.lazy(() => import('./MyComponent')) and render it inside a Suspense boundary with a fallback UI. The component's code is only fetched when it's first rendered.
No — children is a new object reference on every render, so React.memo will always re-render if you pass JSX as children. Lift the children out or restructure your component tree to avoid this.
Route-based splitting lazy-loads entire page-level components when a route is first visited, which is the highest-impact change for initial load time. Component splitting targets large UI pieces (modals, charts) that aren't visible on first paint.
Use the Coverage tab in Chrome DevTools to see how much JavaScript is unused on initial load, and check the Network tab to confirm chunks are loaded on demand. React DevTools Profiler shows which components re-render unnecessarily.