Отложенные данные

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

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

Самое приятное то, что вы можете переключаться между одним режимом или другим, просто добавляя или удаляя ключевое слово await из обещания, которое разрешает данные. Райан Флоренс дал прекрасное объяснение этого механизма в своем докладе»Когда получать» (серьезно, это потрясающее выступление. Если вы не смотрели, добавьте его в закладки и посмотрите после того, как закончите читать этот пост!)

Я взглянул на отложенное демонстрационное приложение из Примеры React-маршрутизатора папка и документация чтобы раскрыть весь его потенциал, однако, я не мог найти пример с fetch поэтому я решил попробовать и поиграть с ним, вот что я нашел.


С использованием defer с помощью

я создал фиктивный сервер с MSW для имитации задержки выборки, а также для отображения того же текста из примера.

Вот моя первая наивная попытка:

// Don't copy paste! bad code! keep on reading...
export const loader = async () => {

    return defer({
      critical1: await fetch('/test?text=critical1&delay=250').then(res => res.json()),
      critical2: await fetch('/test?text=critical2&delay=500').then(res => res.json()),
      lazyResolved: await fetch('/test?text=lazyResolved&delay=0').then(res => res.json()),
      lazy1: fetch('/test?text=lazy1&delay=1000').then(res => res.json()),
      lazy2: fetch('/test?text=lazy2&delay=1500').then(res => res.json()),
      lazy3: fetch('/test?text=lazy3&delay=2000').then(res => res.json()),
      lazyError: fetch('/test?text=lazy3&delay=2500').then(res => { 
        throw Error('Oh noo!')
      }),
    });
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Итак, что мы здесь делаем?

Во-первых, возврат «голой выборки» из обычного загрузчика работает, потому что именно этого ожидают загрузчики, и React Router развернет ответ для вас, однако, defer принимает ценности или же обещания которые разрешаются в значения, поэтому, чтобы получить значения, нам нужно вручную развернуть ответ выборки.

Прием отличный! однако вам придется проделать некоторую дополнительную работу, включая выдачу ошибок и распаковку промисов, как мы сделали выше. Используйте платформу, ура! 😅

Вот результат:

Отложить 1

Все выглядело отлично, пока я не открыл вкладку сети и что-то выглядело не так 🤔

Захват 03.11.2022 в 21:39:00

Первые два запроса критических данных, которые используют await ключевое слово происходит водопадом, а не параллельно, как остальные! на самом деле, если вы добавите ожидание ко всем вызовам в объекте отсрочки, все они будут происходить каскадом, что дает!

Неа!, конечно нет! оказывается, я забыл как работает Javascript™️

Описание изображения

Водопад возникает из-за того, что каждый раз, когда вы создаете отдельный awaitон приостановит выполнение перед переходом к следующей строке и перед выполнением следующей выборки.

Что мы хотим сделать, чтобы избежать водопада, так это запускать все эти запросы на выборку одновременно и только await за ответ не фактический fetch.

«Чем раньше вы инициируете выборку, тем лучше, потому что чем раньше она начнется, тем раньше она может закончиться»

@TkDodo

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


// You can copy this one if you want
export const loader = async () => {

// fire them all at once  
  const critical1Promise = fetch('/test?text=critical1&delay=250').then(res => res.json());
  const critical2Promise = fetch('/test?text=critical2&delay=500').then(res => res.json());
  const lazyResolvedPromise = fetch('/test?text=lazyResolved&delay=100').then(res => res.json());
  const lazy1Promise = fetch('/test?text=lazy1&delay=500').then(res => res.json());
  const lazy2Promise = fetch('/test?text=lazy2&delay=1500').then(res => res.json());
  const lazy3Promise = fetch('/test?text=lazy3&delay=2500').then(res => res.json());
  const lazyErrorPromise = fetch('/test?text=lazy3&delay=3000').then(res => { 
        throw Error('Oh noo!')
  });

// await for the response
  return defer({
      critical1: await critical1Promise,
      critical2: await critical2Promise,
      lazyResolved: lazyResolvedPromise,
      lazy1: lazy1Promise,
      lazy2: lazy2Promise,
      lazy3: lazy3Promise,
      lazyError: lazyErrorPromise
  })
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Вы также можете сделать Promise.all() но с приведенным выше примером легче понять, что происходит.

Вот как теперь выглядит вкладка сети, красивые параллельные зеленые полосы.

параллельная выборка

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


Критические данные

В критических данных используется ключевое слово «ожидание», поэтому загрузчик и React Router будут ждать, пока данные не будут готовы перед первым рендерингом (без загрузки счетчиков 🎉).

Что произойдет, если критически важные данные (с использованием ожидания) вернут ошибку? 🤔 ну а загрузчик будет кидать и булькать до ближайшей границы ошибки и уничтожать всю страницу или весь этот отрезок маршрута.

Захват 2022-11-03 и 22 35 24

Если вы хотите выйти из строя изящно и не хотите уничтожать всю страницу, удалите await, что в основном говорит React Router, эй! Меня не волнует, если эти данные не работают, это не так важно (критично), поэтому вместо этого отображайте локализованную границу ошибки. Это именно то, что lazyError делает в первом примере.


Ленивое решение

Мы не используем await на lazyResolved поле, однако мы вообще не видим загрузчика. Как так? На самом деле это замечательная функция отсрочки: если ваши необязательные данные быстрые (быстрее, чем ваши критические данные), то вы вообще не увидите счетчик, потому что ваше обещание будет выполнено к тому времени, когда критические данные закончатся и произойдет первый рендеринг. :

Самая медленная задержка критических данных 500ms но lazyResolved данные принимают только 100ms так что ко времени critical2решается, т. lazyResolved обещание уже разрешено, и данные доступны немедленно.

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

Вы можете поиграть с изменением задержек и увеличением/уменьшением времени, чтобы увидеть, отображаются ли спиннеры или нет. Например, если мы увеличим задержку для lazyResolved к 3500ms мы увидим загрузочный счетчик.

Захват 03.11.2022 в 22:41:40

Вывод

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

Исходный код примеров доступен здесь: