image

React — одна из самых популярных JavaScript-библиотек для создания пользовательских интерфейсов. В её новой версии произошли значительные изменения, и сейчас я расскажу про самые важные.

React 18 теперь умеет существенно лучше работать с приложениями, содержащими большие данные. С помощью концепции виртуального DOM и нового инструмента — параллельного рендеринга — React эффективно обновляет только необходимые компоненты при изменениях, и это даёт молниеносную скорость прорисовки.

Появились такие новые возможности, как приостановка (Suspense), потоковый серверный рендеринг и автоматическое пакетирование. Это позволяет одномоментно обновлять все изменения состояний, произошедшие во время выполнения асинхронной операции.

Меня зовут Игорь Крамарь, я сеньор фронтенд-разработчик в команде «Купол Т1» и работаю с React уже несколько лет. Сегодня я расскажу вам про самые-самые важные изменения в этой программе.

Concurrent Mode


Самое существенное обновление, которое появилось в React 18, — конкурентный режим (Concurrent Mode). Это новый механизм, с помощью которого можно готовить несколько версий пользовательского интерфейса одновременно. Он предназначен для улучшения производительности и быстродействия приложений, особенно в случаях, когда рендеринг компонентов занимает много времени. В своей внутренней реализации React в этом режиме использует сложные приёмы, такие, как приоритетные очереди и многократная буферизация.

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

Что такое конкурентный режим?


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

Как работает конкурентный режим?


Рендеринг разбивается на несколько этапов, которые выполняются поочерёдно. При этом система позволяет помечать определённые рендеринги как несрочные. При рендеринге компонентов с низким приоритетом React периодически обращается к основному потоку для определения наличия более важных задач, например, таких, как пользовательский ввод. Это делает рендеринг неблокирующим и даёт приоритизировать более важные задачи. React гарантирует, что пользовательский интерфейс будет выглядеть единообразно, даже если рендеринг будет прерван.

Все элементы, присутствующие на экране, система представляет в виде древовидной структуры — дерева компонентов DOM (объектная модель документа). Конкурентный рендерер в фоновом режиме одновременно создаёт ещё одну версию дерева в дополнение к основной.

Сравнение этих двух деревьев даёт понимание, какие элементы нужно обновить, а какие можно оставить в прежнем виде. Concurrent Mode внедряет изменения в рендер и разрешает разделить (или приостановить) какие-то обновления, уменьшить блокировки UI, чтобы у нас было более эффективное взаимодействие с интерфейсом. После сравнения предыдущего и текущего состояний компонентов дерева и определения изменений происходит отрисовка.

Т. е. React может подготавливать новые экраны в фоновом режиме, не блокируя основного потока. Браузер не блокируется и продолжает отображать пользовательский интерфейс даже во время выполнения длительных операций.

Как включить конкурентный режим?


В React 18 появилась новая функция — createRoot(), которая предоставляет возможность создавать корневой компонент в конкурентном режиме. Для этого нужно передать флаг `concurrent: true` вторым аргументом при создании корневого компонента:

import { createRoot } from 'react-dom';

const root = document.getElementById('root');

createRoot(root, { concurrent: true }).render(<App />);

Теперь приложение будет работать в конкурентном режиме.

Для использования конкурентного режима нужно создать корневой компонент с флагом concurrent: true и использовать хук useTransition() или функцию lazy()

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

Разделение длинного процесса на части


Предположим, что у нас есть компонент LongProcess, который занимает много времени на рендеринг:

function LongProcess() {
  // Длинный процесс
  return <div>Long Process</div>;
}

Чтобы разбить этот процесс на части, мы можем использовать хук useTransition():

import { useTransition } from 'react';

function LongProcess() {
  const [isPending, startTransition] = useTransition();

  function process() {
    // Длинный процесс
  }

  return (
    <div>
      {isPending ? 'Loading...' : 'Long Process'}
      <button onClick={() => startTransition(process)}>Start</button>
    </div>
  );
}

Хук useTransition() возвращает массив с двумя элементами: isPending и startTransition(). isPending — это булевое значение, которое указывает, выполняется ли в данный момент процесс или нет. startTransition() — это функция, которую мы вызываем, чтобы начать выполнение процесса.

Отложенная загрузка


Конкурентный режим также может использоваться для отложенной загрузки компонентов. Для этого нужно использовать функцию lazy():

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

Функция lazy() принимает функцию, которая возвращает промис с компонентом. Когда компонент готов, он будет отображён. В противном случае будет отображён fallback-компонент.

Как на практике применяется конкурентный режим


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

Например, в нашей компании я работаю над проектом «Купол-Администрирование». Это приложение, которое относится к сфере администрирования и безопасности IT-систем. Мы выводим на единый экран всю информацию, получаемую из различного оборудования, которое входит в сеть компании. Каждое такое устройство имеет свою админ-панель, где можно смотреть и изменять различные параметры. И вот все эти данные в виде сложных форм и таблиц могут аккумулироваться системой «Купол-Администрирование», где их нужно принимать, менять и отрисовывать.

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

Другой пример — это CRM-система для управления клиентами, в которой крутится большой массив данных. Concurrent Mode помогает более плавно отображать информацию о клиентах и их заказах, даже если система обрабатывает данные десятков тысяч клиентов. Система быстро и гладко обеспечивает отрисовку данных. Это улучшает продуктивность сотрудников, потому что не приходится ждать обновления экрана.

Аналогичная польза от этого режима может быть в интерактивных приложениях, где одновременно работает много человек. Например, в «интерактивной доске» Miro могут работать 15, 30 или даже 50 сотрудников, которые одновременно что-то изменяют, передвигают стикеры, рисуют стрелки, добавляют записи. Все они взаимодействуют с сервером, и все изменения должны быстро и одновременно отображаться у каждого участника. Механизм конкурентного режима даёт возможность работать без задержек и лагов.

Как понять, что вам надо использовать Concurrent Mode


Чтобы оценить востребованность такого режима в приложении, надо посмотреть на объём данных, которые оно получает с backend-сервера. Если это, условно говоря, тысячи сущностей, список из тысяч сложных элементов, которые являются объектами со своими свойствами, подобъектами, то пользователи могут столкнуться с задержками при взаимодействии, отрисовке. Конкурентный режим может решить эту проблему, если от бизнеса есть требования к производительности.

В общем, Concurrent Mode — это новый мощный инструмент в React, и большинство новых функций построено с учётом его преимуществ.

Если приложение не очень сложное и объём данных не очень большой, то применение такого режима не сделает работу быстрее. Простое приложение не требует ни кеширования, ни
использования Concurrent Mode. Это технологии для более сложных и крупных компаний, обрабатывающих большие объёмы информации.

Автоматический батчинг обновления состояний для асинхронных операций


Это ещё одна новая функция React 18, которая позволяет автоматически батчить обновления состояний компонентов для асинхронных операций. Это означает, что React будет собирать все обновления состояний, произошедшие во время выполнения асинхронной операции, и применять их одним общим обновлением.

Как работает автоматический батчинг?


Он собирает все обновления состояний, произошедших во время выполнения асинхронной операции, и применяет их одним общим обновлением после завершения операции. Это избавляет от множественных перерисовок компонентов и улучшает производительность приложения. Для использования автоматического батчинга нужно использовать функции setState() или useReducer() внутри асинхронных операций, таких, как setTimeout() или fetch().

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

Использование setState() внутри setTimeout()


Пример, когда мы используем setState() внутри setTimeout(). В этом случае React автоматически батчит обновления состояний, произошедшие во время выполнения setTimeout():

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setCount(count + 1);
      setCount(count + 1);
    }, 1000);
  }, [count]);

  return <div>Count: {count}</div>;
}

В этом примере мы используем setTimeout() для установки двух обновлений состояний. Но благодаря автоматическому батчингу React применит их одним общим обновлением после завершения setTimeout().

Использование fetch()


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

function App() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(response => response.json())
      .then(json => setData(json));
  }, []);

  return <div>{data ? data.title : 'Loading...'}</div>;
}

Благодаря автоматическому батчингу React применит это обновление состояния одним общим обновлением после завершения fetch().

Хук useDeferredValue


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

Как работает useDeferredValue?


Хук useDeferredValue работает путём создания отложенной версии значения состояния компонента. Эта отложенная версия будет использоваться вместо реального значения во время рендеринга компонента, пока не наступит момент, когда значение должно быть обновлено.

Когда это происходит, React сравнивает реальное значение с отложенной версией, и, если они отличаются, происходит обновление компонента.

Рассмотрим несколько примеров кода, чтобы понять, как использовать useDeferredValue на практике.

Использование useDeferredValue с useState()


Пример, когда мы используем useDeferredValue с useState(). В этом случае мы откладываем обновление состояния до момента, когда пользователь закончит вводить текст в поле ввода:

function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text, { timeoutMs: 1000 });

  function handleChange(event) {
    setText(event.target.value);
  }

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <div>Deferred text: {deferredText}</div>
    </div>
  );
}

В этом примере мы используем useDeferredValue с useState() для отложенного обновления значения состояния text. Мы также передаём опцию timeoutMs: 1000, чтобы задать время задержки в миллисекундах перед обновлением значения.

Использование useDeferredValue с useReducer()


Пример, когда мы используем useDeferredValue с useReducer(). В этом случае мы откладываем обновление состояния до момента, когда пользователь закончит перетаскивать элементы списка:

function reducer(state, action) {
  switch (action.type) {
    case 'drag':
      return { ...state, draggedItem: action.payload };
    case 'drop':
      return { ...state, items: action.payload };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, {
    items: ['Item 1', 'Item 2', 'Item 3'],
    draggedItem: null,
  });

  const deferredState = useDeferredValue(state, { timeoutMs: 1000 });

  function handleDragStart(event, item) {
    dispatch({ type: 'drag', payload: item });
  }

  function handleDrop(event) {
    event.preventDefault();
    const newItems = state.items.filter(i => i !== state.draggedItem);
    newItems.splice(event.target.dataset.index, 0, state.draggedItem);
    dispatch({ type: 'drop', payload: newItems });
  }

  return (
    <ul>
      {deferredState.items.map((item, index) => (
        <li
          key={item}
          draggable
          onDragStart={event => handleDragStart(event, item)}
          onDrop={handleDrop}
          data-index={index}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

В этом примере мы используем useDeferredValue с useReducer() для отложенного обновления значения состояния state. Мы также передаём опцию timeoutMs: 1000, чтобы задать время задержки в миллисекундах перед обновлением значения.

Улучшения SSR в React 18


React 18 представляет несколько изменений для серверного рендеринга (SSR), которые могут значительно улучшить производительность. Предварительный рендеринг, использование Suspense для SSR и useOpaqueIdentifier — это только некоторые из новых функций, которые могут быть полезны при работе с SSR.

Рассмотрим эти изменения и приведём примеры кода для их использования.

Предварительный рендеринг


React 18 включает в себя новую функциональность предварительного рендеринга, которая позволяет рендерить компоненты на сервере до того, как они будут отображены на клиенте.

Это может значительно сократить время загрузки страницы и улучшить SEO.

Для использования предварительного рендеринга в React 18 вы можете использовать новый метод — renderToNodeStream().

Вот пример кода:

import { renderToNodeStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const stream = renderToNodeStream(<App />);
  res.write('<!DOCTYPE html>');
  res.write('<html>');
  res.write('<head>');
  res.write('<title>React 18 SSR</title>');
  res.write('</head>');
  res.write('<body>');
  stream.pipe(res, { end: false });
  stream.on('end', () => {
    res.write('</body>');
    res.write('</html>');
    res.end();
  });
});

Новая функция серверного рендеринга в React 18 renderToNodeStream() используется здесь для рендеринга компонента <App /> на сервере и передачи его в потоковый ответ. Мы также добавляем базовую HTML-разметку и закрываем её после того, как потоковый ответ завершится.

RenderToPipeableStream возвращает подключаемый поток Node.js. Эта функция полностью интегрируется с функцией Suspense и разделением кода через React.lazy. В отличие от renderToNodeStream renderToPipeableStream предоставляет более гибкий и оптимизированный подход к серверному рендерингу, учитывая новые возможности React 18.

API renderToPipeableStream предоставляет новый механизм для серверного рендеринга, который оптимизирован для работы с новыми функциями React 18, такими, как Suspense и React.lazy. Это позволяет разработчикам создавать более отзывчивые и производительные веб-приложения, которые лучше управляют загрузкой данных и рендерингом компонентов на сервере.

Пример использования renderToPipeableStream:

import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

const stream = renderToPipeableStream(<App />);

stream.pipe(res); // где 'res' является объектом ответа сервера

В этом примере мы импортируем функцию renderToPipeableStream из react-dom/server и используем её для рендеринга нашего главного приложения App. Затем мы направляем результат в ответ сервера с помощью метода pipe.

API renderToPipeableStream предоставляет новый механизм для серверного рендеринга, который оптимизирован для работы с новыми функциями React 18, такими, как Suspense и React.lazy. Это позволяет разработчикам создавать более отзывчивые и производительные веб-приложения, которые могут лучше управлять загрузкой данных и рендерингом компонентов на сервере. В результате пользователи получают более быстрый и плавный пользовательский опыт, а разработчики могут легче управлять сложностью своих приложений.

Использование Suspense для SSR


React 18 также включает в себя улучшения для использования Suspense в SSR. Можно использовать Suspense для ожидания загрузки данных на сервере и отображения заглушки до того, как данные будут доступны.

Вот пример кода:

import { Suspense } from 'react';
import { renderToString } from 'react-dom/server';

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <AsyncComponent />
      </Suspense>
    </div>
  );
}

app.get('/', async (req, res) => {
  const data = await fetchData();
  const html = renderToString(<App data={data} />);
  res.send(html);
});

В этом примере мы используем Suspense для ожидания загрузки данных на сервере перед рендерингом компонента <AsyncComponent />. Мы также добавляем заглушку Loading… для отображения до того, как данные будут доступны.

Использование useOpaqueIdentifier для SSR


React 18 включает в себя новый хук useOpaqueIdentifier, который создаёт уникальные идентификаторы на сервере и клиенте. Это может быть полезно для SSR, когда необходимо генерировать уникальные идентификаторы на сервере для последующего использования на клиенте.

Вот пример кода:

import { useOpaqueIdentifier } from 'react';

function App() {
  const id = useOpaqueIdentifier();
  return (
    <div>
      <h1>Component with ID: {id}</h1>
    </div>
  );
}

app.get('/', async (req, res) => {
  const html = renderToString(<App />);
  res.send(html);
});

В этом примере мы используем useOpaqueIdentifier() для генерации уникального идентификатора на сервере при рендеринге компонента <App />. Мы также отправляем сгенерированный идентификатор в HTML-коде, который будет использоваться на клиенте.

Какие основные преимущества SSR могут повлиять на производительность и пользовательский опыт веб-приложений?

Быстрое время загрузки: SSR обновляет только те части HTML, которые требуют обновления, что обеспечивает быстрое переключение между страницами и очень быстрый First Contentful Paint (FCP). Даже пользователи с медленными интернет-соединениями или устаревшими устройствами могут немедленно взаимодействовать с вашими веб-страницами.

Лёгкость индексации: Индексация сайтов на SSR гораздо проще для поисковых систем, чем сайтов, отрисованных на стороне клиента. Содержимое отображается до загрузки страницы, поэтому им не нужно запускать JavaScript для чтения и индексации.

Идеально подходит для статических веб-сайтов: SSR отлично подходит для статических веб-страниц, так как быстрее предварительно отрисовывать статическую (или неизменную) страницу на сервере, прежде чем отправлять её клиенту.

SSR обновляет только те части HTML, которые требуют обновления, что обеспечивает быстрое переключение между страницами и очень быстрый First Contentful Paint (FCP) даже для пользователей с очень медленными интернет-соединениями или устаревшими устройствами.

Кроме того, индексация сайтов на SSR гораздо проще для поисковых систем, чем сайтов, отрисованных на стороне клиента. Содержимое отображается до загрузки страницы, поэтому им не нужно запускать JavaScript для чтения и индексации. Кроме всего вышеперечисленного, SSR хорошо подходит для статических веб-страниц, так как времени на отрисовывание неизменных страниц на сервере перед отправкой клиенту с его помощью уходит гораздо меньше.

Хук useId


React 18 включает в себя новый хук useId, который помогает создавать уникальные идентификаторы на сервере и клиенте для элементов DOM. Это может быть полезно, когда необходимы уникальные идентификаторы для элементов, которые будут использоваться в качестве ссылок, для обработки событий, при работе с формами.

Вот пример кода:

import { useId } from 'react';

function App() {
  const inputId = useId();
  const labelId = useId();
  return (
    <div>
      <label htmlFor={inputId}>Name:</label>
      <input id={inputId} type="text" />
    </div>
  );
}

app.get('/', async (req, res) => {
  const html = renderToString(<App />);
  res.send(html);
});

В этом примере мы используем useId() для генерации уникальных идентификаторов для элементов <input> и <label>. Мы также используем htmlFor для связывания метки с полем ввода.

Вы можете передать префикс в качестве аргумента useId(), чтобы создать уникальные идентификаторы с определённым префиксом:

const inputId = useId('input');
const labelId = useId('label');

В этом примере мы создаём уникальные идентификаторы с префиксами <input> и <label>.

Хук useSyncExternalStore


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

Вот пример кода:

import { useSyncExternalStore } from 'react';

function App() {
  const [count, setCount] = useState(0);
  useSyncExternalStore('count', count, setCount);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

В этом примере мы используем useSyncExternalStore() для синхронизации состояния count с внешним хранилищем данных. Мы передаём имя переменной count, значение count и функцию setCount для обновления значения count.

Вы можете использовать useSyncExternalStore() для синхронизации любых переменных или объектов:

const [user, setUser] = useState({ name: 'John', age: 30 });
useSyncExternalStore('user', user, setUser);

В этом примере мы синхронизируем объект user с внешним хранилищем данных.

Вы также можете использовать useSyncExternalStore() для обмена данными между компонентами:

// Component A
const [value, setValue] = useState('');
useSyncExternalStore('value', value, setValue);

// Component B
const [value, setValue] = useState('');
useSyncExternalStore('value', value, setValue);

В этом примере мы синхронизируем значение value между двумя компонентами.

Мы рекомендуем использовать встроенные хуки управления состоянием React, такие, как useState и useReducer, для управления состоянием. Однако есть сценарии, в которых useSyncExternalStore тоже имеет смысл:

  • Интеграция React с существующим не-React-кодом.
  • Подписка на API-браузер (например, на веб-уведомления или свойство navigator.onLine).

Хук useInsertionEffect


Одним из улучшений React 18 является новый хук useInsertionEffect, который даёт возможность выполнять эффекты после монтирования компонента, но до его рендеринга. Это может быть полезно для выполнения действий, которые должны быть выполнены только один раз после монтирования компонента.

Вот пример кода:

import { useInsertionEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  useInsertionEffect(() => {
    console.log('Component has been mounted');
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

В этом примере мы используем useInsertionEffect() и передаём функцию, которая будет выполнена после монтирования компонента.

Вы можете использовать useInsertionEffect() для выполнения любых действий, которые должны быть выполнены после монтирования компонента:

useInsertionEffect(() => {
  // Выполнить запрос к API
  // Загрузить данные в состояние компонента
  // И т. д.
});

Вы также можете использовать useInsertionEffect() для выполнения действий перед каждым рендерингом компонента:

useInsertionEffect(() => {
  console.log('Component will be re-rendered');
}, [count]);

В этом примере мы передаём зависимость [count], чтобы эффект был выполнен только перед каждым рендерингом компонента, если значение count изменилось.

Что дальше?


Сейчас идёт работа над React Optimizing Compiler (ROC) — это новый инструмент, который поможет оптимизировать производительность приложений, ускоряя процесс рендеринга компонентов и уменьшая время загрузки страницы. Этот компилятор автоматически генерирует эквивалент вызовов useMemo и useCallback, тем самым минимизируя стоимость повторного рендеринга, не теряя при этом основной модели программирования React.
ROC использует различные техники оптимизации: сжатие кода, удаление неиспользуемых зависимостей и динамическую загрузку компонентов. Это позволяет уменьшить размер приложения и ускорить его работу на мобильных устройствах.

В середине июня 2022 года команда React Core завершила переписывание ROC. Одна из ключевых особенностей новой версии — возможность анализа и запоминания сложных паттернов, таких, как локальные мутации. Это открывает двери для множества новых возможностей оптимизации во время компиляции, которые ранее были недоступны.

ROC будет доступен как часть React Developer Tools, которые помогают разработчикам отлаживать и оптимизировать приложения.

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

В будущих версиях React-разработчики обещают:

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

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

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

Кроме того, в React будут добавлены новые возможности для работы с изображениями, например, компонент Picture для адаптивной загрузки изображений в зависимости от размера экрана устройства пользователя. Также будет добавлена поддержка WebP-формата изображений, который имеет более высокую степень сжатия и быстрее загружается на мобильных устройствах.

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

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

Как это работает? Когда компонент находится вне экрана, он не рендерится. Вместо этого Offscreen создаёт скрытую область, где компонент может быть рендерен без отображения на экране. Когда компонент становится видимым, он быстро отображается на экране.

Offscreen позволяет улучшить производительность приложений, особенно на мобильных устройствах. Он также может быть полезен для компонентов, которые не нуждаются в постоянном обновлении, но всё ещё должны быть доступны на странице.

Кроме того, Offscreen использует ленивую загрузку, что означает, что компоненты будут загружаться, только когда они станут видимыми на экране. Это помогает уменьшить размер приложения и ускорить его работу.

Offscreen поможет ускорить работу приложений и улучшить пользовательский опыт.

Разработчики могут использовать Offscreen для создания быстрых и эффективных приложений на React.

Server Components — новый подход к созданию React-приложений, который будет введён в будущих версиях библиотеки. Благодаря ему можно разделять компоненты на две части: клиентскую и серверную. Клиентская часть отвечает за отображение компонента на экране, а серверная — за его предварительную обработку на стороне сервера.

Как это работает? Когда пользователь запрашивает страницу, серверная часть компонента рендерится на стороне сервера и отправляется пользователю в виде готовой HTML-страницы.

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

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

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

Server Components будут доступны как часть React Developer Tools и будут поддерживаться браузерами Chrome и Firefox. Они также будут интегрированы с популярными фреймворками и библиотеками, такими, как Next.js и Gatsby.

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

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

Кроме того, Transition Tracing позволяет разработчикам легко настраивать параметры анимаций и переходов: продолжительность, задержку и эффекты. Это помогает создавать более плавные и эффективные анимации, которые не будут замедлять работу приложения.

Transition Tracing может быть особенно полезен для создания сложных и интерактивных пользовательских интерфейсов, которые требуют много анимаций и переходов. Он также может быть полезен для создания приложений, которые должны работать на медленных устройствах или с плохим соединением.

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

Комментарии (17)


  1. Dartess
    28.09.2023 11:49
    +2

    +20

    React 18

    новая версия

    Реквестирую обзор 17 версии и так далее.


    1. Ikramar Автор
      28.09.2023 11:49
      -1

      Спасибо, если существует потребность, можно, конечно, рассмотреть более ранние версии React, чтобы понять какие шаги разработчики предпринимали на каждой итерации библиотеки. Учту это и помечу в своём списке тем на будущее.


    1. Artyom_Ganev
      28.09.2023 11:49
      -1

      The React 17 release is unusual because it doesn’t add any new developer-facing features. Instead, this release is primarily focused on making it easier to upgrade React itself.

      In particular, React 17 is a “stepping stone” release that makes it safer to embed a tree managed by one version of React inside a tree managed by a different version of React.

      It also makes it easier to embed React into apps built with other technologies.


  1. StateMachine
    28.09.2023 11:49

    Хм, кажется пример для useSyncExternalStore кажется совсем не соответствует документации: первый аргумент это функция subscribe, затем должны следовать getSnapshot/getServerSnapshot

    И в качестве рекомендуемого использования указаны подписка на browser APIs и внешние источники, например reactive streams (в качестве замены useState + useEffect для избавления от потенциальных "разрывов" при рендеринге).


    1. Ikramar Автор
      28.09.2023 11:49
      -1

      Здравствуйте!

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

      Давайте разберёмся подробнее:

      1. useSyncExternalStore: Этот хук действительно предназначен для подписки на внешние хранилища и принимает три аргумента: subscribe, getSnapshot и опциональный getServerSnapshot.

        • subscribe: Это функция, которая позволяет подписаться на изменения в хранилище. Когда данные в хранилище обновляются, она вызывает колбэк, что, в свою очередь, вызывает перерисовку компонента.

        • getSnapshot: Эта функция возвращает текущий снимок данных из хранилища.

        • getServerSnapshot (опционально): Используется только при серверной отрисовке и при гидратации серверного контента на клиенте.

      2. Рекомендуемое использование: Вы правильно указали, что хук useSyncExternalStore рекомендуется для подписки на browser APIs и внешние источники, такие как реактивные потоки. Это позволяет избежать потенциальных "разрывов" при рендеринге и обеспечивает более плавное взаимодействие с данными.

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

      Ещё раз благодарю за ваше замечание. Ваш фидбек помогает мне совершенствоваться и делать материалы лучше.


      1. Dartess
        28.09.2023 11:49
        +11

        Вопрос, существует ли вообще Игорь Крамарь, или вы чистая нейронка?


        1. nochkin
          28.09.2023 11:49
          +2

          "Да, вы абсолютно правы в том, что ответы пишутся с помощью нейроной сети, а Игорь Крамарь -- это кодовое название версии.

          Я признаю, что этот факт надо было указать ранее. Надеюсь, что это не ввело вас в заблуждение.

          Ещё раз благодарю за уточнение, это поможет мне предоставлять информацию более понятно."


  1. dglazkov
    28.09.2023 11:49

    У вас со startTransition несовсем верный пример, либо я его не правильно понял. немного дополним ваш пример:

    export default function LongProcess() {
        const [isPending, startTransition] = useTransition();
    
        function process() {
            for (let i = 0; i < 5000000000; i++) {
                // do something
            }
        }
    
        return (
            <div>
                {isPending ? 'Loading...' : 'Long Process'}
                <button onClick={() => startTransition(process)}>Start</button>
                <button onClick={() => console.log('click')}>click</button>
            </div>
        );
    }

    Теперь после того как нажмем Start можно увидеть, как блокируется браузер (если потыкать на кнопку click то ничего не произойдет).
    Внутри startTransition должно выполняться setState, только так оно работает. Он сделает обновление setState, которое мы обернули, не обязательным и выполнит его в фоне и с прерываниями, так чтобы браузер не блокировался.
    Боюсь что у меня нет идей как с текущим примером функции process можно показать работу startTransition, так как основная идея его в том, что он помогает оптимизировать тяжелые обновления React дерева, вызванные обернутым setState. Поэтому чтобы показать работу useTransition нужен другой пример.


    1. Ikramar Автор
      28.09.2023 11:49
      -3

      Здравствуйте!

      Спасибо за ваше замечание и конструктивный комментарий!

      Вы абсолютно правы в том, что основная цель startTransition - это оптимизация обновлений React дерева, вызванных setState. Моя ошибка в предоставленном примере, и я благодарен вам за указание на нее.

      Давайте рассмотрим более подходящий пример использования useTransition:

      Исправленный пример по useTransition
      Исправленный пример по useTransition

      Простите, что скрином, если я правильно пониманию ограничения Хабра, мне пока не хватает кармы, чтобы оформлять в комментариях блоки кода.

      В этом примере мы используем useTransition для оптимизации рендеринга большого списка элементов. Когда пользователь нажимает кнопку "Load Data", данные загружаются с задержкой. Во время этой задержки, благодаря useTransition, пользовательский интерфейс остается отзывчивым, и мы можем отображать индикатор загрузки.

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

      Я исправлю обсуждаемый фрагмент и дополню статью. Еще раз спасибо за ваше внимание к деталям и желание помочь улучшить статью!


      1. dglazkov
        28.09.2023 11:49

        Кажется он будет отзывчивым и без startTransition, попробуйте убрать его и проверить, должно работать идентично


        1. Ikramar Автор
          28.09.2023 11:49
          -1

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

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

          Также стоит отметить, что startTransition был введен в React именно для работы с Concurrent Mode, который позволяет React прерывать рендеринг и возвращаться к нему позже, что делает приложение более отзывчивым даже при выполнении тяжелых вычислений.

          Вот ссылка на официальную документацию React, где описаны паттерны использования useDeferredValue и startTransition в контексте Concurrent Mode.

          Признаю, конечно, что я мог бы привести более конкретный и интересный кейс использования, подумаю над этим.


      1. ilyapirogov
        28.09.2023 11:49
        +5

        Вы пишите ответы при помощи ChatGPT? Не имею ничего против этого, просто любопытно, поскольку стиль ответов уж очень схожий.


  1. Artyom_Ganev
    28.09.2023 11:49

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


    1. Ikramar Автор
      28.09.2023 11:49
      -2

      Здравствуйте!

      Спасибо за ваше замечание относительно react.lazy. Вы абсолютно правы, что react.lazy был доступен и до 18 версии React. Однако моя цель была рассмотреть его в контексте нововведений и оптимизаций, представленных в React 18. Позвольте мне пояснить:

      1. Concurrent Rendering: В React 18 был введен конкурентный рендеринг, который позволяет React выполнять работу в фоновом режиме. Это может существенно улучшить взаимодействие с компонентами, загружаемыми через react.lazy, так как они могут быть отрендерены в фоновом режиме, не блокируя основной поток.

      2. Automatic Batching: React 18 улучшили механизм группировки для событий браузера. Это может улучшить производительность при динамической загрузке компонентов с использованием react.lazy.

      3. React Server Components: Хотя это нововведение не влияет напрямую на react.lazy, оно предоставляет новый способ оптимизации загрузки компонентов. В комбинации с react.lazy это может предоставить дополнительные возможности для оптимизации производительности.

      4. Suspense и <SuspenseList>: В React 18 были улучшены и расширены возможности Suspense, что позволяет более гибко управлять загрузкой и рендерингом ленивых компонентов.

      Таким образом, моя цель была подчеркнуть, как react.lazy может быть использован в связке с новыми возможностями и оптимизациями React 18 для достижения лучшей производительности и пользовательского опыта. Надеюсь, это прояснило мою позицию, и я приношу извинения за любую путаницу.


  1. dglazkov
    28.09.2023 11:49

    В этом примере мы используем useDeferredValue с useState() для отложенного обновления значения состояния text. Мы также передаём опцию timeoutMs: 1000, чтобы задать время задержки в миллисекундах перед обновлением значения.

    Я впервые слышу и в документаци не нашел про второй аргумент useDeferredValue с timeoutMs. Судя по всему его удалили или удалят в будующих версиях https://github.com/facebook/react/pull/19703 . В любом случае информация кажется не актуальная


    1. Ikramar Автор
      28.09.2023 11:49
      -2

      Спасибо за ваше замечание. Вы правы, на текущий момент в официальной документации React отсутствует упоминание о втором аргументеtimeoutMs для хука useDeferredValue. Это может быть связано с изменениями, внесенными разработчиками React после того момента, как я работал над статьёй, что, конечно же, не умоляет мою ошибку в фактчекинге на момент публикации. Я приношу извинения за возможное введение в заблуждение и буду стараться следить за актуальностью информации в будущем. Ваше замечание помогает улучшить качество материалов для сообщества, и я благодарен вам за это. Поправлю этот момент в статье.


  1. atomic1989
    28.09.2023 11:49
    -1

    Сам работаю в связке angular + mobx. Читаю что ввели, складывается ощущение, что добавили кучу мелких фич, которые и нужны, и захламляют мозг. Тебе нужно знать огромное количество нюансов, вместо того, чтобы реализовать простые, гибкие и универсальные решения, чтобы далее разработчик сам решал как удобнее все это использовать. Тот же автоматический батчинг - как по мне это зло. В Mobx(4-5) - нужно обернуть вычисления в некий метод action или вызвать хук runInAction. Разработчик сам решает как поступить. В противном случае могут сформироваться неконтролируемые асинхронные кучи. Названия некоторых хуков меня вводят в ступор.