ReactPerformanceOptimizationVirtualizationConcurrent Mode

React Performance: The 60 FPS Manifesto (2026) 🏎️

R
React Core Team Observer
Featured Guide 40 min read

"Every 100ms of latency costs 1% in sales."

Google's new metric, Interaction to Next Paint (INP), punishes apps that freeze on click. If your event handler takes >200ms, you fail. Period.

02. Fiber Architecture Deep Dive

To optimize React, you must understand Fiber. Fiber is a reimplementation of the stack, specialized for React components. It allows React to pause work (rendering) and come back to it later.

The "Work Loop"

  • Stack Reconciler (React 15): Recursive. Once it starts rendering, it cannot stop until the bottom of the tree. (Blocks main thread).
  • Fiber Reconciler (React 16+): Iterative. It breaks rendering into "Units of Work". It checks the frame budget (5ms) after every unit. If time is up, it yields to the browser.

Deep Dive: The React Compiler (React 19)

Forget manual memoization. The new React Compiler automatically memoizes components and hooks for you. It understands your code at a low level and inserts `useMemo` where appropriate, ensuring fine-grained updates without the headache.

03. Render vs. Commit Phases

Phase 1: Render (Slow)

React calls your component functions. It compares the old Virtual DOM with the new one.
Key: This is pure JavaScript calculation. No DOM touches. May happen multiple times.

Phase 2: Commit (Fast)

React applies the changes to the real DOM (insert, update, delete).
Key: This must happen in one go to prevent visual glitches.

Optimization Strategy: Prevent the "Render Phase" from running unnecessarily.

04. Virtualization (Windowing)

If you render 10,000 items, you create 10,000 DOM nodes. The browser's Layout/Paint steps will choke. Virtualization renders only what is visible + a small buffer.

"Never render a list without virtualization if it can grow > 100 items."

05. Memoization Strategy

Using `React.memo` everywhere is an anti-pattern (it adds comparison overhead). Use it only on heavy components or huge lists.

// ❌ BAD: Wrapper recreated every render
<HeavyComponent onClick={() => console.log('click')} />

// ✅ GOOD: Stable reference
const handleClick = useCallback(() => console.log('click'), []);
<HeavyComponent onClick={handleClick} />

06. Concurrent Features

Sometimes you must do heavy work. In React 18, you can mark updates as "Transition" (Low Priority). This tells React: "If the user types, interrupt this heavy rendering task to update the input immediately."

const [isPending, startTransition] = useTransition();

// Typing feels instant (High Priority)
setInputValue(e.target.value); 

startTransition(() => {
  // Chart re-render happens in background (Low Priority)
  setChartData(heavyCalculation(e.target.value)); 
});

07. Senior Takeaways

  • 1. Measure First: Never optimize without the React Profiler. You are probably optimizing the wrong thing.
  • 2. State Colocation: Move state down. If only the `Button` needs the state, don't put it in `App`. This prevents re-rendering the whole tree.
  • 3. Context Splitting: Don't put everything in one `AppContext`. Split it into `UserContext`, `ThemeContext`, etc., so consumers don't re-render unnecessarily.

08. FPS Render Lab

Simulate a heavy workload (blocking the main thread) and see how Virtualization and Concurrency fix the frame rate.

Interactive Playground

import React, { useState, useEffect, useMemo, useTransition, useRef } from 'react';

// ==========================================
// 🏎️ REACT PERFORMANCE LAB
// ==========================================

// --- Helper: Simulate Heavy Work (Main Thread blocking) ---
const slowMath = (ms) => {
    const start = performance.now();
    while (performance.now() - start < ms) {
        // Blocks thread
    }
};

const HeavyItem = React.memo(({ index, highlight, isVirtualized }) => {
    // Artificial lag per item
    if (!isVirtualized) slowMath(0.5); // 0.5ms per item = 500ms for 1000 items

    return (
         <div className={`p-3 border-b border-gray-100 dark:border-white/5 flex justify-between items-center h-[50px] ${highlight ? 'bg-yellow-100 dark:bg-yellow-900/20' : 'bg-white dark:bg-[#111]'}`}>
             <span className="font-mono text-xs text-gray-400">ITEM #{index}</span>
             <div className="flex gap-2">
                 <div className="w-16 h-2 bg-gray-200 dark:bg-white/10 rounded"></div>
                 <div className="w-8 h-2 bg-gray-200 dark:bg-white/10 rounded"></div>
             </div>
         </div>
    );
});

export default function PerformanceLab() {
  const [useVirtualization, setUseVirtualization] = useState(false);
  const [useConcurrency, setUseConcurrency] = useState(false);
  const [itemCount, setItemCount] = useState(1000);
  const [inputValue, setInputValue] = useState(""); // High priority
  const [filterQuery, setFilterQuery] = useState(""); // Low priority (transition)
  const [isPending, startTransition] = useTransition();
  const [fps, setFps] = useState(60);
  
  // FPS Meter simulation
  useEffect(() => {
      let frameCount = 0;
      let startTime = performance.now();
      let animId;
      
      const loop = () => {
          frameCount++;
          const now = performance.now();
          if (now - startTime >= 1000) {
              setFps(Math.min(60, frameCount));
              frameCount = 0;
              startTime = now;
          }
          animId = requestAnimationFrame(loop);
      }
      animId = requestAnimationFrame(loop);
      return () => cancelAnimationFrame(animId);
  }, [filterQuery]); // Dependency ensures we catch drops during render

  // Data Generation (Memoized)
  const items = useMemo(() => Array.from({ length: itemCount }, (_, i) => ({ id: i, text: `Item ${i}` })), [itemCount]);
  
  // Filter Logic (Heavy)
  const visibleItems = useMemo(() => {
      // If NOT utilizing concurrency (normal react), this blocks input
      return items.filter(item => {
           // Artificial slowdown for filter
           if(!useConcurrency) slowMath(0.1); 
           return item.text.includes(filterQuery)
      });
  }, [items, filterQuery, useConcurrency]);

  // Input Handler
  const handleChange = (e) => {
      const val = e.target.value;
      setInputValue(val); // Always update input immediately
      
      if (useConcurrency) {
          startTransition(() => {
              setFilterQuery(val);
          });
      } else {
          setFilterQuery(val); // Blocks immediately in standard mode
      }
  };

  return (
    <div className="bg-slate-50 dark:bg-[#0f1115] p-6 lg:p-10 rounded-3xl border border-slate-200 dark:border-white/5 shadow-2xl font-sans min-h-[850px] flex flex-col">
       
        {/* Header HUD */}
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
            
            {/* FPS Meter */}
            <div className="bg-white dark:bg-[#1a1c20] p-6 rounded-2xl border border-slate-200 dark:border-white/5 shadow-sm flex items-center justify-between">
                <div>
                     <div className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">Frame Rate</div>
                     <div className={`text-4xl font-black ${fps < 30 ? 'text-red-500' : fps < 55 ? 'text-yellow-500' : 'text-green-500'}`}>
                         {fps} <span className="text-sm font-medium text-slate-500">FPS</span>
                     </div>
                </div>
                <span></span>
            </div>

            {/* Main Thread Status */}
            <div className="bg-white dark:bg-[#1a1c20] p-6 rounded-2xl border border-slate-200 dark:border-white/5 shadow-sm flex items-center justify-between">
                <div>
                     <div className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">Main Thread</div>
                     <div className={`text-xl font-bold ${fps < 30 ? 'text-red-500' : 'text-green-500'}`}>
                         {fps < 30 ? 'BLOCKED 🛑' : 'IDLE ✅'}
                     </div>
                </div>
                <span className={fps < 30 ? "text-red-500 animate-pulse text-4xl" : "text-slate-200 dark:text-slate-700 text-4xl"}>🖥️</span>
            </div>

             {/* Node Count */}
             <div className="bg-white dark:bg-[#1a1c20] p-6 rounded-2xl border border-slate-200 dark:border-white/5 shadow-sm flex items-center justify-between">
                <div>
                     <div className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">DOM Nodes</div>
                     <div className="text-xl font-bold text-slate-700 dark:text-slate-200">
                         {useVirtualization ? '~20 (Virtualized)' : `${visibleItems.length} (Raw)`}
                     </div>
                </div>
                <span className="text-4xl">📚</span>
            </div>
        </div>

        <div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-8 overflow-hidden">
            
            {/* CONTROLS */}
            <div className="space-y-6">
                
                {/* Mode Toggles */}
                 <div className="bg-white dark:bg-[#1a1c20] p-6 rounded-2xl border border-slate-200 dark:border-white/5 shadow-sm">
                     <h4 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Optimizations</h4>
                     <div className="space-y-4">
                         <div className="flex items-center justify-between">
                             <div className="flex items-center gap-2">
                                 <span className={useVirtualization ? "text-green-500" : "text-gray-400"}></span>
                                 <span className="font-bold text-sm text-slate-700 dark:text-slate-300">Virtualization</span>
                             </div>
                             <button onClick={() => setUseVirtualization(!useVirtualization)} className={`w-12 h-6 rounded-full transition-colors relative ${useVirtualization ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700'}`}>
                                 <div className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform ${useVirtualization ? 'translate-x-6' : ''}`}></div>
                             </button>
                         </div>
                         <div className="flex items-center justify-between">
                             <div className="flex items-center gap-2">
                                 <span className={useConcurrency ? "text-blue-500" : "text-gray-400"}>📊</span>
                                 <span className="font-bold text-sm text-slate-700 dark:text-slate-300">Concurrent Mode</span>
                                 {useConcurrency && <span className="text-[10px] bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-200 px-1 rounded font-bold">REACT 18</span>}
                             </div>
                             <button onClick={() => setUseConcurrency(!useConcurrency)} className={`w-12 h-6 rounded-full transition-colors relative ${useConcurrency ? 'bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'}`}>
                                 <div className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform ${useConcurrency ? 'translate-x-6' : ''}`}></div>
                             </button>
                         </div>
                     </div>
                 </div>

                 {/* Input Stress Test */}
                <div className="bg-white dark:bg-[#1a1c20] p-6 rounded-2xl border border-slate-200 dark:border-white/5 shadow-sm">
                     <h4 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Input Stress Test</h4>
                     <p className="text-xs text-slate-500 mb-4">Typing forces a re-filter. Look for keystroke lag.</p>
                     
                     <input 
                        type="text" 
                        value={inputValue}
                        onChange={handleChange}
                        placeholder="Type quickly to test lag..."
                        className="w-full bg-slate-50 dark:bg-black/50 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 outline-none focus:ring-2 ring-blue-500 transition-all font-mono"
                     />
                     <div className="mt-2 text-[10px] text-slate-400 flex justify-between">
                         <span>Status: {isPending ? <span className="text-yellow-500 font-bold">SUSPENDED (Concurrent)</span> : <span className="text-green-500 font-bold">SYNC</span>}</span>
                     </div>
                </div>

                <div className="bg-yellow-50 dark:bg-yellow-900/10 p-6 rounded-2xl border border-yellow-200 dark:border-yellow-900/20">
                     <div className="flex gap-2 items-start">
                         <span>⚠️</span>
                         <p className="text-xs text-yellow-800 dark:text-yellow-200 leading-relaxed">
                             <strong>Without Virtualization:</strong> Rendering 1000 items creates 1000 `divs`. This chokes frame rates. <br/>
                             <strong>Without Concurrency:</strong> Typing blocks the thread while filtering.
                         </p>
                     </div>
                </div>
            </div>

            {/* RENDER VIEWPORT */}
            <div className="lg:col-span-2 bg-white dark:bg-[#000] rounded-2xl border border-slate-200 dark:border-slate-800 overflow-hidden relative flex flex-col h-[500px] lg:h-auto">
                <div className="bg-gray-100 dark:bg-[#111] px-4 py-2 text-[10px] font-bold text-gray-500 uppercase tracking-widest border-b border-gray-200 dark:border-white/5 flex justify-between">
                    <span>Render Output</span>
                    <span>{visibleItems.length} Results</span>
                </div>
                
                {/* THE LIST */}
                <div className="flex-1 overflow-y-auto relative">
                    {useVirtualization ? (
                       <VirtualList items={visibleItems} itemHeight={50} />
                    ) : (
                        <div className="flex flex-col">
                             {visibleItems.map((item, i) => (
                                 <HeavyItem key={item.id} index={i} highlight={false} isVirtualized={false} />
                             ))}
                        </div>
                    )}
                </div>

                {isPending && (
                    <div className="absolute top-2 right-2 bg-yellow-500 text-black text-[10px] font-bold px-2 py-1 rounded shadow animate-pulse">
                        RENDERING IN BACKGROUND...
                    </div>
                )}
            </div>
        </div>
    </div>
  );
}

// ----------------------------------------------------
// 🐇 SIMPLE VIRTUAL LIST IMPLEMENTATION
// ----------------------------------------------------
const VirtualList = ({ items, itemHeight }) => {
    const [scrollTop, setScrollTop] = useState(0);
    const containerHeight = 600; 
    
    const totalHeight = items.length * itemHeight;
    const startIndex = Math.floor(scrollTop / itemHeight);
    const endIndex = Math.min(
        items.length - 1,
        Math.floor((scrollTop + containerHeight) / itemHeight)
    );
    
    // Create visible items
    const visibleItems = [];
    for (let i = startIndex; i <= endIndex; i++) {
        const item = items[i];
        if(!item) continue;
        visibleItems.push(
            <div 
                key={item.id}
                style={{
                    position: 'absolute',
                    top: i * itemHeight,
                    width: '100%',
                    height: itemHeight
                }}
            >
                <HeavyItem index={i} highlight={false} isVirtualized={true} />
            </div>
        );
    }

    return (
        <div 
            className="h-full overflow-y-auto relative"
            onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
        >
            <div style={{ height: totalHeight, position: 'relative' }}>
                {visibleItems}
            </div>
        </div>
    )
}