Rendering
Rendering performance issues, hydration errors, and UI update problems in frontend applications
Rendering Troubleshooting
Rendering issues directly impact user experience — from blank screens to sluggish interactions. This guide addresses common rendering problems across React and modern frontend frameworks.
1. Hydration Mismatch Errors (SSR/SSG)
Problem
Warning: Text content did not match. Server: "March 5, 2025" Client: "March 6, 2025"
Error: Hydration failed because the initial UI does not match what was rendered on the server.- Content differs between server-rendered HTML and client-rendered output
- Layout flickers on initial load
- Interactive elements don't respond after page load
Root Cause
Server and client render different output due to:
- Date/time differences (server timezone vs client timezone)
Math.random()orDate.now()producing different values- Browser-only APIs (
window,localStorage) accessed during render - Conditional rendering based on client-only state
Solution
Use useEffect for client-only values:
function Timestamp() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
setTime(new Date().toLocaleString());
}, []);
if (!time) return <span>Loading...</span>; // Matches server output
return <span>{time}</span>;
}suppressHydrationWarning for intentional mismatches:
<time dateTime={date.toISOString()} suppressHydrationWarning>
{date.toLocaleDateString()}
</time>Next.js dynamic with ssr: false for client-only components:
import dynamic from 'next/dynamic';
const ClientOnlyChart = dynamic(() => import('./Chart'), {
ssr: false,
loading: () => <ChartSkeleton />,
});2. Infinite Re-render Loops
Problem
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.function BadComponent() {
const [count, setCount] = useState(0);
// ✗ Called during render — triggers re-render — infinite loop
setCount(count + 1);
// ✗ Object literal creates new reference every render — infinite loop
useEffect(() => {
fetchData();
}, [{ page: 1 }]); // New object on every render
return <div>{count}</div>;
}Root Cause
- State updates called directly in render body (not in event handler or effect)
useEffectdependencies that change every render (objects, arrays, functions)- Derived state that triggers its own update
Solution
Move state updates to effects or event handlers:
function GoodComponent() {
const [count, setCount] = useState(0);
// ✓ State update in effect, runs only once
useEffect(() => {
setCount(prev => prev + 1);
}, []);
return <div>{count}</div>;
}Memoize dependencies:
function SearchResults({ filters }: { filters: Filters }) {
// ✓ Memoize object to maintain referential equality
const stableFilters = useMemo(() => filters, [filters.query, filters.category]);
useEffect(() => {
fetchResults(stableFilters);
}, [stableFilters]);
}Derive state instead of syncing it:
// ✗ Bad — syncing state causes extra renders
function FilteredList({ items, query }: Props) {
const [filtered, setFiltered] = useState(items);
useEffect(() => {
setFiltered(items.filter(item => item.name.includes(query)));
}, [items, query]);
}
// ✓ Good — derive during render
function FilteredList({ items, query }: Props) {
const filtered = useMemo(
() => items.filter(item => item.name.includes(query)),
[items, query]
);
return <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}3. Slow List Rendering — Large Dataset Performance
Problem
Rendering thousands of items causes:
- Sluggish scrolling
- High memory usage
- Long initial render time
- Unresponsive UI during updates
Solution
Virtual scrolling with TanStack Virtual:
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, // Estimated row height
overscan: 5, // Render 5 extra items outside viewport
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<ListItem item={items[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}Pagination for server-side data:
function PaginatedList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['items'],
queryFn: ({ pageParam = 0 }) => fetchItems({ offset: pageParam, limit: 20 }),
getNextPageParam: (lastPage) => lastPage.nextOffset,
});
return (
<div>
{data?.pages.flatMap(page => page.items).map(item => (
<ListItem key={item.id} item={item} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}4. Unnecessary Component Re-renders
Problem
Components re-render even when their output hasn't changed, causing performance degradation.
Diagnosis
// React DevTools Profiler or manual logging
function ExpensiveComponent({ data }: Props) {
console.log('ExpensiveComponent rendered'); // Check how often this fires
// Or use React DevTools "Highlight updates when components render"
}Solution
React.memo for expensive components:
const ExpensiveList = React.memo(function ExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{/* Complex rendering */}</li>
))}
</ul>
);
});useCallback for stable function references:
function Parent() {
const [count, setCount] = useState(0);
// ✓ Stable reference — won't cause child re-render
const handleClick = useCallback((id: string) => {
console.log('clicked', id);
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild onClick={handleClick} />
</>
);
}React Compiler (React 19+) — automatic memoization:
// With React Compiler, manual memo/useCallback/useMemo are unnecessary
// The compiler automatically optimizes re-renders
function Parent() {
const [count, setCount] = useState(0);
const handleClick = (id: string) => {
console.log('clicked', id);
};
// React Compiler automatically memoizes this
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild onClick={handleClick} />
</>
);
}5. Memory Leaks — Components Not Cleaning Up
Problem
- Memory usage grows over time
- Browser tab becomes sluggish
- DevTools shows increasing number of detached DOM nodes
Root Cause
- Event listeners not removed on unmount
- Timers (
setInterval,setTimeout) not cleared - WebSocket/EventSource connections not closed
- Subscriptions not unsubscribed
- Async operations updating unmounted components
Solution
Clean up all side effects:
function LiveData({ channelId }: { channelId: string }) {
const [data, setData] = useState<Data | null>(null);
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/${channelId}`);
ws.onmessage = (event) => {
setData(JSON.parse(event.data));
};
const interval = setInterval(() => {
ws.send(JSON.stringify({ type: 'ping' }));
}, 30000);
const handleResize = () => { /* ... */ };
window.addEventListener('resize', handleResize);
// ✓ Clean up everything
return () => {
ws.close();
clearInterval(interval);
window.removeEventListener('resize', handleResize);
};
}, [channelId]);
return <div>{/* render data */}</div>;
}AbortController for async operations:
useEffect(() => {
const controller = new AbortController();
async function loadData() {
try {
const response = await fetch('/api/data', { signal: controller.signal });
const result = await response.json();
setData(result); // Safe — aborted fetch throws, won't reach here
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return;
setError(err);
}
}
loadData();
return () => controller.abort();
}, []);6. White Screen of Death (WSOD)
Problem
Application renders a blank white screen with no visible error. Common in production where error overlay is not available.
Root Cause
- Unhandled JavaScript error crashes the React tree
- Missing Error Boundaries
- Failed dynamic imports
- Invalid JSON parsing
- Accessing properties on
null/undefined
Solution
Error Boundaries at multiple levels:
import { Component, type ErrorInfo, type ReactNode } from 'react';
class ErrorBoundary extends Component<
{ children: ReactNode; fallback?: ReactNode },
{ hasError: boolean; error?: Error }
> {
state = { hasError: false, error: undefined as Error | undefined };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
// Report to error tracking service
reportError({ error, componentStack: info.componentStack });
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Use at multiple levels
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<ErrorBoundary fallback={<SectionError />}>
<MainContent />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError />}>
<Sidebar />
</ErrorBoundary>
</ErrorBoundary>
);
}Retry for dynamic imports:
function lazyWithRetry(importFn: () => Promise<any>, retries = 3) {
return lazy(async () => {
for (let i = 0; i < retries; i++) {
try {
return await importFn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
throw new Error('Failed to load module');
});
}
const Dashboard = lazyWithRetry(() => import('./Dashboard'));7. React Strict Mode Double Rendering
Problem
In development, components render twice, effects run twice:
// Console shows:
// "Component rendered" (×2)
// "Effect fired" (×2)Root Cause
React 18 Strict Mode intentionally double-invokes renders and effects in development to surface impure code and missing cleanup.
Solution
This is expected behavior in development only. The fix is to ensure your code handles it correctly:
// ✗ Problem: effect without cleanup
useEffect(() => {
const connection = createConnection();
connection.connect(); // Connects twice, never disconnects first
}, []);
// ✓ Solution: proper cleanup
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect(); // Cleanup runs between double-invoke
}, []);Do NOT remove Strict Mode to "fix" double rendering. It helps catch real bugs before production.
Summary: Rendering Debugging Workflow
Is the screen blank?
├─ Yes → Check console for errors → Add Error Boundaries
└─ No
├─ Is the UI flickering?
│ └─ Check for hydration mismatches → useEffect for client-only values
├─ Is the UI sluggish?
│ ├─ Profiler shows too many renders → React.memo / useCallback
│ ├─ Large list rendering → Virtualization
│ └─ Memory growing → Check effect cleanup
└─ Is there an infinite loop?
└─ Check for state updates in render body → Move to effects/handlers