React Performance Metrics: Core Web Vitals & DevTools Profiler
Question 1: What are Core Web Vitals and how to measure React app performance?β
Main Answerβ
Core Web Vitals are three key metrics that Google uses to measure user experience on web pages. They directly impact your site's search ranking and user satisfaction. The three metrics are:
- Largest Contentful Paint (LCP) - When the largest visible element loads (target: < 2.5 seconds)
- First Input Delay (FID) - Time from user interaction to browser response (target: < 100ms)
- Cumulative Layout Shift (CLS) - Visual stability during page load (target: < 0.1)
To measure these in a React app, use the web-vitals library which provides a simple callback interface:
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
// Measure all Core Web Vitals
getCLS(console.log); // CLS metric
getFID(console.log); // FID metric
getLCP(console.log); // LCP metric
getTTFB(console.log); // Time to First Byte
// Example output:
// {name: 'LCP', value: 2100, rating: 'good', delta: 2100, id: '...'}
Lighthouse audits automatically test these metrics. You can also integrate them into your analytics pipeline to track performance across real user sessions (Real User Monitoring - RUM).
<details> <summary><strong>π Deep Dive: Core Web Vitals Internals</strong></summary>
Core Web Vitals Internalsβ
Largest Contentful Paint (LCP)
LCP is determined by the browser's PerformanceObserver API and measures when the largest content element (image, video, or text block) becomes visible to the user. The browser continuously updates LCP during page load - the final value is what matters for metrics.
// How PerformanceObserver tracks LCP internally
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP candidate:', {
element: lastEntry.element,
startTime: lastEntry.startTime,
renderTime: lastEntry.renderTime,
loadTime: lastEntry.loadTime,
size: lastEntry.size
});
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
The browser determines LCP by analyzing:
- Images with
srcor CSSbackground-image <img>,<image>in SVG,<video>poster images- Text elements (paragraphs, headings, list items, etc.)
- Elements rendered via canvas
Critical timing phases for LCP in React:
- Initial HTML parse (50-200ms) - Browser downloads and parses HTML
- JavaScript execution (0-500ms) - React bootstrap, component initialization
- Component render (200-1000ms) - React renders tree, creates DOM
- Image fetch + decode (500-2000ms) - Largest image downloads and decodes
- Style application (0-100ms) - CSSOM creation and style computation
First Input Delay (FID)
FID measures the delay between user input and the browser's ability to begin processing event handlers. This happens when JavaScript is blocking the main thread.
// Browser measures FID like this (simplified)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const delay = entry.processingStart - entry.startTime;
console.log('FID:', {
inputEvent: entry.name,
delay: delay, // This is FID
processingTime: entry.duration - delay
});
}
});
observer.observe({ entryTypes: ['first-input'] });
In React apps, FID is impacted by:
- Long-running JavaScript (bundle size, minification issues)
- Heavy re-renders during user interaction
- Third-party scripts (analytics, ads, tracking)
- Blocking network requests in event handlers
Cumulative Layout Shift (CLS)
CLS measures the sum of all unexpected layout shifts during the entire page lifetime. Each shift is scored as: (impact fraction Γ distance fraction)
// Browser calculates CLS via LayoutShift entries
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
const clsValue = entry.hadRecentInput ? 0 : entry.value;
console.log('Layout shift:', {
value: entry.value,
sources: entry.sources, // Elements that shifted
hadRecentInput: entry.hadRecentInput // Ignore recent inputs
});
}
}
});
observer.observe({ entryTypes: ['layout-shift'] });
Common CLS culprits in React:
- Dynamically loaded ads without reserved space
- Images without explicit width/height (before lazy-loading)
- Injected content without height reservation
- Web fonts causing text reflow (FOUT/FOIT)
Web Vitals Library Integrationβ
The web-vitals library wraps these APIs with utility functions:
// Advanced: Track metrics with custom reporting
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendMetricToAnalytics(metric) {
// Send to your analytics backend
navigator.sendBeacon('/analytics', JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta, // Delta since last report
id: metric.id, // Unique identifier
url: window.location.href,
userAgent: navigator.userAgent
}));
}
// Report all metrics
getCLS(sendMetricToAnalytics);
getFID(sendMetricToAnalytics);
getLCP(sendMetricToAnalytics);
getTTFB(sendMetricToAnalytics);
getFCP(sendMetricToAnalytics);
Thresholds and ratings:
const METRIC_THRESHOLDS = {
LCP: { good: 2500, 'needs-improvement': 4000 },
FID: { good: 100, 'needs-improvement': 300 },
CLS: { good: 0.1, 'needs-improvement': 0.25 },
FCP: { good: 1800, 'needs-improvement': 3000 },
TTFB: { good: 600, 'needs-improvement': 1800 }
};
React-Specific Performance Considerationsβ
React renders happen in two phases:
- Render phase (interruptible) - Component render, calculations
- Commit phase (synchronous) - DOM updates, side effects
Long renders block user input, directly impacting FID:
// Bad: Blocks main thread for 500ms during render
function HeavyComponent() {
const data = useMemo(() => {
// Synchronous 500ms calculation
let result = [];
for (let i = 0; i < 10000000; i++) {
result.push(i * i);
}
return result;
}, []);
return <div>{data.length}</div>;
}
// Good: Move to web worker or schedule with startTransition
function HeavyComponent() {
const [data, setData] = useState([]);
useTransition(); // Marks non-urgent updates as transitions
useEffect(() => {
// Offload to web worker
const worker = new Worker('heavy-calculation.worker.js');
worker.postMessage({ type: 'CALCULATE', limit: 10000000 });
worker.onmessage = (e) => setData(e.data);
}, []);
return <div>{data.length}</div>;
}
</details>
<details> <summary><strong>π Real-World Scenario: E-commerce Performance Crisis</strong></summary>
Company: E-commerce Platform
Problem: The product listing page had poor Core Web Vitals after a React 18 upgrade. Users complained about slow interactions and layout jumps.
Initial metrics (from Lighthouse & Sentry RUM):
- LCP: 4.2 seconds (poor - target < 2.5s)
- FID: 280ms (poor - target < 100ms)
- CLS: 0.35 (poor - target < 0.1)
Root cause analysis:
- LCP: 4.2s - Largest image in hero section was 3.2MB unoptimized JPEG
- FID: 280ms - JavaScript bundle was 850KB, took 220ms to parse + execute
- CLS: 0.35 - Product images without aspect ratio caused reflow after loading
Solution implementation:
// 1. Fix LCP: Optimize images and use lazy-loading
function ProductHero() {
return (
<picture>
{/* WebP for modern browsers */}
<source srcSet="hero.webp 1x, hero@2x.webp 2x" type="image/webp" />
{/* Fallback JPEG - now 450KB (87% smaller!) */}
<img
src="hero.jpg"
alt="Hero product"
loading="eager" // LCP candidate
fetchPriority="high" // Prioritize in resource queue
width={1200} // Explicit dimensions prevent CLS
height={600}
/>
</picture>
);
}
// 2. Fix FID: Code splitting and lazy components
const ProductReviews = lazy(() => import('./ProductReviews'));
const ProductRelated = lazy(() => import('./ProductRelated'));
function ProductPage() {
return (
<>
<ProductHero /> {/* Critical - loads immediately */}
<ProductDetails />
{/* Non-critical - lazy loaded below fold */}
<Suspense fallback={<div>Loading...</div>}>
<ProductReviews />
<ProductRelated />
</Suspense>
</>
);
}
// 3. Fix CLS: Reserve space with aspect-ratio CSS
const ProductImage = styled.img`
width: 100%;
aspect-ratio: 4 / 3; /* Reserve space before image loads */
object-fit: cover;
`;
// 4. Defer non-critical JavaScript
function ProductMetadata() {
const [reviews, setReviews] = useState(null);
useEffect(() => {
// Delay non-critical data fetch to after page interactive
const timeoutId = setTimeout(() => {
fetch('/api/reviews').then(r => r.json()).then(setReviews);
}, 5000); // 5s delay after initial render
return () => clearTimeout(timeoutId);
}, []);
}
Results after optimization:
Before: LCP=4.2s, FID=280ms, CLS=0.35
After: LCP=1.8s, FID=65ms, CLS=0.08
Improvement: 57% β, 77% β, 77% β
Core Web Vitals score: 24/100 β 94/100 (in Lighthouse)
Monitoring implementation:
// Send real metrics to Sentry for RUM tracking
import * as Sentry from "@sentry/react";
import { getCLS, getFID, getLCP } from 'web-vitals';
getCLS(metric => {
Sentry.captureMessage(`CLS: ${metric.value}`, {
level: metric.rating === 'good' ? 'info' : 'warning',
tags: { metric: 'CLS' }
});
});
getFID(metric => {
Sentry.captureMessage(`FID: ${metric.value}ms`, {
level: metric.rating === 'good' ? 'info' : 'warning',
tags: { metric: 'FID' }
});
});
getLCP(metric => {
Sentry.captureMessage(`LCP: ${metric.value}ms`, {
level: metric.rating === 'good' ? 'info' : 'warning',
tags: { metric: 'LCP' }
});
});
</details>
<details> <summary><strong>βοΈ Trade-offs: Measurement and Optimization Strategies</strong></summary>
Measurement Strategy Comparison:
| Aspect | Lab Testing (Lighthouse) | Field Testing (RUM) | Synthetic Monitoring |
|---|---|---|---|
| Accuracy | Simulated throttling | Real users, real network | Controlled, consistent |
| Coverage | Single snapshot | All user sessions | Periodic checks |
| Cost | Free | Analytics platform fee | Monitoring service fee |
| Latency insight | No | Yes (network variability) | Yes (predictable) |
| Device variety | Limited | High (varies) | Standardized |
| Best for | Local development | Production insights | Alerting/SLOs |
Trade-offs in optimization choices:
// TRADE-OFF 1: Image optimization vs. quality loss
// Option A: Aggressive WebP compression
<img src="product.webp?quality=60" /> // 50KB, slightly visible artifacts
// Option B: Higher quality WebP
<img src="product.webp?quality=85" /> // 180KB, pixel-perfect
// Option C: AVIF (newer, better compression)
<picture>
<source srcSet="product.avif" type="image/avif" /> // 40KB, excellent
<img src="product.webp" /> // Fallback
</picture>
// TRADE-OFF 2: Lazy-loading vs. eager loading
// Option A: Lazy load all below-fold images
<img loading="lazy" src="..." /> // Slower LCP, better initial load
// Option B: Eager load LCP candidate
<img fetchPriority="high" src="..." /> // Faster LCP, may block other resources
// TRADE-OFF 3: Code splitting vs. request overhead
// Option A: One large bundle
// Pros: Single HTTP request, no waterfall
// Cons: Blocks interaction until all code loads
// Option B: Code-split into 5 chunks
// Pros: Load only critical code initially
// Cons: 4 additional HTTP requests, potential latency
// Choose based on: network speed (RTT), bundle size, cache effectiveness
Tool comparison for measurement:
// Lighthouse (Lab)
// β
Comprehensive audit, reproducible
// β Doesn't capture real user variability
// Run: npx lighthouse https://example.com
// Web Vitals (Field)
// β
Real user data, production metrics
// β Privacy constraints, sampling requirements
// Implementation: import { getLCP } from 'web-vitals'
// Chrome DevTools (Debugging)
// β
Detailed waterfall, frame-by-frame analysis
// β Single-device view, not representative
// Access: DevTools > Performance tab
// PageSpeed Insights (Lab + Field)
// β
Combines Lighthouse + Chrome User Experience Report
// β Limited customization
// Visit: pagespeed.web.dev
// Datadog/New Relic (Continuous monitoring)
// β
Alerting, trend analysis, correlation with errors
// β Expensive, requires integration
// Cost: $200-2000+/month
React-specific optimization trade-offs:
// TRADE-OFF: useMemo optimization vs. memory overhead
// Bad: Over-memoization causes memory bloat
const Component = React.memo(({ items }) => {
const sorted = useMemo(() => items.sort(...), [items]); // Always recalculates
const filtered = useMemo(() => sorted.filter(...), [sorted]); // Unnecessary memo
return <div>{filtered}</div>;
});
// Good: Memoize only expensive operations
const Component = React.memo(({ items }) => {
// Only memoize if calculation > 1ms
const result = useMemo(
() => items.sort(...).filter(...),
[items]
);
return <div>{result}</div>;
});
// TRADE-OFF: Prefetching vs. bandwidth usage
// Aggressive prefetch: All routes pre-loaded (extra 300KB)
const routes = ['dashboard', 'settings', 'profile'];
routes.forEach(route => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = `/js/${route}.chunk.js`;
document.head.appendChild(link);
});
// Conservative prefetch: Only likely next routes (30KB extra)
if (userRole === 'admin') {
prefetchRoute('/admin-dashboard');
}
</details>
<details> <summary><strong>π¬ Explain to Junior: Core Web Vitals in Simple Terms</strong></summary>
Core Web Vitals: Simple Analogy
Think of your React app like a restaurant:
- LCP is "time until the appetizer arrives" - how long before the main content shows up
- FID is "how fast the waiter responds to your request" - how quick the browser reacts to clicks
- CLS is "how much the table shakes" - visual stability as elements load and shift
// Real scenario: Loading a product page
// 1. LCP: Hero image takes 4 seconds to load
// β Bad: User waits 4 seconds to see anything
// β
Good: Page shows in 1.5 seconds, hero loads during that time
// 2. FID: Clicking "Add to Cart" takes 300ms to respond
// β Bad: User clicks, waits 300ms, worries button didn't work
// β
Good: Instant response (50ms), feedback is immediate
// 3. CLS: Price shifts down 200px after ad loads
// β Bad: User's eyes moving constantly, disorienting experience
// β
Good: All elements reserve space, nothing shifts
Measuring Core Web Vitals: Step-by-step
// Step 1: Install web-vitals
// npm install web-vitals
// Step 2: Import and use
import { getCLS, getFID, getLCP } from 'web-vitals';
// Step 3: Create callback function
function handleMetric(metric) {
console.log(`${metric.name}: ${metric.value}ms`);
// Send to your backend
fetch('/metrics', {
method: 'POST',
body: JSON.stringify(metric)
});
}
// Step 4: Measure
getCLS(handleMetric);
getFID(handleMetric);
getLCP(handleMetric);
// Output examples:
// LCP: 2100ms β
Good (< 2500ms)
// FID: 80ms β
Good (< 100ms)
// CLS: 0.08 β
Good (< 0.1)
Interview Answer Template:
"Core Web Vitals are three metrics Google uses to measure user experience. LCP measures when the main content loads, FID measures how fast the browser responds to clicks, and CLS measures visual stability.
To measure them in React, I use the web-vitals library which provides simple callbacks for each metric. I also run Lighthouse audits regularly to check performance.
For a real-world example, I optimized an e-commerce product page by: first, using WebP images instead of JPEG, which cut LCP from 4.2s to 1.8s; second, code-splitting to reduce the JavaScript bundle, which improved FID from 280ms to 65ms; and third, adding aspect-ratio CSS to prevent layout shifts from images loading, which improved CLS from 0.35 to 0.08.
The key insight is that Core Web Vitals directly impact user experience and SEO ranking, so optimizing them should be part of your regular development workflow."
</details>
Question 2: How to use React DevTools Profiler for performance optimization?β
Main Answerβ
React DevTools Profiler is a browser extension that measures component render times and helps identify performance bottlenecks. It records the entire React component tree during renders, showing exactly which components took time and why they rendered.
To use it:
- Install React DevTools extension in Chrome or Firefox
- Open the Profiler tab in DevTools
- Click the record button to start measuring
- Interact with your app (click buttons, scroll, etc.)
- Stop recording to see the results
The profiler shows:
- Render duration - Time spent in React's render phase
- Component commits - Which components updated the DOM
- Why a component rendered - Changed props, state, or context
// React 18+ includes Profiler API for measuring components
import { Profiler } from 'react';
function MyApp() {
function onRenderCallback(
id, phase, actualDuration, baseDuration, startTime, commitTime
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
return (
<Profiler id="MyApp" onRender={onRenderCallback}>
<YourComponent />
</Profiler>
);
}
Use the profiler data to identify slow components, then optimize with memoization, code splitting, or state restructuring.
<details> <summary><strong>π Deep Dive: React DevTools Profiler Architecture</strong></summary>
React DevTools Profiler Architectureβ
The React DevTools Profiler intercepts React's internal hooks to measure rendering performance. React exposes timing information via:
- Hook Protocol - Special communication between React and extension
- Fiber Architecture - React's internal representation of components
- Reconciliation Process - How React diffs and updates components
// How React measures renders internally (simplified)
class Profiler {
constructor() {
this.measurements = [];
}
recordRender(fiberRoot, phase, startTime) {
const duration = performance.now() - startTime;
// Walk the fiber tree
this.recordFiberTree(fiberRoot, {
id: fiberRoot.componentName,
phase: phase, // 'mount' or 'update'
actualDuration: duration,
baseDuration: this.calculateBaseDuration(fiberRoot)
});
}
recordFiberTree(fiber, parentData) {
// Record each fiber (component) in the tree
const timing = {
name: fiber.elementType?.name || 'Unknown',
phase: parentData.phase,
actualDuration: fiber._debugHookTypes?.length || 0,
startTime: performance.now()
};
this.measurements.push(timing);
// Traverse to children
if (fiber.child) {
this.recordFiberTree(fiber.child, parentData);
}
}
}
Profiler Metrics Explainedβ
Render Phase Duration:
The render phase is where React calculates what DOM changes are needed. This phase can be interrupted.
// In the profiler, you'll see:
// Component A: 45ms (render phase)
// ββ Component B: 20ms
// ββ Component C: 15ms
// ββ Component D: 10ms
// This hierarchy shows component tree nesting
// Indented components are children of parent components
Commit Phase Duration:
The commit phase is when React actually updates the DOM and runs effects. This phase is synchronous and cannot be interrupted.
// Commit phase includes:
// 1. DOM updates (1-5ms usually)
// 2. Effect cleanup functions (useEffect cleanup)
// 3. Layout effects (useLayoutEffect)
// 4. Effect callbacks (useEffect callbacks)
// If commit takes > 16ms (on 60fps display), frame drops occur
// Example:
// Commit phase: 22ms
// ββ DOM updates: 3ms
// ββ Effects: 15ms (TOO LONG!)
// ββ Layout effects: 4ms
// ββ Callbacks: 0ms
Identifying Render Causesβ
React DevTools shows why a component rendered:
// When you click on a component in the profiler, it shows:
// "Component 'ProductList' rendered"
// Reasons:
// β Prop 'items' changed from [...] to [...]
// β State 'page' changed from 1 to 2
// β Context 'theme' changed
// This helps identify:
// 1. Unnecessary prop changes (memoization needed)
// 2. State that could be local instead of global
// 3. Context that's updating too frequently
React Profiler API for Programmatic Measurementβ
Beyond the DevTools extension, React provides the Profiler API:
import { Profiler } from 'react';
function onRenderCallback(
id, // 'string' from Profiler id prop
phase, // 'mount' or 'update'
actualDuration, // Render + commit time for this component
baseDuration, // Estimated time without memoization
startTime, // React timestamp when render started
commitTime // React timestamp when render was committed
) {
// Send to analytics or log
const isSlowRender = actualDuration > 1; // > 1ms
if (isSlowRender) {
console.warn(`${id} (${phase}) took ${actualDuration}ms`);
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Header />
<MainContent />
<Sidebar />
</Profiler>
);
}
// You can nest multiple Profilers
function MainContent() {
return (
<Profiler id="MainContent" onRender={onRenderCallback}>
<Article />
<Comments />
</Profiler>
);
}
// Actual output:
// MainContent (update) took 5.2ms
// ββ Article (update) took 3ms
// ββ Comments (update) took 2.2ms
Flame Chart Visualizationβ
The DevTools Profiler shows a flame chart where:
- X-axis: Time (milliseconds)
- Y-axis: Component nesting hierarchy
- Bar width: Duration of render
- Color coding: Lighter = faster, darker = slower
Timeline visualization:
0ms 100ms 200ms 300ms
|__________|__________|__________|___________|
[App render ]
[Header ]
[Nav ]
[Search ] (SLOW - 150ms!)
[Sidebar ]
[Menu ]
[Profile]
Live Profiling: Real Exampleβ
function EcommercePage() {
const [products, setProducts] = useState([]);
const [filters, setFilters] = useState({});
return (
<Profiler id="EcommercePage" onRender={logRender}>
<FilterPanel filters={filters} onChange={setFilters} />
<ProductGrid products={products} filters={filters} />
</Profiler>
);
}
// When you open DevTools Profiler and record:
// 1. Click filter checkbox
// 2. See in profiler:
// EcommercePage (update): 45ms
// ββ FilterPanel (update): 10ms
// ββ ProductGrid (update): 35ms
// ββ ProductCard (update): 2ms Γ 15 cards = 30ms
// ββ Pagination (update): 5ms
// Insight: ProductGrid is slow because it re-renders all 15 cards
// Solution: Memoize ProductCard to prevent re-renders when props don't change
</details>
<details> <summary><strong>π Real-World Scenario: Social Media Feed Performance</strong></summary>
Company: Social Media Feed App (Twitter-like)
Problem: The feed was sluggish. When new tweets loaded, the entire feed stuttered, and scrolling became janky. Users reported "feels slow even though it's not actually slow."
Using DevTools Profiler to diagnose:
// Step 1: Profile with DevTools extension
// Open DevTools > React > Profiler tab
// Record while scrolling feed
// Results showed:
FeedContainer (update): 420ms π΄ VERY SLOW
ββ TweetList (update): 410ms
β ββ Tweet #1 (update): 25ms
β ββ Tweet #2 (update): 28ms
β ββ Tweet #100 (update): 24ms Γ 100 = 2400ms!! π΄π΄π΄
ββ SidebarAds (update): 8ms
ββ TrendingSection (update): 2ms
// Problem: ALL 100 tweets re-render when ANY tweet updates!
// Root cause: Global context change β entire feed re-renders
Investigation with Profiler API:
// Step 2: Add Profiler API to pinpoint exactly what changed
import { Profiler } from 'react';
function TweetList({ tweets }) {
return (
<Profiler id="TweetList" onRender={analyzeRender}>
{tweets.map(tweet => (
<Profiler key={tweet.id} id={`Tweet-${tweet.id}`} onRender={analyzeRender}>
<Tweet tweet={tweet} />
</Profiler>
))}
</Profiler>
);
}
function analyzeRender(id, phase, actualDuration, baseDuration) {
const waisted = baseDuration - actualDuration;
console.log({
component: id,
phase,
actualTime: actualDuration.toFixed(2) + 'ms',
estimatedWithoutMemo: baseDuration.toFixed(2) + 'ms',
wasMemoCacheHit: actualDuration < baseDuration / 2
});
}
// Output when scrolling (user didn't change tweets, just scrolled):
// TweetList (update): 410ms actual, 420ms base β memo NOT working
// Tweet-123 (update): 25ms actual, 25ms base β re-rendered
// Tweet-456 (update): 28ms actual, 28ms base β re-rendered
// ^ ALL tweets re-rendered even though we only scrolled!
Root cause: Context causing unnecessary renders
// Original (problematic) code:
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<FeedContainer />
</ThemeContext.Provider>
);
}
function FeedContainer() {
const { theme } = useContext(ThemeContext);
return (
<div className={theme}>
<TweetList /> {/* Entire list re-renders when theme changes! */}
</div>
);
}
function Tweet({ tweet }) {
const { theme } = useContext(ThemeContext); // EVERY tweet consumes context
return <div className={`tweet ${theme}`}>{tweet.text}</div>;
}
// Problem: When ANY context value changes, ALL consumers re-render
// We changed theme? All 100 tweets re-render (even if they don't use it)
Solution: Context splitting + memoization
// Step 3: Fix with context optimization
const ThemeContext = createContext();
// Separate context for theme (rarely changes)
const UserContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<FeedContainer />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// Memoize Tweet to prevent re-renders when context doesn't change
const Tweet = memo(function Tweet({ tweet }) {
const { theme } = useContext(ThemeContext);
return (
<div className={`tweet ${theme}`}>
<p>{tweet.text}</p>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if tweet content changed
return prevProps.tweet.id === nextProps.tweet.id;
});
// Additionally: Split context into read-only and write parts
const ThemeReadContext = createContext();
const ThemeWriteContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeReadContext.Provider value={theme}>
<ThemeWriteContext.Provider value={setTheme}>
{children}
</ThemeWriteContext.Provider>
</ThemeReadContext.Provider>
);
}
// Now only components that READ theme re-render, not writers
Results after optimization:
Before optimization:
FeedContainer (update): 420ms
ββ TweetList (update): 410ms
ββ Tweet Γ 100: 2400ms total π΄
After optimization:
FeedContainer (update): 15ms
ββ TweetList (update): 2ms β
ββ Tweet Γ 100: 2ms (memoized, skipped) β
Improvement: 420ms β 15ms (96% reduction!)
Scroll FPS: 30fps β 60fps (smooth!)
Profiler output after fix:
// When scrolling (NO tweets changed):
TweetList (update): 2ms, baseDuration: 400ms, cached: YES β
Tweet-123 (update): 0ms (memoized, skipped)
Tweet-456 (update): 0ms (memoized, skipped)
// When a new tweet arrives:
TweetList (update): 8ms
Tweet-NEW (mount): 2.5ms β
</details>
<details> <summary><strong>βοΈ Trade-offs: Profiling Tools and Optimization Strategies</strong></summary>
Profiler Measurement Impact:
| Aspect | DevTools Profiler | Profiler API | Chrome DevTools Performance Tab |
|---|---|---|---|
| Overhead | 10-15% slower | 2-5% overhead | 1-2% overhead |
| Accuracy | Good (with overhead) | High | Very high |
| What it measures | Component renders | Component timing | Full page waterfall |
| Best for | Finding slow components | Analytics/monitoring | Finding bottlenecks |
| Production use | No (extension only) | Yes (conditional) | No (DevTools only) |
Trade-offs in optimization strategies:
// TRADE-OFF 1: Memoization memory overhead vs. render time
// Option A: Memoize everything
const Tweet = memo(function Tweet({ tweet }) {
return <div>{tweet.text}</div>;
});
// Pros: Tweet never re-renders unnecessarily
// Cons: Takes extra ~100 bytes per memoized component
// Option B: Selective memoization
const Tweet = memo(function Tweet({ tweet, onLike }) {
// Only memoize expensive components
return (
<div>
<ExpensiveRichText text={tweet.text} /> {/* Memoize this */}
<SimpleButton onClick={onLike}>Like</SimpleButton> {/* Don't memoize */}
</div>
);
}, (prev, next) => prev.tweet.id === next.tweet.id);
// Pros: Minimal overhead, selective optimization
// Cons: Must manually decide what to memoize
// TRADE-OFF 2: Context granularity vs. complexity
// Option A: Single context (simple, but causes renders)
const AppContext = createContext();
<AppContext.Provider value={{ user, theme, notifications }}>
// Option B: Multiple contexts (complex, but no unnecessary renders)
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
// Pros: Precise rendering
// Cons: More code, harder to manage
// TRADE-OFF 3: Synchronous state updates vs. responsiveness
// Option A: Immediate state update (blocks UI)
function handleLike(tweetId) {
setLikedTweets([...likedTweets, tweetId]); // Blocks immediately
// If 100+ tweets liked, might take 50ms (frames drop)
}
// Option B: Deferred update with startTransition (responsive but delayed)
function handleLike(tweetId) {
startTransition(() => {
setLikedTweets([...likedTweets, tweetId]); // Non-blocking
});
// UI stays responsive, but update happens in background
}
When to use each profiling tool:
// Use DevTools Profiler when:
// β
Debugging specific rendering issues
// β
Want visual timeline of renders
// β
Analyzing real user interactions
// β
Need to understand component tree timing
// Use Profiler API when:
// β
Need to send metrics to analytics
// β
Want conditional profiling in production
// β
Tracking performance regressions
// β
Building custom monitoring dashboard
// Use Chrome Performance tab when:
// β
Profiling entire page lifecycle
// β
Analyzing rendering at pixel level
// β
Measuring JavaScript execution time
// β
Checking network waterfall
// Example: Combined approach for real monitoring
const enableProfiling = process.env.NODE_ENV === 'development' ||
location.hostname === 'staging.myapp.com';
export function createProfiler(componentName) {
return enableProfiling ? (
<Profiler id={componentName} onRender={sendToAnalytics} />
) : null;
}
// In staging: Send metrics to analytics backend
// In production: Disabled to avoid overhead
</details>
<details> <summary><strong>π¬ Explain to Junior: Using DevTools Profiler</strong></summary>
React DevTools Profiler: Simple Analogy
Think of React DevTools Profiler like a stopwatch for your app:
- Recording a session = Starting the stopwatch while user clicks buttons
- Component render = One person doing a task
- Duration = How long that task took
- Why it rendered = What triggered the task to run
// Simple example: Click a button, see what re-renders
function Counter() {
const [count, setCount] = useState(0);
return (
<Profiler id="Counter" onRender={logRender}>
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<ExpensiveComponent count={count} />
</div>
</Profiler>
);
}
function ExpensiveComponent({ count }) {
// Simulate expensive calculation
const result = useMemo(() => {
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum * count;
}, [count]);
return <div>Result: {result}</div>;
}
function logRender(id, phase, actualDuration) {
console.log(`${id} (${phase}): ${actualDuration.toFixed(2)}ms`);
}
// Click button, see in console:
// Counter (update): 2.5ms
How to use React DevTools Profiler (step-by-step):
// Step 1: Install React DevTools extension (if not already)
// Chrome Web Store: React Developer Tools
// Step 2: Open your React app in Chrome
// You should see React icon in top-right
// Step 3: Open DevTools (F12), go to "React" tab, click "Profiler"
// Step 4: Click red record button βΊοΈ in Profiler tab
// Step 5: Interact with your app (click buttons, scroll, etc.)
// Step 6: Click record button again to stop βΉοΈ
// Step 7: You'll see timeline showing:
// - Which components rendered
// - How long each took
// - What triggered the render
// Step 8: Click on a component to see:
// - "Why did this render?" section
// - Props that changed
// - State that changed
// - Context that changed
Real example: Finding a slow component
// Let's say your app feels laggy when scrolling through a list
// Step 1: Open DevTools Profiler and record while scrolling
// Step 2: Stop recording - you see:
// FeedList (update): 450ms π΄ SLOW
// ββ Post (update): 5ms Γ 50 posts = 250ms
// ββ Comments section: 150ms
// ββ Sidebar: 50ms
// Step 3: The Posts section is slow. Click on one Post component.
// Step 4: See "Why did this render?" shows:
// β Props 'feed' changed
// β Context 'theme' changed β THIS!
// Step 5: Conclusion: Posts are re-rendering because theme changed,
// but they don't even use the theme!
// Step 6: Solution: Use React.memo() to skip re-render if props don't change
const Post = memo(({ post }) => {
return <article>{post.title}</article>;
});
// Step 7: Re-profile after fix:
// FeedList (update): 100ms β
77% faster!
// ββ Post: skipped (memoized) 0ms β
Interview Answer Template:
"React DevTools Profiler is a browser extension that helps me identify slow components. I use it by recording a session while interacting with the app, then looking at the timeline to see which components took time to render.
The key insight is the 'why did this render' section - it shows me if a component re-rendered because of props, state, or context changes. Then I can decide if it was necessary.
For example, in a social media feed, I found that all 50 post components were re-rendering every time the theme changed, even though posts didn't use the theme. By wrapping the Post component with React.memo(), I prevented unnecessary re-renders and reduced render time from 450ms to 100ms.
The Profiler API also lets me add timing measurements to specific components and send data to analytics for production monitoring."
</details>
Source: performance-01-metrics.md