В данной статье мы рассмотрим понятие "батчинг" в ReactJS и посмотрим, как он может повысить производительность и улучшить пользовательский опыт веб-приложений.
React 18 включает в себя улучшения производительности «из коробки» путем использования более частых операций пакетной обработки по умолчанию, что устраняет необходимость вручную объединять обновления в коде приложения или библиотеки.
Это функциональность, о которой большинству пользователей можно не думать. Однако она может быть актуальна для преподавателей и разработчиков библиотек.
Что такое пакетная обработка (batching)?
Пакетная обработка (batching) - это когда React группирует несколько обновлений состояния в одно перерисовку для повышения производительности. Например, если у вас есть два обновления состояния внутри одного события клика, React всегда собирает их в одну перерисовку. Если вы выполните следующий код, вы увидите, что каждый раз, когда вы кликаете, React выполняет только одну перерисовку, хотя вы устанавливаете состояние дважды:Это отлично для производительности, потому что это позволяет избежать ненужных повторных рендеров. Это также предотвращает отображение компонента в "незаконченном" состоянии, где была обновлена только одна переменная состояния, что может вызвать ошибки. Это может напомнить вам о том, как официант ресторана не бежит на кухню, когда вы выбираете первое блюдо, а ждет, пока вы закончите заказ. Однако, React не всегда одинаково объединял обновления. Например, если вам нужно загрузить данные, а затем обновить состояние в указанном вышеfunction App() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { setCount(c => c + 1); // Пока не происходит повторное отображение setFlag(f => !f); // Пока не происходит повторное отображение // React перерисует только один раз в конце (это пакетная обработка!) } return ( <div> <button onClick={handleClick}>Next</button> <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> </div> ); }
handleClick
, то React не будет объединять обновления и будет выполнять два независимых обновления.
Это происходит потому, что React раньше выполнял пакетную обработку только во время события браузера (например, нажатия кнопки), а здесь мы обновляем состояние после обработки события (в callback-функции fetch
):
До React 18 мы объединяли обновления только в обработчиках событий React. Обновления внутриfunction App() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { fetchSomething().then(() => { // React 17 и более ранние НЕ объединяют эти обновления, потому что // они запускаются ПОСЛЕ события в callback-функции, а не ВО ВРЕМЯ события setCount(c => c + 1); // Вызывает повторное отображение setFlag(f => !f); // Вызывает повторный рендеринг }); } return ( <div> <button onClick={handleClick}>Next</button> <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> </div> ); }
Promise
, setTimeout
, нативных обработчиков событий или любых других событий по умолчанию не объединялись в React.
Что такое автоматическая пакетная обработка?
Начиная с React 18 с использованиемcreateRoot
, все обновления будут автоматически объединяться, независимо от их происхождения.
Это означает, что обновления внутри таймеров, Promise
, нативных обработчиков событий или любых других событий будут объединяться так же, как и обновления внутри событий React. Мы ожидаем, что это приведет к уменьшению работы по отрисовке и, следовательно, к лучшей производительности в ваших приложениях:
Примечание: Ожидается, что вы начнете использоватьfunction App() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { fetchSomething().then(() => { // React 18 и позднее ОБЪЕДИНЯЕТ эти обновления: setCount(c => c + 1); setFlag(f => !f); // React перерисует только один раз в конце (это пакетная обработка!) }); } return ( <div> <button onClick={handleClick}>Next</button> <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> </div> ); }
createRoot
вместе с переходом на React 18.
React будет автоматически объединять обновления, независимо от того, где они происходят, так что это:
Примечание: React объединяет обновления только тогда, когда это обычно безопасно. Например, React гарантирует, что для каждого события, инициированного пользователем, такого как клик или нажатие клавиши, DOM полностью обновляется перед следующим событием. Это гарантирует, например, что форма, которая отключается при отправке, не может быть отправлена дважды.function handleClick() { setCount(c => c + 1); setFlag(f => !f); // React перерисует только один раз в конце (это пакетная обработка!) } и равносильно этому: setTimeout(() => { setCount(c => c + 1); setFlag(f => !f); // React перерисует только один раз в конце (это пакетная обработка!) }, 1000); и равносильно этому: fetch(/*...*/).then(() => { setCount(c => c + 1); setFlag(f => !f); // React перерисует только один раз в конце (это пакетная обработка!) }) и равносильно этому: elm.addEventListener('click', () => { setCount(c => c + 1); setFlag(f => !f); // React перерисует только один раз в конце (это пакетная обработка!) });
Что, если я не хочу использовать пакетную обработку?
Обычно пакетная обработка безопасна, но некоторый код может зависеть от считывания чего-либо из DOM сразу после изменения состояния. Для таких случаев вы можете использоватьReactDOM.flushSync()
, чтобы отключить пакетную обработку:
Подробнее в оригинальной статье на GitHubimport { flushSync } from 'react-dom'; // Обратите внимание: react-dom, а не react function handleClick() { flushSync(() => { setCounter(c => c + 1); }); // React уже обновил DOM flushSync(() => { setFlag(f => !f); }); // React уже обновил DOM }