Оптимизация производительности React: Context API vs Redux Toolkit в реальных проектах

Распространённая картина: приложение на React начинает тормозить, интерфейс дёргается при обновлении состояния, а в инструментах разработчика мелькают десятки ненужных перерисовок. Часто корень проблемы — в неоптимальном управлении состоянием. Сегодня разберём как избежать этих проблем, работая с Context API и Redux Toolkit.

Анатомия проблемы: почему ваш Context перерисовывает всё подряд

Рассмотрим типичный пример провайдера авторизации:

jsx
const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [permissions, setPermissions] = useState([]);

  const login = async (credentials) => {
    setIsLoading(true);
    const data = await api.login(credentials);
    setUser(data.user);
    setPermissions(data.permissions);
    setIsLoading(false);
  };

  const value = { user, isLoading, permissions, login };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Почему это проблематично? При любом изменении состояния — обновлении isLoading во время логина — все компоненты, использующие этот контекст, перерисуются, даже если им нужна только часть данных. Компоненту отображения пермишенов не важно состояние загрузки, но он всё равно получит ненужный ре-рендер.

Диагностика в DevTools

Откройте React DevTools, включите "Highlight updates when components render". Вы увидите, как при изменении isLoading подсвечивается всё дерево ниже провайдера — ключевой маркер избыточных ре-рендеров.

Решения для Context: разделяй и властвуй

Стратегия 1: Мемоизация контекста

jsx
export function AuthProvider({ children }) {
  // Состояние остаётся прежним...
  
  const login = useCallback(async (credentials) => {
    // Логика входа
  }, []);

  const value = useMemo(() => ({
    user,
    isLoading,
    permissions,
    login
  }), [user, isLoading, permissions, login]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

useMemo и useCallback предотвращают создание нового объекта значения при каждом рендере. Но проблема избыточных перерисовок остаётся при обновлении любых данных.

Стратегия 2: Сегментирование контекстов

Создадим независимые контексты для разных логических блоков:

jsx
const UserContext = createContext();
const LoadingContext = createContext();
const PermissionsContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [permissions, setPermissions] = useState([]);

  return (
    <LoadingContext.Provider value={isLoading}>
      <UserContext.Provider value={user}>
        <PermissionsContext.Provider value={permissions}>
          {children}
        </PermissionsContext.Provider>
      </UserContext.Provider>
    </LoadingContext.Provider>
  );
}

Теперь компонент, использующий только пермишены, не будет реагировать на изменения isLoading или user.

Когда Context становится недостаточным

Для простых приложений этих оптимизаций достаточно. Но при масштабировании возникают новые проблемы:

  1. Обновления, зависящие от нескольких состояний: Компоненту нужны данные из двух несвязанных контекстов
  2. Асинхронные зависимости: Загрузка данных с последовательными запросами
  3. Сложные преобразования данных: Комбинирование данных из нескольких источников
  4. DevTools и история изменений: Отладка потока данных

Рассмотрим запрос, где нам нужны данные пользователя и его настроек:

jsx
const { user } = useContext(UserContext);
const { preferences } = useContext(PreferencesContext);

useEffect(() => {
  if (user && preferences) {
    loadDashboardData(user.id, preferences.locale);
  }
}, [user, preferences]);  // Сложная зависимость

Эффект запускается хаотично при независимых обновлениях контекстов. Войдите в Redux Toolkit.

Redux Toolkit: не ваш дедушкин Redux

Редокс избавляется от шаблонного кода через:

  1. createSlice: Объединяет экшены и редьюсеры
  2. createAsyncThunk: Стандартизация асинхронных операций
  3. RTK Query: Встроенное решение для API-запросов
  4. Оптимизированные селекторы: createSelector из Reselect

Реализация нашего сценария с RTK

javascript
// store/authSlice.js
const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    permissions: [],
    isLoading: false
  },
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(loginUser.pending, (state) => {
      state.isLoading = true;
    });
    builder.addCase(loginUser.fulfilled, (state, action) => {
      state.user = action.payload.user;
      state.permissions = action.payload.permissions;
      state.isLoading = false;
    });
  }
});

export const loginUser = createAsyncThunk(
  'auth/login',
  async (credentials, { rejectWithValue }) => {
    try {
      return await api.login(credentials);
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

Селекторы с мемоизацией

javascript
const selectUser = state => state.auth.user;
const selectPermissions = state => state.auth.permissions;

export const selectUserPermissions = createSelector(
  [selectUser, selectPermissions],
  (user, permissions) => {
    // Тяжёлая логика преобразования
    return permissions.map(perm => `${user.id}_${perm}`);
  }
);

Почему это эффективнее: преобразование выполняется только при изменении исходных user или permissions.

Интеграция с React

jsx
const Dashboard = () => {
  const userPermissions = useSelector(selectUserPermissions);
  const isLoading = useSelector(state => state.auth.isLoading);

  if (isLoading) return <Spinner />;

  return <PermissionsList items={userPermissions} />;
}

Redux автоматически обрабатывает подписки и предотвращает ре-рендеры когда данные не изменились.

Контрольный список: Context или Redux Toolkit?

КритерийContext APIRedux Toolkit
Размер приложенияМалый/СреднийСредний/Крупный
Частота обновлений состоянияНизкая/СредняяВысокая
Сложность преобразования данныхПростые операцииКомплексные селекторы
Асинхронные потоки данныхРучное управлениеВстроенная поддержка
Инструменты отладкиБазовыеDevTools + Time Travel
Bundle size0Kb (встроен в React)≈10Kb (gzipped)

Гибридный подход: лучшее из двух миров

Нет правил без исключений. Мы часто комбинируем подходы:

jsx
// Структура проекта:
/src
  /store   # Redux для глобального состояния
  /context # Локальные контексты для UI состояний

Пример: используем Redux для данных пользователя и API-запросов, а Context для темы оформления и локальных состояний формы:

jsx
const ThemeContext = createContext();

export function App() {
  return (
    <ReduxProvider store={store}>
      <ThemeContext.Provider value={darkMode}>
        <MainLayout />
      </ThemeContext.Provider>
    </ReduxProvider>
  );
}

// Где-то внутри компонента:
const theme = useContext(ThemeContext);
const currentUser = useSelector(selectCurrentUser);

Микроменеджмент подписок

Для максимальной производительности в сложных списках соединяем React.memo с селекторами:

jsx
const UserListItem = memo(({ userId }) => {
  const user = useSelector(state => 
    selectUserById(state, userId)  // Селектор с кешированием
  );
  
  return <li>{user.name}</li>;
});

Заключение: рекомендации для реальных проектов

  1. Начинайте с Context: Для простых сценариев используйте разделённые контексты с useMemo/useCallback

  2. Переходите на Redux Toolkit при:

    • Появлении компонентов, зависящих от нескольких состояний
    • Необходимости сложных преобразований данных
    • Потребности в продвинутой отладке
  3. Оптимизируйте селекторы: Всегда используйте memoized селекторы при работе с Redux

  4. Измеряйте производительность: Не гадайте — используйте React DevTools Profiler и window.performance.mark()

  5. Избегайте преждевременной оптимизации: Добавляйте сложные решения только при явных метриках проблем

Оптимальное управление состоянием в React напоминает дирижирование оркестром: каждая часть должна обновляться в нужный момент, без лишних партий. Выбор инструментов должен определяться спецификой вашего приложения, а не модными трендами. Когда в следующий раз увидите дёргающийся интерфейс, вспомните — вероятно, где-то кто-то передаёт объект с зависимостями без мемоизации. Не будьте этим человеком.