React
Паттерны загрузки данных
Открывается страница профиля. Сначала спиннер. Через секунду появляется имя пользователя - и снова спиннер, теперь на ленте постов. Ещё через секунду появляются посты, и спиннер запускается уже на комментариях. Три последовательных ожидания там, где сервер мог бы отдать всё за одно. Это request waterfall - самый частый источник медленных интерфейсов, и почти всегда он возникает не из-за медленной сети, а из-за того, как написан код загрузки.
- Vercel и Next.js: документация App Router открывается разделом про waterfall'ы как про дефолтную ошибку новичка
- Amazon: исследования показывают, что каждые 100 мс задержки заметно снижают конверсию, и цепочки запросов бьют по этому напрямую
- GitHub: страница репозитория грузит дерево файлов, README и список коммитов параллельно, а не по очереди
- Airbnb: карточки объявлений и карта подгружаются независимо, чтобы карта не ждала пока приедут отзывы
- React Docs: официальное руководство рекомендует render-as-you-fetch и предупреждает про fetch-on-render как про источник waterfall'ов
Предварительные знания
- Хук useEffect и понимание, когда он срабатывает после рендера
- Промисы и async/await в JavaScript, включая Promise.all
- Базовое представление о fetch и HTTP-запросах из браузера
Как сообщество перешло от fetch-on-render к render-as-you-fetch
Долгое время канонический способ загрузки данных в React был один: компонент монтируется, в useEffect уходит fetch, до ответа показывается спиннер. Этот паттерн назвали fetch-on-render, и у него обнаружился структурный изъян. Дочерний компонент начинает грузить свои данные только после того, как родитель закончил грузить свои и отрендерился. Запросы выстраиваются в цепочку. С появлением Suspense и Concurrent React команда сформулировала альтернативу render-as-you-fetch: запрос стартует как можно раньше, в идеале параллельно с другими и до рендера компонента, а сам компонент при чтении ещё не готовых данных приостанавливается через Suspense. Библиотеки вроде Relay, а позже TanStack Query и React Router закрепили этот подход как стандарт.
Waterfall против параллельной загрузки
Request waterfall возникает, когда запросы выполняются по очереди, хотя могли бы идти одновременно. Классический случай: загрузка пользователя, затем по его id загрузка постов, затем по постам - комментарии. Если запросы действительно зависят друг от друга по данным, цепочка неизбежна. Но чаще зависимости нет, а последовательность появляется случайно - из-за того, что каждый await стоит на отдельной строке.
В UI waterfall чаще прячется не в одной функции, а в дереве компонентов. Родитель грузит данные в useEffect, рендерится, и только тогда монтируется ребёнок, который запускает свой useEffect. Сеть простаивает между этими шагами. Лечится это либо подъёмом загрузки наверх, либо стартом запросов до рендера.
Promise.all отклоняется целиком, если падает хотя бы один промис. Когда частичный результат допустим (например, показать пользователя без статистики), уместнее Promise.allSettled - он вернёт исход каждого запроса отдельно, и страница не упадёт из-за одного сбойного источника.
Три запроса fetchUser, fetchPosts, fetchStats не зависят друг от друга. Что устранит waterfall?
Render-as-you-fetch и где грузить данные
Fetch-on-render означает: сначала рендер, потом из него запускается запрос. Render-as-you-fetch переворачивает порядок - запрос стартует раньше, ещё до того как компонент попытается прочитать данные. Когда компонент рендерится и данных пока нет, он приостанавливается через Suspense, а сеть тем временем уже работает. Сеть и рендеринг идут параллельно, а не друг за другом.
- Fetch-on-render — Компонент монтируется, в useEffect уходит запрос, до ответа спиннер. Дочерние запросы выстраиваются в цепочку за родительскими. Просто писать, легко получить waterfall.
- Render-as-you-fetch — Запрос инициируется до или одновременно с рендером (в обработчике перехода, в роутере, на сервере). Компонент читает уже летящий промис и приостанавливается через Suspense. Сеть стартует раньше.
Второй ключевой выбор - где физически идёт загрузка. Данные первого экрана и приватные источники (запрос с секретным ключом, прямой доступ к базе) разумнее грузить на сервере: меньше раунд-трипов, секреты не утекают в браузер, клиент получает готовый HTML. Интерактивные догрузки в ответ на действия (фильтры, бесконечный скролл, поиск по мере ввода) естественнее живут на клиенте.
| Сценарий | Где грузить | Почему |
|---|---|---|
| Контент первого экрана | Сервер | Меньше задержки до первого байта, готовый HTML, лучше SEO |
| Запрос с секретным API-ключом | Сервер | Ключ не должен попадать в клиентский бандл |
| Поиск по мере ввода | Клиент | Высокая интерактивность, частые мелкие запросы по действию |
| Бесконечный скролл ленты | Клиент | Догрузка по жесту, состояние живёт в браузере |
Лучшее место инициировать запрос - событие, которое уже известно до рендера: клик по ссылке, начало навигации, загрузка серверного компонента. Роутеры вроде React Router и TanStack Router дают для этого loader'ы, которые стартуют загрузку параллельно с переходом, а не после монтирования экрана.
В чём ключевое отличие render-as-you-fetch от fetch-on-render?
Кэширование и состояния загрузки и ошибок
Сырой fetch без кэша легко приводит к дублям: два компонента запрашивают одного пользователя и шлют два одинаковых запроса. Кэш с дедупликацией склеивает их в один. Дальше встаёт вопрос свежести: сколько данные считаются актуальными (stale time), когда их перезапрашивать, как инвалидировать после мутации. Именно эту работу берут на себя клиентские библиотеки данных.
Состояния загрузки и ошибки - не украшение, а часть контракта. У любого асинхронного UI есть как минимум три исхода: ждём, получили данные, упали. Пропуск любого из них даёт либо пустой экран, либо повисший спиннер, либо краш на необработанном reject. Для loading-состояний при render-as-you-fetch удобен Suspense, для ошибок - error boundary.
В 2026 году для клиентской загрузки доминируют две связки. TanStack Query даёт кэш, дедупликацию, фоновое обновление и инвалидацию. Роутеры (React Router, TanStack Router, Next.js) добавляют loader'ы, которые стартуют запрос параллельно с навигацией. Сырой useEffect+fetch остаётся уместным разве что для разовых простых случаев.
Зачем клиентской библиотеке данных дедупликация запросов по ключу?
Связь с другими темами
Этот урок задаёт словарь паттернов. Дальше курс показывает инструменты, которые их реализуют:
- Хук use() — Читает промис прямо в рендере и приостанавливает компонент - механическая основа render-as-you-fetch
- Server Components — Перенос загрузки на сервер убирает клиентские waterfall'ы и раунд-трипы к API
- Suspense и lazy — Suspense - механизм отображения loading-состояний для приостановленных компонентов
Итог
- Request waterfall - это последовательные запросы, где каждый ждёт предыдущего; чаще всего он рождается из загрузки в useEffect внутри вложенных компонентов
- Параллельная загрузка через Promise.all или одновременный старт независимых запросов сокращает суммарное ожидание до времени самого медленного из них
- Render-as-you-fetch стартует запрос до или одновременно с рендером, а не после; fetch-on-render - его медленная противоположность
- Выбор 'сервер или клиент': данные для первого экрана и приватные источники лучше грузить на сервере, интерактивные догрузки - на клиенте
- Кэширование и продуманные loading/error состояния обязательны: дедупликация запросов, инвалидация и явная обработка ошибок отличают черновик от продакшена
Связанные уроки
- rc-29-use-hook — Поняв паттерны загрузки, разбираем хук use(), который читает промис прямо в рендере и встраивается в render-as-you-fetch
- rc-30-rsc-intro — Server Components переносят выбор 'где грузить' на сервер по умолчанию, убирая часть waterfall'ов целиком
- rc-11-useeffect — Классический способ загрузки в useEffect - отправная точка, от которой урок отталкивается к более точным паттернам