React Lanes
React — одна из самых популярных библиотек для создания пользовательских интерфейсов. Однако при работе с большими объемами данных или сложными вычислениями разработчики часто сталкиваются с проблемой производительности. В этой статье мы рассмотрим концепцию React Lanes — механизм, который позволяет приоритизировать задачи рендеринга и сделать интерфейс более отзывчивым даже при выполнении тяжелых операций.
Что такое React Lanes?
Давайте рассмотрим типовую задачу для React. Пусть это будет тривиальный поиск по текстовым элементам. Для этого возьмем обычное поле ввода и список элементов, который фильтруется в зависимости от введенного значения в это поле.
const SearchList = ({ items, filter }) => { // Heavy filtering operation (simulation) const filteredItems = items.filter((item) => item.toLowerCase().includes(filter.toLowerCase()), ); return ( <ul> {filteredItems.map((item, i) => ( <li key={i}>{item}</li> ))} </ul> ); }; const App = () => { const [inputValue, setInputValue] = useState(""); // Generate large list for demonstration const bigList = Array(10_000) .fill(null) .map((_, i) => `Item ${i + 1}`); const handleChange = (e) => { setInputValue(e.target.value); }; return ( <div> <input type="text" value={inputValue} onChange={handleChange} placeholder="Search..." /> <SearchList items={bigList} filter={inputValue} /> </div> ); }
Для наглядности, пусть список будет состоять из большого числа элементов. Что, очевидно, будет провоцировать тяжелую операцию фильтрации массива. Получившееся приложение можно потрогать руками ниже.
С количеством элементов можно поэкспериментировать в зависимости от мощности устройства и среды, где исполняется приложение. Но так или иначе, если быстро вводить значение в строку поиска, можно заметить некоторый лаг, когда в выборке оказывается много элементов.
Происходит это из-за того, что каждое изменение значения приводит к ререндеру компонента SearchList
, который в свою очередь должен отрисовать необходимое количество дочерних элементов.
Синхронные обновления
Пример выше демонстрирует типичную синхронную работу по обновлению дерева компонентов. Подробно о процессе обновления я писал в статье Детальный React. Реконсиляция, рендеры, Fiber, виртуальное дерево. Движок Fiber пытается выполнить все необходимые изменения за один раз. Другими словами, фаза render
не завершится, пока реконсилер не переберет всё запланированные синхронные изменения.
Конкретно в нашем примере, каждое изменение текстового поля вызывает реконсиляцию SearchList
, а он в свою очередь ставит рендеры дочерних компонентов в той же фазе. И пока все дочерние ноды не будут обработаны, основной поток будет заблокирован работой движка. Если выводимых компонентов слишком много, время блокировки становится существенным и проявляется в виде задержки отклика на пользовательский ввод.
На скриншоте выше в текстовое поле был введен символ 0
, что дает 2620 дочерних элементов. Реконсиляция такого количества элементов заняла 253ms
, учитывая, что задержки более 100ms считаются заметными человеческому глазу, а расчетное время браузерного фрейма 16.6ms (при частоте обновления 60 fps).
При добавлении еще одного "0" выводится 181 элемент. Реконсиляция заняла уже 146ms. Однако, это всё равно много.
Продолжим вводить нули. Следующий "0" даст только 10 элементов и тут мы видим уже приемлемую продолжительность в 10.6ms
Четвертый ноль оставит только один единственный элемент и продолжительность задачи составила всего 2ms
.
Что не на много больше, чем при значения "00000", при котором нет ни одного выводимого элемента.
Теперь проделаем обратную процедуру и будем удалять по одному символу из текстового поля.
Значение "0000". Продолжительность 0.9ms
.
Значение "000". Продолжительность 1.2ms
.
Значение "00". Продолжительность 12.8ms
.
Значение "0". Продолжительность 178ms
.
И наконец, пустая строка. Продолжительность 699ms
.
Приоритеты задач
Проблема, как говорится, налицо. Блокирующие операции довольно долго оставались проблемой, с которой надо было что-то делать. В качестве решения можно было бы предложить разделить тяжелые операции на потоки и выполнить их параллельно. Но JavaScript-задача не может выполняться в нескольких потоках.
ReactPriorityLevel
Первые попытки решить проблему были предприняты в 2016-м. В версии 15.2.1 было впервые введено понятие ReactPriorityLevel
. Раз нельзя разделить задачи на параллельные потоки, можно хотя бы расставить их в порядке важности. Суть ReactPriorityLevel
в том, чтобы проставить флаг приоритета каждому обновлению дерева.
Изначально таких приоритетов было 5, от наиболее важного к менее важному:
- SynchronousPriority - для контролируемого обновления инпутов и синхронных операций
- AnimationPriority - анимации должны завершать расчет очередного шага в текущем фрейме
- HighPriority - обновления, которые должны быть выполнены как можно быстрее для сохранения оптимального отклика
- LowPriority - для запросов и получения ответа из хранилищ
- OffscreenPriority - для элементов, которые были скрыты на странице, но снова становятся видимыми
Expiration Times
В целом, концепция приоритетов дала определенный результат. Но не все проблемы были решены. ReactPriorityLevel
разделял задачи по группам, но внутри группы задачи по-прежнему имеют одинаковый приоритет. Например, на странице вполне может оказаться несколько анимируемых элементов. В конце 2017-го была представлена версия React 16.1, в которой вместо флагов приоритетов предлагалось выставлять время, до которого задача должна быть обработана. Чем важнее задача, тем меньший expirationTime
она имела и, соответственно, бралась в работу раньше остальных.
Lanes
Концепция Expiration Times оказалась вполне рабочей. React 16 жил с ней следующие 3 года. Суть модели заключалась в том, что имея приоритеты A > B > C, нельзя взять в работу B, не выполнив при этом A. Аналогично, нельзя работать над C, не взяв в работу A и B.
Такой подход хорошо себя показывал вплоть до того, пока не появился Suspense
. Приоритизация работает покуда все задачи выполняются линейно. Когда в процесс вмешивается отложенная задача (такая, как Suspense
), появляется ситуация, когда отложенная задача с высоким приоритетом блокирует менее приоритетные основные.
В плане выражения группы сразу из нескольких приоритетов, модель Expiration Times довольно ограничена. Чтобы понять, включать ли задачу в объем работ на текущей итерации, достаточно сравнить относительные приоритеты:
const isTaskIncludedInBatch = priorityOfTask >= priorityOfBatch;
Чтобы решить проблему Suspense, можно было бы использовать Set
с приоритетами. Но это было бы очень накладно с точки зрения производительности и свело бы на нет все преимущества приоритетизации.
В качестве компромисса можно было бы ввести диапазоны приоритетов примерно следующим образом:
const isTaskIncludedInBatch = taskPriority <= highestPriorityInRange && taskPriority >= lowestPriorityInRange;
Но даже если закрыть глаза на то, что в таком случае потребуется держать два поля, этот подход всё равно не решает всех проблем. К примеру, как удалить задачу в середине диапазона? Какое-то решение конечно можно придумать. Но как бы то ни было, любые манипуляции с диапазонами будут неизбежно влиять на другие группы и их поддержка и стабильность превротяться в настоящий кошмар.
Чтобы избежать всех этих проблем, разработчики React решили разделить две концепции приоретизации и группировки. И предложили выражать группы задач относительными числами, представляющими собой битовую маску.
const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
Тип битовой маски представляющий задачу, называется Lane. А битовая маска представляющая группу задач - Lanes.
Как работают Lanes
В официальном релизе Lanes
впервые появились в октябре 2020 с версии React 17.0.0. Это был довольно большой рефакторинг, который существенно повлиял на работу Fiber
.
На данный момент последняя стабильная версия React v19.0.0. Большая часть работы Lanes инкапсулирована в модуле ReactFiberLane
реконсилятора.
Фактически, Lane
является 32-битным числом, где каждый бит указывает на принадлежность задачи к какой-либо из полос.
/packages/react-reconciler/src/ReactFiberLane.js#L39
export const TotalLanes = 31; export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000; export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001; export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010; export const SyncLaneIndex: number = 1; export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100; export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000; export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000; export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000; export const SyncUpdateLanes: Lane = SyncLane | InputContinuousLane | DefaultLane; const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000; const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000; const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000; const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000; const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000; const TransitionLane4: Lane = /* */ 0b0000000000000000000010000000000; const TransitionLane5: Lane = /* */ 0b0000000000000000000100000000000; const TransitionLane6: Lane = /* */ 0b0000000000000000001000000000000; const TransitionLane7: Lane = /* */ 0b0000000000000000010000000000000; const TransitionLane8: Lane = /* */ 0b0000000000000000100000000000000; const TransitionLane9: Lane = /* */ 0b0000000000000001000000000000000; const TransitionLane10: Lane = /* */ 0b0000000000000010000000000000000; const TransitionLane11: Lane = /* */ 0b0000000000000100000000000000000; const TransitionLane12: Lane = /* */ 0b0000000000001000000000000000000; const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000; const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000; const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000; const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000; const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000; const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000; const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000; const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000; export const SomeRetryLane: Lane = RetryLane1; export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000; const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111; export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000; export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000; export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
Условно, все полосы можно разделить на синхронные и отложенные.
Синхронные полосы
К синхронным полосам относятся SyncLane
, InputContinuousLane
и DefaultLane
. Задачи, поставленные в эти полосы имеют наивысший приоритет и выполняются синхронно в текущей фазе реконсилятора.
Исходя из названий можно предположить, что разные типы задач могут иметь разный приоритет даже в синхронной фазе. Cамыми приоритетными считаются:
- Размонтирование рута, что приводит к немедленной очистке стейта React и инициирует асинхронное удаление элементов дерева
- Hot Reload, т.к. JS модули должны быть немедленно заменены на новые
- Любые оверрайды в Dev Tools всегда должны происходить в первую очередь
- Хук useSyncExternalStore синхронно читает данные из внешнего хранилища, это должно происходить в самом начале реконсиляции файбера
- Хук useOptimistic гарантирует синхронное обновление оптимистичного стейта
- В текущей версии React официально пока еще нет компонента
<Activity />
(ранее носил название<Offscreen />
), но в движке он присутствует и когда-нибудь в будущем станет доступным. Этот компонент имеет три режима: "hidden", "visible" и "manual". В отличие от первых двух, режим manual предполагает, что разработчик сам отвечает за скрытие/показ компонента и вручную вызываетActivity.attach()
иActivity.detach()
. Эти два метода так же выполняются синхронно, чтобы React мог немедленно поставить в очередь работу по монтированию/размонтированию компонента.
Все выше перечисленные процессы происходят в полосе SyncLane
.
Приоритеты событий
Неотъемлемым диспатчером реакций в React являются события. Точнее, синтетические события. И среди всего множества потенциальных событий есть более приоритетные и менее приоритетные. Поэтому React разделяет их все на три условных группы:
- Дискретные - события, вызванные напрямую пользователем (например,
MouseEvent
илиKeyboardEvent
) и при этом все события в последовательности - намеренные, например "click". Таким событиям присваивается приоритетDiscreteEventPriority
и они выполнятся в SyncLane. Эти события могут прерывать фоновые задачи, но не могут быть сгруппированы на временной дистанции (каждое событие происходит здесь и сейчас).
import { useState } from "react"; import { createRoot } from "react-dom/client"; function App() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; return <button onClick={handleClick}>Update state in the SyncLane</button>; } const root = createRoot(document.getElementById("root")); root.render(<App />);
На данный момент список дискретных событий выглядит так:
beforetoggle cancel click close contextmenu copy cut auxclick dblclick dragend dragstart drop focusin focusout input invalid keydown keypress keyup mousedown mouseup paste pause play pointercancel pointerdown pointerup ratechange reset resize seeked submit toggle touchcancel touchend touchstart volumechange change selectionchange textInput compositionstart compositionend compositionupdate beforeblur afterblur beforeinput blur fullscreenchange focus hashchange popstate select selectstart
- Продолжительные - события, вызванные напрямую пользователем, но пользователь не может различить отдельные события в последовательности (например,
mouseover
). Таким событиям присваивается приоритетContinuousEventPriority
и они выполняются в InputContinuousLane. Эти события могут прерывать фоновые задачи и могут быть сгруппированы на протяжении времени.
import { useState } from "react"; import { createRoot } from "react-dom/client"; function App() { const [count, setCount] = useState(0); const handleMouseOver = () => setCount(count + 1); return ( <div onMouseOver={handleMouseOver}> Update state in the InputContinuousLane </div> ); } const root = createRoot(document.getElementById("root")); root.render(<App />);
Список продолжительных событий:
drag dragenter dragexit dragleave dragover mousemove mouseout mouseover pointermove pointerout pointerover scroll touchmove wheel mouseenter mouseleave pointerenter pointerleave
- Остальные - события, которые не попадают в первые две группы (кроме
message
, у них приоритет определяется динамически в зависимости от текущего приоритета планировщика). Таким событиям присваивается приоритетDefaultEventPriority
и они выполняются в DefaultLane соответственно. Отсутствие события так же считаетсяDefaultEventPriority
.
import { createRoot } from "react-dom/client"; const root = createRoot(document.getElementById("root")); root.render( <div> No events emitted. Common rendering uses the DefaultLane </div> );
Отложенные полосы
Отложенные полосы, как не трудно догадаться из названия, могут выполняться асинхронно с задержкой. Они имеют более низкий приоритет по сравнению с синхронными полосами и выполняются в фоне после того, как все синхронные полосы были отработаны. Задачи, выполняемые в отложенных полосах считаются неблокирующими.
TransitionLane
Пожалуй самый очевидный способ поставить отложенную задачу - через startTransition
.
import { startTransition, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; function App() { const [, setHigh] = useState(0); const [, setLow] = useState(0); useEffect(() => { startTransition(() => { // 2. TransitionLane will be proceeded in a background after a sync lane setLow((prevLow) => prevLow + 1); }); // 1. DefaultLane will be done first setHigh((prevHigh) => prevHigh + 1); }, []); return null; } const root = createRoot(document.getElementById("root")); root.render(<App />);
Задачи, которые ставятся в TransitionLane
, могут параллелиться. Для этого предусмотрено целых 15 полос TransitionLane1..15
. В отличие от синхронных полос, где параллелизм большого смысла не имеет, их эффективнее объединять в пакеты, чем параллелить.
import { startTransition, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; function App() { const [, setHigh] = useState(0); const [, setLow] = useState(0); useEffect(() => { // TransitionLane1 startTransition(() => { setLow((prevLow) => prevLow + 1); }); // TransitionLane2 setTimeout(() => { startTransition(() => { setLow((prevLow) => prevLow + 1); }); }, 0); // TransitionLane3 setTimeout(() => { startTransition(() => { setLow((prevLow) => prevLow + 1); }); }, 0); // DefaultLane setHigh((prevHigh) => prevHigh + 1); }, []); return null; } const root = createRoot(document.getElementById("root")); root.render(<App />);
RetryLane
Ещё один способ выполнить отложенную задачу - Suspense
.
import { Suspense, use } from "react"; import { createRoot } from "react-dom/client"; const cache = new Map(); function fetchData() { if (!cache.has("data")) { cache.set("data", getData()); } return cache.get("data"); } async function getData() { // Add a fake delay to make waiting noticeable. await new Promise((resolve) => { setTimeout(resolve, 1_000); }); return ["item1", "item2", "item3"]; } function AsyncComponent() { const items = use(fetchData()); return ( <ul> {items.map((item) => ( <li key={item}>{item}</li> ))} </ul> ); } function App() { return ( <> <h1>Retry lanes</h1> <Suspense fallback="Loading..."> <AsyncComponent /> </Suspense> </> ); } const root = createRoot(document.getElementById("root")); root.render(<App />);
Говоря простым языком, Suspense
рендерит сначала fallback компонент. Далее, когда один или несколько промисов внутри Suspended компонента зарезолвятся, Fiber попытается повторить рендеринг, поставив задачу в RetryLane
. Как и TransitionLane
, RetryLane
может параллелиться и имеет четыре полосы RetryLane1..4
.
function AsyncComponent() { const items = use(fetchData()); const meta = use(fetchMeta()); return ( <ul> {items.map((item) => ( <li key={items}>{item}</li> ))} </ul> ); } function App() { return ( <> <h1>Retry lanes</h1> <Suspense fallback="Loading..."> <AsyncComponent /> </Suspense> </> ); } const root = createRoot(document.getElementById("root")); root.render(<App />);
Аналогичная работа происходит и с компонентами SuspenseList
и Activity
. Но они пока не включены в последний, на сегодняшний день релиз React. Поэтому рассматривать их подробно сейчас не имеет смысла.
IdleLane
Следующая по приоритету полоса - IdleLane
. Идея IdleLane
заключается в том, чтобы некоторые задачи выполнять только тогда, когда движок простаивает и не занят другими, более важными задачами. Это можно сравнить с Idle-периодом в браузерном планировщике (я писал про это подробнее в статье Chromium. Отрисовка страницы с помощью Blink, CC и планировщика), только на уровне движка Fiber и React-планировщика.
На данный момент нет ни React DOM API, которые могли бы вызвать обновление с idle-приоритетом, ни нативных DOM событий с idle-приоритетом. Поэтому пока этой полосой можно воспользоваться только через внутренние методы движка, не доступные в prod-сборке React.
OffscreenLane
Чуть выше, когда я писал про синхронные полосы, я уже упоминал так называемый компонент Offscreen
, который теперь носит название Activity
. Его идея заключается в том, чтобы определять, находится ли компонент в данный момент в видимой части страницы, или за её пределами. Если компонент сейчас не виден на экране, то и задачи его имеют низкий приоритет. Собственно, для таких задач и была выделена полоса OffscreenLane
.
К сожалению, это еще один компонент, который пока не вошел в текущий релиз React. Но у него есть все шансы попасть в один из следующих. Будем надеяться, что в обозримом будущем мы его таки увидим.
DeferredLane
Не трудно догадаться, что полоса DeferredLane
предназначена для хука useDeferredValue. Однако, с ней не всё так просто, как может показаться.
import { useDeferredValue } from "react"; import { createRoot } from "react-dom/client"; function App() { const text = useDeferredValue("Final"); return <div>{text}</div>; } const root = createRoot(document.getElementById("root")); root.render(<App />);
Если у хука не указан второй аргумент - initialValue
, ему не с чем будет сравнивать новое значение при монтировании и он выполнится синхронно в DefaultLane
. Однако, если у хука имеется предыдущее значение, с которым он может сравнить текущее, например когда хук смонтирован и обновляется повторно или если указать initialValue
, мы увидим следующую картину.
import { useDeferredValue } from "react"; import { createRoot } from "react-dom/client"; function App() { const text = useDeferredValue("Final", "Initial"); return <div>{text}</div>; } const root = createRoot(document.getElementById("root")); root.render(<App />);
Работа по факту была выполнена в TransitionLane
. Если бы мы попробовали выполнить хук в Offscreen
компоненте, то увидели бы, что он выполнен в OffscreenLane
. Дело в том, что DeferredLane
является промежуточной технической полосой. Она служит только для логического отделения отложенных задач от остальных. Собственного приоритета выполнения эта задача не имеет и всегда подмешивается к другим полосам исходя из ситуации.
Неблокирующий рендеринг
Давайте теперь вернемся к нашему исходному примеру и попробуем избавиться от блокировок интерфейса тяжелыми операциями.
const SearchList = ({ items, filter }) => { // Heavy filtering operation (simulation) const filteredItems = items.filter((item) => item.toLowerCase().includes(filter.toLowerCase()), ); return ( <ul> {filteredItems.map((item, i) => ( <li key={i}>{item}</li> ))} </ul> ); }; const App = () => { const [inputValue, setInputValue] = useState(""); const deferredValue = useDeferredValue(inputValue); // Generate large list for demonstration const bigList = Array(10_000) .fill(null) .map((_, i) => `Item ${i + 1}`); const handleChange = (e) => { setInputValue(e.target.value); }; return ( <div> <input type="text" value={inputValue} onChange={handleChange} placeholder="Search..." /> <SearchList items={bigList} filter={deferredValue} /> </div> ); }
Всё, что мы сделали, в SearchList
стали передавать deferredValue
вместо inputValue
. Это позволило нам поставить задачу по расчету массива элементов в отложенных TransitionLane
и разблокировать синхронные обновления элемента input.
Конечно, чудес от React ждать не стоит. Тяжелая операция по-прежнему остается тяжелой. А React, будучи просто JavaScript-библиотекой, по-прежнему выполняется в основном потоке браузера. Система Lanes по своей сути является лишь способом приоритизации задач. Более важные задачи выполняются в первую очередь, менее важные - откладываются на потом. Однако, если тяжелая отложенная задача взята в работу, она всё равно будет блокировать основной поток.
Практические рекомендации по использованию Lanes
Понимание системы Lanes в React может существенно помочь в оптимизации производительности приложений. Вот несколько практических рекомендаций:
- Используйте useDeferredValue для тяжелых операций рендеринга. Как мы видели в примере с поиском, отложенное значение позволяет сохранить отзывчивость интерфейса даже при работе с большими объемами данных.
- Применяйте startTransition для неприоритетных обновлений UI. Когда вам нужно выполнить обновление, которое не требует мгновенной реакции (например, переключение вкладок или загрузка дополнительного контента), оборачивайте его в
startTransition
. - Разделяйте синхронные и асинхронные части вашего приложения. Элементы, требующие немедленной реакции (поля ввода, кнопки), должны обновляться синхронно, а тяжелые вычисления и отображение больших списков лучше отложить.
- Используйте Suspense для асинхронной загрузки данных. Это позволит React автоматически управлять приоритетами задач при загрузке данных с сервера.
- Помните о вложенности компонентов. Чем глубже вложен компонент, тем больше времени может занять его обновление. Старайтесь выносить тяжелые компоненты ближе к корню дерева.
Будущее React Lanes
Система Lanes продолжает развиваться. В будущих версиях React мы можем ожидать:
- Появление официального API для компонента
Activity
, который позволит более гибко управлять приоритетами рендеринга в зависимости от видимости компонентов. - Возможное внедрение поддержки Web Workers для выполнения тяжелых вычислений в отдельных потоках.
- Дальнейшее развитие API для более тонкой настройки приоритетов задач.
- Улучшение интеграции с инструментами профилирования для более наглядного отображения работы системы приоритетов.
Заключение
Система Lanes в React представляет собой мощный инструмент для оптимизации пользовательского опыта. Она не делает ваше приложение быстрее в абсолютном смысле, но позволяет более разумно распределять вычислительные ресурсы, отдавая приоритет тому, что важно для пользователя прямо сейчас.
Правильное использование useDeferredValue
, startTransition
и Suspense
может значительно улучшить воспринимаемую производительность вашего приложения, делая его более отзывчивым даже при выполнении сложных операций. Это особенно важно для современных веб-приложений, которые часто работают с большими объемами данных и сложными интерфейсами.
В конечном счете, понимание внутреннего устройства React и принципов работы системы приоритетов позволяет разработчикам создавать более эффективные и дружелюбные к пользователю приложения, что является ключевой целью любой фронтенд-разработки.
EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru
English version: https://blog.frontend-almanac.com/react-lanes