Параллельный режим React - взгляд в будущее

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

Итак, что же такое “параллельность”? 

Как вам, вероятно, уже известно, JavaScript — однопоточный язык программирования. Это означает, что при выполнении одной из задач кода поток блокируется и следующая задача запускается только после реализации предыдущей. Однако это не значит, что мы не можем выполнять несколько задач одновременно. Звучит запутанно? Давайте рассмотрим эту ситуацию на простом житейском примере. 

Предположим, что вы жаворонок и не представляете начало дня без чашки чая и тоста с малиновым джемом. 

Если следовать логике наших предыдущих рассуждений, то вам сначала нужно приготовить чай и только после этого сделать тост. 

Чай-> Тост

Неразумно, вы не находите? В конце концов, нам же нужно, чтобы чай остался горячим, а не остыл, пока мы делаем тост. 

Чтобы повысить нашу производительность и сразу получить горячий чай и свежий тост, следует разбить обе задачи на несколько мини задач и спланировать их удобным для нас образом.

Пример параллельности в реальной жизни

Готово! Из примера видно, как один человек, решая две разные задачи, смог выполнить обе одновременно и добиться желаемого результата. 

Так что же такое “параллельность”? Это способ структурировать программу, разбив ее на части, выполнение которых будет проходить независимо друг от друга. Именно так мы сможем выйти за границы одного потока и улучшить производительность нашего приложения. 

Теперь, когда мы знаем определение, посмотрим, как параллельность работает в React. 

Главная задача — оптимизация пользовательского опыта

Поток UI браузера отвечает за отображение всех изменений, инициируемых CSS, пользовательским вводом и самим JavaScript на экране. Предполагается, что для оптимизации пользовательского опыта современный компьютер должен отображать 60 кадров в секунду. В связи с этим выполнение кода при каждом цикле рендеринга должно занимать не более 16.67 миллисекунд, а в реальности и того меньше — что-то около 10 миллисекунд (так как, учитывая вышесказанное, браузер занимается реализацией других задач UI).

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

Ниже приводится рабочий пример приложения, отображающего сетку из 100×100 случайно закрашенных пикселей при каждом нажатии клавиши. Попробуйте что-нибудь напечатать ниже: 

Блокировка отображения

Раздражает, да? Пример с отображением 10 000 узлов DOM, конечно же, может показаться преувеличением, но зато так можно быстро обозначить проблему. 

И пока многие разработчики для улучшения рабочего процесса будут использовать такие техники, как мемоизация и дебаунс, они будут лишь откладывать решение проблемы: рендеринг по-прежнему подобен грузовику, блокирующему дорогу. 

Но улучшение работы пользователя не ограничивается только решением проблем производительности. Необходимо принимать во внимание то, что сам пользователь понимает под лучшим опытом. 

Независимо от того, являетесь ли вы обладателем высококлассного компьютера или мало бюджетного телефона, вам не убежать от отображение процесса загрузки, макета приложения и плейсхолдеров. И пусть они успешно сигнализируют о том, что происходит, но объединение их чрезмерного количества или демонстрация без необходимости приводит скорее к снижению производительности, чем к ее улучшению. Если данные быстро загружаются на вашем компьютере, то зачем вам в принципе видеть процесс загрузки? Не будет ли лучше, немного подождать и сразу перейти на полностью отображенную и готовую страницу? 

Средство спасения — нефотореалистичный рендеринг

Помните, ту часть статьи, где речь шла о параллельности, как о способе разбить задачи на части и таким образом запустить выполнение нескольких задач? Это именно то, что делает сейчас React: процесс рендеринга разбивается на несколько небольших задач, и планировщик даже позволяет нам располагать их по степени важности (“временная нарезка”). Данный параллельный режим React дает возможность: 

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

Процесс рендеринга больше не блокирует поток, так как он становится нефотореалистичным. Кроме того, его можно отложить ради выполнения первоочередных задач, таких как нажатие клавиши пользователем.

Настало время воплотить теорию в практику и поработать с новыми возможностями параллельного режима! 

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

1. Использовать “экспериментальные” пакеты react и react-dom
2. Изменить изначальный вызов рендеринга следующим образом: 

// Параллельный режим
const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);

useDeferredValue

Помните демо, в котором UI тормозил при каждом нажатии клавиши? Для улучшения работы пользователя, нам нужен способ распределить пользовательские вводы по степени значимости и только потом приступать к рендерингу огромной сетки. К счастью, у нас есть такой способ: useDeferredValue

useDeferredValue — это хук, который обертывает свойство/состояние значения и получает максимальное время отсрочки. Этот способ позволяет сообщить React, что компоненты, зависящие от данного значения, могут быть отображены позднее. 

import { useState, useDeferredValue } from 'react';const [value, setValue] = useState('');
const deferredValue = useDeferredValue(value, {
  timeoutMs: 5000
});

Обратите внимание, что эта возможность не предотвращает частый вызов функции рендеринга! React по-прежнему будет отображать компоненты “на стороне” и по мере их готовности сбрасывать изменения в DOM. 

Внимание: использование этого хука может привести к перебоям изображения вследствие выборочного отображения приоритетных компонентов UI. 

Блокировка против прерываемого отображения

Задержка при извлечении данных 

Задержка при извлечении данных — еще одна потрясающая новая функция, от которой лично я в большом восторге. Если вы в курсе новейших функций React, то вы, возможно, уже знаете и об этой. Она идет совместно с React.lazy, представленной в версии 16.8. Задержка позволяет нам показать плейсхолдер в процессе ожидания отделения части кода нашего приложения. 

Задержка при извлечении данных подразумевает ту же идею, но работает с кодом, используя промисы. 

import Spinner from './Spinner';<Suspense fallback={<Spinner />}>
   <SomeComponent />
</Suspense>

Эффективность этой функции можно рассмотреть на примере простого приложения без задержки:

Это приложение загружает список ТВ-шоу. При нажатии на название ТВ-шоу, оно открывает подробную страницу с комментариями.

const [tvData, setTvData] = useState(null);useEffect(()=>{
   setTvData(null);
   tvDataApi(id).then(value =>{
     setTvData(value);
   })
}, [id]);if (tvData === null) return <Spinner />;return <div className="tvshow-details">
  <div className="flex">
   <div>
     <h2>{tvData.name}</h2>
     <div className="details">{tvData.description}</div>
    </div>
   <div>
     <img src={`${id}.jpg`} alt={tvData.name} />
   </div>
  </div>
  {/* comments section */
  <Comments id={id} />
</div>

Если внимательно посмотреть здесь на способ загрузки данных, то можно заметить следующее: 

  1. В настоящее время отсутствует единый способ отображения плейсхолдера при загрузке данных. Каждый разработчик сам выбирает подходящий способ. 
  2. Мы создаем водопад API вызовов, так как можем только отображать компонент комментариев по мере готовности и рендеринга данных страницы. Конечно же, есть и другие способы выполнить эту задачу, но они не отличаются простотой или удобством. 

Задержка решает оба вопроса, так как на сегодняшний день она обеспечивает простой синтаксис для изображения плейсхолдеров. Кроме того, с помощью функций параллельного режима она может дополнительно запустить процесс рендеринга компонентов, тем самым инициируя API вызовы, а также отобразить плейсхолдер. Эта потрясающая функция сильно облегчает работу с данными.

export const TvShowDetails = ({ id }) => {
  return (
   <div className="tvshow-details">
      <Suspense fallback={<Spinner />}>
          <Details id={id} />
          <Suspense fallback={<Spinner />}>
             <Comments id={id} />
          </Suspense>
      </Suspense>
   </div>
  );
};

Для работы с задержкой при извлечении данных нам потребуется обернуть наши промисы функцией (смотрите пример wrapPromise в ниже приведенном демо), которая вернет различные значения в зависимости от того, что ожидает Suspense на каждом этапе процесса. Команда React работает над созданием библиотеки react-cache, включающей эту функцию, но проект еще находится на стадии разработки. 

В любом случае, использование этого синтаксиса упрощает наш компонент. Можно отказаться от использования useEffect и больше не волноваться о том, что же произойдет, если данные не будут готовы. Т.е. мы можем рассматривать компонент, как если бы эти данные уже в нём присутствовали, и позволить задержке сделать все остальное. 

const detailsData = detailsResource(id).read();return <div>{detailsData.name} | {detailsData.score}</div>

Но на этом этапе возникает еще одна проблема. Что если API вызов внутри компонента комментариев завершится первым? Было бы странно сначала показывать его, так ведь? К счастью, у нас есть способ скоординировать порядок изображения компонентов с помощью задержки. 

SuspenseList

SuspenseList — это компонент, которым мы можем обернуть другие компоненты задержки. Он получает два свойства: revealOrder и дополнительное свойство tail, при помощи которых мы можем сообщить React порядок отображения дочерних обернутых компонентов задержки. (Здесь документация для всех вариантов). 

const [id, setId] = useState(1);<SuspenseList revealOrder="forwards">
  <Suspense fallback={<Spinner />}>
    <Details id={id} />
  </Suspense>
  <Suspense fallback={<Spinner />}>
    <Comments id={id} />
  </Suspense>
</SuspenseList>

Обратите внимание, что SuspenseList не имеет отношения к рендерингу или вызовам API порядка загрузки, а лишь сообщает React, когда отображать загруженный компонент. 

useTransition

Ранее в статье шла речь об отображении процесса загрузки и о том, что для улучшения пользовательского опыта можно было бы их совсем не отображать, особенно при условии быстрого интернет соединения. 

Теперь у нас есть возможность это сделать с помощью хука useTransition. Онпозволяет дождаться готовности данных перед их отправкой в DOM, поэтому по сути мы ничего не видим до тех пор, пока не произойдут изменения. useTransition получаетто количество времени, которое мы готовы ждать передтем, как вернуться к отображению процессу загрузки или другому резервному компоненту.

Он также возвращает два компонента: 

  • функцию startTransition, которая сообщает React, какое обновление состояния мы хотим отложить.  
  • логический тип isPending, который сообщает нам, происходит ли переход в данный момент.  
const [id, setId] = useState(1);const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });const onClick = id => {
  startTransition(() => {
    setId(id);
  });
};

И конечно же, использование данного хука неотделимо от использования задержки при извлечении данных. 

Рассмотрим демо ниже: 

Как вы уже успели убедиться, параллельный режим React укомплектован превосходными функциями, которые сильно облегчают работу с распространенными случаями использования, а также позволяют улучшить пользовательский опыт. Дата официального релиза еще не объявлена, но, надеюсь, нам не придется ждать долго. 

Читайте также:


Перевод статьи Sveta Slepner: What is React Concurrent Mode?