Skip to main content

Глобальний стейт і продуктивність

Що таке "глобальний стан"

Глобальний стан - це дані, до яких можуть звертатися різні компоненти з різних частин застосунку, наприклад:

  • поточний користувач,
  • тема (dark/light),
  • авторизація,
  • кошик товарів,
  • налаштування застосунку тощо.

Такі дані зазвичай зберігаються в:

  • React Context API (useContext);
  • Redux / Zustand / Jotai / MobX;
  • глобальних store-синглтонах.

Проблема: надлишковий глобальний стан

Коли розробник починає класти все підряд у глобальний store, навіть те, що потрібно лише одному компоненту, виникає ланцюгова реакція перерендерів і ускладнення застосунку.


Чому це шкідливо для продуктивності

1. Кожне оновлення глобального стану викликає масові перерендери

React за замовчуванням сповіщає всіх споживачів контексту (або store), які його читають, навіть якщо вони не використовують змінене поле.

javascript
const ThemeContext = createContext(); function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Header /> <Main /> <Footer /> </ThemeContext.Provider> ); }

Якщо ти зміниш theme, → всі компоненти, що використовують useContext(ThemeContext), будуть перерендерені, навіть якщо вони не відображають тему напряму.

Чим більше компонентів прив'язано до контексту, тим більше непотрібних оновлень.


2. Втрачається "локалізація" стану

Якщо все зберігається в одному великому store, будь-який апдейт → тригерить підписників по всьому дереву.

Наприклад, зміна "пошукового запиту" в списку товарів не повинна чіпати "панель профілю користувача", але якщо все в одному Redux-слайсі, обидва перерахуються.


3. Неможливо ефективно мемоізувати

Коли дані глобальні, складно ізолювати компонент:

javascript
const user = useSelector((state) => state.user);

→ При будь-якій зміні в state.user React-Redux тригерить перерендер, навіть якщо компонент використовує лише user.name.

Якщо таких "дрібних" підписок десятки, починається "каскад перерендерів".


4. Контексти не вміють часткових оновлень

React Context не підтримує дифф за значеннями. Якщо у value хоч щось змінилося, всі useContext-споживачі отримують новий об'єкт → перерендер.

javascript
<Provider value={{ theme, user }}>...</Provider>

→ навіть якщо змінився user, компонент, що використовує лише theme, все одно перемалюється, тому що об'єкт { theme, user } новий за посиланням.


5. Зростає когнітивне навантаження

Чим більше глобального стану, тим важче зрозуміти:

  • що де зберігається;
  • хто оновлює;
  • які компоненти залежать від чого.

Це призводить до непередбачуваних побічних ефектів, і оптимізація стає складнішою, ніж сама бізнес-логіка.


6. Проблеми з конкурентним рендерингом

У React 18 при увімкненому concurrent mode React може:

  • призупинити рендер;
  • відкотити зміни;
  • перерендерити частину дерева.

Великий глобальний store робить це дорожче, тому що React змушений копіювати і тримати знімки більшого обсягу даних.


Приклад (наочно)

javascript
// все в глобальному контексті - погана практика const GlobalContext = createContext(); function App() { const [theme, setTheme] = useState('light'); const [cart, setCart] = useState([]); const [search, setSearch] = useState(''); const value = { theme, cart, search, setTheme, setCart, setSearch }; return ( <GlobalContext.Provider value={value}> <Header /> // використовує лише theme <Search /> // використовує лише search <Cart /> // використовує лише cart </GlobalContext.Provider> ); }

Якщо змінити search,

  • Header і Cart все одно перерендеряться, тому що value (об'єкт) змінився за посиланням.

Рішення (як зробити правильно)

1. Розділяй контексти / стора Створюй окремі провайдери для незалежних частин:

javascript
<ThemeProvider> <CartProvider> <SearchProvider> <App /> </SearchProvider> </CartProvider> </ThemeProvider>

Кожен контекст оновлюється ізольовано.


2. Тримай локальний стан локальним Якщо дані потрібні лише одному компоненту (або його дітям), використовуй useState прямо в ньому, а не в Redux/Context.

javascript
function SearchBar() { const [query, setQuery] = useState(''); ... }

3. Роби селективні підписки Якщо використовуєш Redux / Zustand, використовуй селектори і shallow порівняння, щоб підписуватися лише на потрібні поля.

javascript
const price = useSelector(state => state.cart.totalPrice);

→ Зміниться лише якщо totalPrice зміниться.


4. Використовуй мемоізацію і split-компоненти

  • Обгортай важкі компоненти в React.memo;
  • Передавай обробники через useCallback;
  • Дроби компонент так, щоб глобальні дані не тягли все дерево.

5. Для складних станів - useContextSelector (або Jotai / Zustand) React 19 додасть Context Selectors нативно. Поки можна використовувати use-context-selector: вона дозволяє підписуватися на конкретне значення в контексті, а не на весь об'єкт.


Підсумок

ПричинаЧому шкідливо
Масові перерендериУсі підписники оновлюються навіть при дрібній зміні
Погана ізоляціяБудь-яка зміна тригерить все дерево
Важкі знімкиЗбільшує пам'ять і роботу GC
Складно зрозуміти залежностіЗростає ймовірність багів
Неможлива точкова оптимізаціяContext не робить дифф за полями

Правильне мислення:

"Зберігай мінімально необхідне в глобальному стані. Все інше тримай локально, якомога ближче до місця використання."

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.