Как предотвратить лишние рендеры в React?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#React

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

Основные способы предотвращения лишних рендеров в React:

  1. React.memo — мемоизация функциональных компонентов
  2. useMemo — кэширование результатов вычислений
  3. useCallback — предотвращение пересоздания функций
  4. PureComponent — для классовых компонентов
  5. Правильная структура компонентов — выделение частей, которые меняются реже
  6. Стабильные объекты и массивы — вынесение за пределы компонента или мемоизация
  7. Правильная работа с Context — разделение на более мелкие контексты

Пример использования React.memo:

// Компонент перерисовывается только при изменении name
const UserGreeting = React.memo(function UserGreeting({ name }) {
  console.log("UserGreeting рендерится");
  return <h1>Привет, {name}!</h1>;
});
 
function App() {
  const [name, setName] = useState("Мария");
  const [counter, setCounter] = useState(0);
  
  return (
    <div>
      <UserGreeting name={name} />
      <button onClick={() => setName("Алексей")}>
        Изменить имя
      </button>
      <button onClick={() => setCounter(c => c + 1)}>
        Счетчик: {counter}
      </button>
    </div>
  );
}

Полный ответ

Предотвращение ненужных перерисовок — один из ключевых аспектов оптимизации производительности React-приложений. React по умолчанию перерисовывает компоненты даже когда это не всегда необходимо, поэтому важно знать, как это контролировать. 🚀

1. React.memo для функциональных компонентов

React.memo — это HOC (компонент высшего порядка), который мемоизирует компонент, предотвращая его перерисовку, если пропсы не изменились:

const MovieCard = React.memo(function MovieCard({ title, rating }) {
  console.log(`Рендер: ${title}`);
  
  return (
    <div className="movie-card">
      <h3>{title}</h3>
      <span>Рейтинг: {rating}/10</span>
    </div>
  );
});
 
// Использование
function MovieList() {
  const [movies, setMovies] = useState(initialMovies);
  const [filter, setFilter] = useState("");
  
  // MovieCard перерисовывается только при изменении конкретного фильма
  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
        placeholder="Фильтр"
      />
      {movies.map(movie => (
        <MovieCard 
          key={movie.id} 
          title={movie.title} 
          rating={movie.rating} 
        />
      ))}
    </div>
  );
}

Собственный компаратор для React.memo

Можно настроить собственную логику сравнения пропсов:

const ProjectItem = React.memo(
  function ProjectItem({ project, onSelect }) {
    return (
      <div onClick={() => onSelect(project.id)}>
        {project.name} 
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Рендер происходит только при изменении id или имени
    return (
      prevProps.project.id === nextProps.project.id &&
      prevProps.project.name === nextProps.project.name
    );
  }
);

2. useMemo для вычисляемых значений

useMemo позволяет кэшировать результаты дорогостоящих вычислений между рендерами:

function ProductList({ products, category }) {
  // Фильтрация выполняется только при изменении products или category
  const filteredProducts = useMemo(() => {
    console.log("Фильтрация продуктов");
    return products.filter(p => p.category === category);
  }, [products, category]);
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

3. useCallback для функций

useCallback предотвращает создание новых функций при каждом рендере:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");
 
  // Функция не пересоздаётся при каждом рендере
  const handleDelete = useCallback((id) => {
    setTodos(todos => todos.filter(todo => todo.id !== id));
  }, []); // Зависимости пусты, так как используем функциональное обновление
  
  return (
    <div>
      <input 
        value={newTodo} 
        onChange={(e) => setNewTodo(e.target.value)} 
      />
      <button onClick={() => {
        if (newTodo) {
          setTodos([...todos, { id: Date.now(), text: newTodo }]);
          setNewTodo("");
        }
      }}>
        Добавить
      </button>
      
      {todos.map(todo => (
        // TodoItem не будет перерисовываться, если меняется только текст в input
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onDelete={handleDelete} 
        />
      ))}
    </div>
  );
}
 
const TodoItem = React.memo(function TodoItem({ todo, onDelete }) {
  console.log(`Рендер: ${todo.text}`);
  return (
    <div>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Удалить</button>
    </div>
  );
});

4. PureComponent для классовых компонентов

PureComponent — аналог React.memo для классовых компонентов:

class UserProfile extends React.PureComponent {
  render() {
    console.log("UserProfile рендерится");
    const { name, email } = this.props;
    
    return (
      <div className="profile">
        <h2>{name}</h2>
        <p>{email}</p>
      </div>
    );
  }
}

5. Правильная структура компонентов

Разделение компонентов по частоте изменений:

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  
  // Часто меняющийся компонент (при каждом вводе)
  return (
    <div>
      <SearchBar 
        query={query} 
        onChange={setQuery} 
      />
      {/* Редко меняющийся компонент (только при новых результатах) */}
      <SearchResults results={results} />
    </div>
  );
}
 
// Оптимизированные компоненты
const SearchBar = React.memo(function SearchBar({ query, onChange }) {
  return (
    <input 
      value={query} 
      onChange={(e) => onChange(e.target.value)} 
      placeholder="Поиск" 
    />
  );
});
 
const SearchResults = React.memo(function SearchResults({ results }) {
  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
});

6. Стабильные объекты и массивы

Предотвращение создания новых объектов при каждом рендере:

// ❌ Плохо: новый объект стилей при каждом рендере
function Button({ primary }) {
  return (
    <button style={{ 
      color: primary ? 'white' : 'black',
      background: primary ? 'blue' : 'gray'
    }}>
      Нажми меня
    </button>
  );
}
 
// ✅ Хорошо: мемоизированные стили
function Button({ primary }) {
  const buttonStyle = useMemo(() => ({
    color: primary ? 'white' : 'black',
    background: primary ? 'blue' : 'gray'
  }), [primary]);
  
  return <button style={buttonStyle}>Нажми меня</button>;
}
 
// Альтернативно: статические объекты
const primaryStyle = { color: 'white', background: 'blue' };
const secondaryStyle = { color: 'black', background: 'gray' };
 
function Button({ primary }) {
  return (
    <button style={primary ? primaryStyle : secondaryStyle}>
      Нажми меня
    </button>
  );
}

7. Оптимизация Context

Разделение контекста на более мелкие части:

// ❌ Плохо: один большой контекст
const AppContext = React.createContext();
 
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  return (
    <AppContext.Provider value={{ 
      user, setUser, 
      theme, setTheme, 
      notifications, setNotifications 
    }}>
      {children}
    </AppContext.Provider>
  );
}
 
// ✅ Хорошо: разделенные контексты
const UserContext = React.createContext();
const ThemeContext = React.createContext();
const NotificationContext = React.createContext();
 
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <NotificationContext.Provider value={{ notifications, setNotifications }}>
          {children}
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

8. Использование библиотеки Reselect с Redux

Для приложений использующих Redux:

import { createSelector } from 'reselect';
 
// Базовые селекторы
const getUsers = state => state.users;
const getFilter = state => state.filter;
 
// Мемоизированный селектор
const getFilteredUsers = createSelector(
  [getUsers, getFilter],
  (users, filter) => {
    console.log('Фильтрация пользователей');
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }
);
 
// Использование в компоненте
function UserList() {
  const filteredUsers = useSelector(getFilteredUsers);
  // Фильтрация выполняется только при изменении users или filter
  
  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Инструменты для обнаружения проблем

1. React DevTools Profiler

Профилирование для выявления лишних рендеров:

// Добавьте <React.Profiler> в ключевых местах
function App() {
  const handleRender = (id, phase, actualDuration) => {
    console.log(`Компонент ${id} рендерился за ${actualDuration}ms`);
  };
  
  return (
    <React.Profiler id="Navigation" onRender={handleRender}>
      <Navigation />
    </React.Profiler>
  );
}

2. Библиотека why-did-you-render

Автоматически показывает предотвратимые перерендеры:

// Setup в файле wdyr.js
import React from 'react';
 
if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}
 
// Использование в компоненте
function ExampleComponent(props) {
  // ... код компонента
}
 
ExampleComponent.whyDidYouRender = true;

Распространенные ошибки

1. Излишняя оптимизация

// ❌ Слишком рано: оптимизация простых компонентов
const Text = React.memo(function Text({ content }) {
  return <p>{content}</p>;
});
 
// ✅ Правильно: оптимизируйте только компоненты со сложной логикой или глубоким деревом
const ComplexChart = React.memo(function ComplexChart({ data }) {
  // Сложные вычисления и большое дерево компонентов
  return <ChartComponent data={data} />;
});

2. Зависимости в массивах useCallback/useMemo

// ❌ Неправильно: забыты зависимости
function UserList({ users, onUserSelect }) {
  const handleClick = useCallback((userId) => {
    console.log(`Selected: ${userId}`);
    onUserSelect(userId);
  }, []); // Зависимость onUserSelect отсутствует
  
  // ... код компонента
}
 
// ✅ Правильно: все зависимости включены
function UserList({ users, onUserSelect }) {
  const handleClick = useCallback((userId) => {
    console.log(`Selected: ${userId}`);
    onUserSelect(userId);
  }, [onUserSelect]);
  
  // ... код компонента
}

3. Неправильное использование React.memo

// ❌ Неэффективно: объект создается при каждом рендере родителя
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      <MemoizedChild 
        data={{ value: 42 }} // Новый объект при каждом рендере
      />
    </div>
  );
}
 
const MemoizedChild = React.memo(function Child({ data }) {
  console.log("Child рендерится несмотря на memo");
  return <div>{data.value}</div>;
});
 
// ✅ Эффективно: стабильная ссылка на объект
function Parent() {
  const [count, setCount] = useState(0);
  
  // Объект создается один раз
  const data = useMemo(() => ({ value: 42 }), []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Счетчик: {count}
      </button>
      <MemoizedChild data={data} />
    </div>
  );
}

Резюме

Основные техники предотвращения лишних рендеров:

  • React.memo для функциональных компонентов
  • useMemo для кэширования вычисляемых значений
  • useCallback для стабильных функций
  • PureComponent для классовых компонентов
  • Разделение компонентов по частоте обновления
  • Избегание создания новых объектов/массивов при рендере
  • Разделение контекста на мелкие части

Рекомендации:

  • Не оптимизируйте преждевременно
  • Используйте инструменты профилирования для выявления проблем
  • Следите за зависимостями в хуках
  • Применяйте оптимизации сначала к компонентам, которые перерисовываются чаще всего

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


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪