Чем отличаются render phase и commit phase в React Fiber?

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Сложный
#React

Краткий ответ

React Fiber разделяет работу на две основные фазы с разными характеристиками:

Render Phase (Фаза рендеринга) 🔄

  1. Прерываемая — может быть остановлена и возобновлена
  2. Без побочных эффектов — чистые вычисления
  3. Может выполняться несколько раз для одного обновления
  4. Асинхронная — не блокирует браузер

Commit Phase (Фаза фиксации) ⚡

  1. Синхронная — выполняется за один проход
  2. С побочными эффектами — DOM изменения, lifecycle методы
  3. Выполняется один раз для каждого обновления
  4. Неprерываемая — должна завершиться полностью
// Render Phase - может прерываться
function Component() {
  const [count, setCount] = useState(0);
  console.log('Render phase'); // Может выполниться несколько раз
  return <div>{count}</div>;
}
 
// Commit Phase - выполняется один раз
useEffect(() => {
  console.log('Commit phase'); // Выполнится только один раз
}, []);

Полный ответ

React Fiber — это как умный планировщик задач, который разделяет работу на две фазы для оптимальной производительности! Понимание этих фаз критично для написания эффективных React приложений. 🚀

Render Phase — фаза вычислений

Это фаза чистых вычислений без побочных эффектов:

function ExpensiveComponent({ data }) {
  // Render Phase - чистые вычисления
  const processedData = useMemo(() => {
    console.log('Processing data...'); // Может выполниться несколько раз
    return data.map(item => ({
      ...item,
      processed: true
    }));
  }, [data]);
  
  // Render Phase - создание виртуального DOM
  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} data={item} />
      ))}
    </div>
  );
}

Характеристики Render Phase:

  1. Прерываемость — React может остановить работу:
function Component() {
  // Этот код может выполниться несколько раз
  const expensiveValue = calculateSomething();
  
  // React может прервать здесь для обработки более приоритетных задач
  return <div>{expensiveValue}</div>;
}
  1. Отсутствие побочных эффектов:
// ❌ Неправильно в Render Phase
function BadComponent() {
  document.title = 'New Title'; // Побочный эффект!
  localStorage.setItem('key', 'value'); // Побочный эффект!
  
  return <div>Content</div>;
}
 
// ✅ Правильно в Render Phase
function GoodComponent() {
  const data = processData(); // Чистое вычисление
  
  return <div>{data}</div>;
}

Commit Phase — фаза применения изменений

Это фаза применения изменений с побочными эффектами:

function Component() {
  const [count, setCount] = useState(0);
  
  // Commit Phase - Layout Effects (синхронно)
  useLayoutEffect(() => {
    // Выполняется синхронно после DOM мутаций
    const element = document.getElementById('counter');
    element.style.color = count > 5 ? 'red' : 'black';
  }, [count]);
  
  // Commit Phase - Effects (асинхронно)
  useEffect(() => {
    // Выполняется асинхронно после рендеринга
    document.title = `Count: ${count}`;
    
    // Cleanup функция
    return () => {
      document.title = 'React App';
    };
  }, [count]);
  
  return <div id="counter">{count}</div>;
}

Подфазы Commit Phase:

  1. Before Mutation — перед изменением DOM:
// getSnapshotBeforeUpdate выполняется здесь
class Component extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Получаем снимок перед изменениями
    return document.getElementById('list').scrollTop;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    // Используем снимок после изменений
    if (snapshot !== null) {
      document.getElementById('list').scrollTop = snapshot;
    }
  }
}
  1. Mutation — изменение DOM:
// Здесь React применяет изменения к DOM
function Component({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}
  1. Layout — после изменения DOM:
function Component() {
  const ref = useRef();
  
  useLayoutEffect(() => {
    // Выполняется синхронно после DOM изменений
    const rect = ref.current.getBoundingClientRect();
    console.log('Element dimensions:', rect);
  });
  
  return <div ref={ref}>Content</div>;
}

Практические различия

Приоритизация обновлений:

function App() {
  const [urgent, setUrgent] = useState(0);
  const [normal, setNormal] = useState(0);
  
  const handleUrgentUpdate = () => {
    // Высокий приоритет - прервёт другие обновления
    flushSync(() => {
      setUrgent(prev => prev + 1);
    });
  };
  
  const handleNormalUpdate = () => {
    // Обычный приоритет - может быть прерван
    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 Boundary ловит ошибки в Render Phase
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    // Обновляет состояние для показа UI ошибки
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // Логирование ошибки (Commit Phase)
    console.error('Error caught:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Что-то пошло не так.</h1>;
    }
    
    return this.props.children;
  }
}

Оптимизация производительности

Использование Concurrent Features:

import { startTransition, useDeferredValue } from 'react';
 
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  
  // Render Phase может быть прерван для более важных обновлений
  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) => {
    // Немедленное обновление input
    setQuery(value);
    
    // Отложенное обновление результатов
    startTransition(() => {
      // Этот код может быть прерван
      performExpensiveSearch(value);
    });
  };
  
  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      <SearchResults query={query} />
    </div>
  );
}

Debugging и профилирование

React DevTools Profiler:

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

Лучшие практики

  1. В Render Phase — только чистые вычисления 🧮
  2. В Commit Phase — побочные эффекты и DOM операции 🔧
  3. Используйте useMemo/useCallback для оптимизации Render Phase ⚡
  4. Минимизируйте работу в useLayoutEffect 🚀

Частые ошибки

Неправильно:

function Component() {
  // Побочный эффект в Render Phase
  localStorage.setItem('data', 'value');
  
  return <div>Content</div>;
}

Правильно:

function Component() {
  useEffect(() => {
    // Побочный эффект в Commit Phase
    localStorage.setItem('data', 'value');
  }, []);
  
  return <div>Content</div>;
}

Заключение

Понимание фаз React Fiber критично для производительности:

  • Render Phase — прерываемые чистые вычисления
  • Commit Phase — синхронное применение изменений
  • Concurrent Features используют прерываемость Render Phase
  • Побочные эффекты только в Commit Phase

Используйте это знание для создания быстрых приложений! 🎯