What is the difference between render phase and commit phase in React Fiber?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Hard
#React

Brief Answer

React Fiber divides work into two main phases with different characteristics:

Render Phase 🔄

  1. Interruptible — can be stopped and resumed
  2. No side effects — pure computations
  3. Can execute multiple times for one update
  4. Asynchronous — doesn’t block the browser

Commit Phase ⚡

  1. Synchronous — executes in one pass
  2. With side effects — DOM changes, lifecycle methods
  3. Executes once for each update
  4. Uninterruptible — must complete fully
// Render Phase - can be interrupted
function Component() {
  const [count, setCount] = useState(0);
  console.log('Render phase'); // May execute multiple times
  return <div>{count}</div>;
}
 
// Commit Phase - executes once
useEffect(() => {
  console.log('Commit phase'); // Will execute only once
}, []);

Full Answer

React Fiber is like a smart task scheduler that divides work into two phases for optimal performance! Understanding these phases is critical for writing efficient React applications. 🚀

Render Phase — computation phase

This is the phase of pure computations without side effects:

function ExpensiveComponent({ data }) {
  // Render Phase - pure computations
  const processedData = useMemo(() => {
    console.log('Processing data...'); // May execute multiple times
    return data.map(item => ({
      ...item,
      processed: true
    }));
  }, [data]);
  
  // Render Phase - virtual DOM creation
  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} data={item} />
      ))}
    </div>
  );
}

Render Phase characteristics:

  1. Interruptibility — React can stop work:
function Component() {
  // This code may execute multiple times
  const expensiveValue = calculateSomething();
  
  // React may interrupt here to handle higher priority tasks
  return <div>{expensiveValue}</div>;
}
  1. No side effects:
// ❌ Wrong in Render Phase
function BadComponent() {
  document.title = 'New Title'; // Side effect!
  localStorage.setItem('key', 'value'); // Side effect!
  
  return <div>Content</div>;
}
 
// ✅ Correct in Render Phase
function GoodComponent() {
  const data = processData(); // Pure computation
  
  return <div>{data}</div>;
}

Commit Phase — changes application phase

This is the phase of applying changes with side effects:

function Component() {
  const [count, setCount] = useState(0);
  
  // Commit Phase - Layout Effects (synchronous)
  useLayoutEffect(() => {
    // Executes synchronously after DOM mutations
    const element = document.getElementById('counter');
    element.style.color = count > 5 ? 'red' : 'black';
  }, [count]);
  
  // Commit Phase - Effects (asynchronous)
  useEffect(() => {
    // Executes asynchronously after rendering
    document.title = `Count: ${count}`;
    
    // Cleanup function
    return () => {
      document.title = 'React App';
    };
  }, [count]);
  
  return <div id="counter">{count}</div>;
}

Commit Phase sub-phases:

  1. Before Mutation — before DOM changes:
// getSnapshotBeforeUpdate executes here
class Component extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Get snapshot before changes
    return document.getElementById('list').scrollTop;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    // Use snapshot after changes
    if (snapshot !== null) {
      document.getElementById('list').scrollTop = snapshot;
    }
  }
}
  1. Mutation — DOM changes:
// Here React applies changes to DOM
function Component({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}
  1. Layout — after DOM changes:
function Component() {
  const ref = useRef();
  
  useLayoutEffect(() => {
    // Executes synchronously after DOM changes
    const rect = ref.current.getBoundingClientRect();
    console.log('Element dimensions:', rect);
  });
  
  return <div ref={ref}>Content</div>;
}

Practical differences

Update prioritization:

function App() {
  const [urgent, setUrgent] = useState(0);
  const [normal, setNormal] = useState(0);
  
  const handleUrgentUpdate = () => {
    // High priority - will interrupt other updates
    flushSync(() => {
      setUrgent(prev => prev + 1);
    });
  };
  
  const handleNormalUpdate = () => {
    // Normal priority - can be interrupted
    setNormal(prev => prev + 1);
  };
  
  return (
    <div>
      <div>Urgent: {urgent}</div>
      <div>Normal: {normal}</div>
      <button onClick={handleUrgentUpdate}>Urgent</button>
      <button onClick={handleNormalUpdate}>Normal</button>
    </div>
  );
}

Error handling:

// Error Boundary catches errors in Render Phase
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    // Updates state to show error UI
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // Error logging (Commit Phase)
    console.error('Error caught:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children;
  }
}

Performance optimization

Using Concurrent Features:

import { startTransition, useDeferredValue } from 'react';
 
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  
  // Render Phase can be interrupted for more important updates
  const results = useMemo(() => {
    return searchData(deferredQuery);
  }, [deferredQuery]);
  
  return (
    <div>
      {results.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}
 
function App() {
  const [query, setQuery] = useState('');
  
  const handleSearch = (value) => {
    // Immediate input update
    setQuery(value);
    
    // Deferred results update
    startTransition(() => {
      // This code can be interrupted
      performExpensiveSearch(value);
    });
  };
  
  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      <SearchResults query={query} />
    </div>
  );
}

Debugging and profiling

React DevTools Profiler:

import { Profiler } from 'react';
 
function App() {
  const onRenderCallback = (id, phase, actualDuration) => {
    console.log('Component:', id);
    console.log('Phase:', phase); // "mount" or "update"
    console.log('Duration:', actualDuration);
  };
  
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <ExpensiveComponent />
    </Profiler>
  );
}

Best practices

  1. In Render Phase — only pure computations 🧮
  2. In Commit Phase — side effects and DOM operations 🔧
  3. Use useMemo/useCallback to optimize Render Phase ⚡
  4. Minimize work in useLayoutEffect 🚀

Common mistakes

Wrong:

function Component() {
  // Side effect in Render Phase
  localStorage.setItem('data', 'value');
  
  return <div>Content</div>;
}

Correct:

function Component() {
  useEffect(() => {
    // Side effect in Commit Phase
    localStorage.setItem('data', 'value');
  }, []);
  
  return <div>Content</div>;
}

Conclusion

Understanding React Fiber phases is critical for performance:

  • Render Phase — interruptible pure computations
  • Commit Phase — synchronous change application
  • Concurrent Features use Render Phase interruptibility
  • Side effects only in Commit Phase

Use this knowledge to create fast applications! 🎯