awesome-everything EN
↑ Обратно к восхождению

Архитектура фронтенда

Fetch waterfall''''ы — диагностика и лечение через Promise.all

Суть Почему последовательные await''''ы делают суммарную задержку равной сумме всех fetch''''ей, как увидеть waterfall в Network panel и три паттерна для их устранения.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 10 min

Страница профиля нуждается в данных пользователя (200ms), его постах (300ms) и количестве подписчиков (150ms). Ни один из них не зависит от другого. Почему же страница загружается 650ms вместо 300ms?

Что такое waterfall

Fetch waterfall — серия запросов, где каждый начинается только после завершения предыдущего. Классический React waterfall:

  1. Компонент A монтируется, вызывает fetch('/api/user') в useEffect
  2. Ответ приходит (200ms) — компонент перерисовывается, рендерит дочерний B
  3. Компонент B монтируется, вызывает fetch('/api/posts') в своём useEffect
  4. Ответ приходит (300ms) — рендерит дочерний C
  5. Компонент C вызывает fetch('/api/followers') (150ms)

Итого: 650ms. Хотя ни один из этих трёх fetch’ей не зависит от данных другого.

Waterfall vs параллельный — те же данные, разница в 2×
Последовательный (waterfall)
user 200ms
posts 300ms
followers 150ms
Итого: 650ms
Параллельный (Promise.all)
user 200ms
posts 300ms
followers 150ms
Итого: 300ms (ограничено самым медленным)

Почему образуются waterfall’ы

Корневая причина — data colocation встречается с последовательным рендерингом. React рендерит дерево компонентов сверху вниз. Каждый компонент запускает свой effect после монтирования. Дочерний компонент не может смонтироваться, пока родитель не отрендерится, а это происходит только после прихода данных родителя. Даже если данные дочернего и родительского компонентов независимы, зависимость рендеринга создаёт зависимость fetch’а.

Этот паттерн особенно опасен в глубоко вложенных деревьях: дерево из 5 уровней с одним fetch’ем на уровень добавляет 5 последовательных round-trip’ов, даже если каждый fetch логически независим.

Фикс 1: Promise.all у родителя

Самое простое лечение: поднять все независимые fetch’и к общему родителю и запустить их параллельно.

// Плохо — waterfall
const user = await fetchUser(id);
const posts = await fetchPosts(id);
const followers = await fetchFollowers(id);

// Хорошо — параллельно
const [user, posts, followers] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchFollowers(id),
]);

Суммарное время падает с суммы до максимума. С 5 независимыми fetch’ами по 200ms: 1000ms → 200ms.

Фикс 2: TanStack Query useQueries

При использовании клиентской библиотеки, useQueries запускает несколько запросов параллельно из одного вызова хука:

const results = useQueries({
  queries: [
    { queryKey: ['user', id], queryFn: () => fetchUser(id) },
    { queryKey: ['posts', id], queryFn: () => fetchPosts(id) },
    { queryKey: ['followers', id], queryFn: () => fetchFollowers(id) },
  ],
});

Все три запускаются одновременно. Каждый результат имеет свой loading/error state.

Фикс 3: Sibling RSC компоненты

В React Server Components, sibling async компоненты по умолчанию fetch’ят параллельно, потому что сервер может разрешить их одновременно:

// Оба fetch'ят одновременно на сервере
async function ProfilePage({ id }) {
  return (
    <div>
      <UserCard id={id} />   {/* fetches user */}
      <PostsList id={id} />  {/* fetches posts — параллельно с UserCard */}
    </div>
  );
}

Ключ: не await один перед рендерингом другого. Держи их как siblings, дай React’у диспетчеризировать их параллельно.

Почему это работает

Fetch waterfall структурно идентичен database N+1 запросу. Список из 10 элементов, каждый из которых требует второго fetch’а, производит 11 последовательных round-trip’ов вместо 2. Та же диагностика, то же лечение: поднять и объединить. Названия разные (N+1 vs waterfall), но проблема одна: последовательно там, где могло быть параллельно.

Стоимость waterfall'а на практике
Типичный round-trip (быстрая сеть)
50–200 ms
5 последовательных независимых fetch'ей
250–1000 ms
5 параллельных fetch'ей (Promise.all)
50–200 ms (только max)
Ускорение Promise.all при N=5
~5x
Глубина waterfall'а в больших SPA
3–8 уровней типично
Закончи аналогию

Заполни пропуск: fetch _______ происходит когда fetch B не может начаться пока fetch A не вернёт ответ, делая суммарное время A + B вместо max(A, B).

Викторина

Страница профиля имеет три useEffect fetch'а — user, posts, followers. Ни один не зависит от другого. Каково суммарное время fetch'а?

Викторина

Ты конвертируешь три последовательных server-side await'а в Promise.all. Каким становится суммарное время fetch'а?

Вспомните перед уходом
  1. 01
    Почему useEffect fetch'и на уровне компонентов образуют waterfall'ы, даже когда данные независимы?
  2. 02
    Назови три паттерна для устранения fetch waterfall'а.
  3. 03
    В чём структурная эквивалентность N+1 и waterfall?
Итог

Fetch waterfall сериализует независимые запросы через каскад рендеринга: каждый компонент монтируется только после прихода данных его родителя, поэтому суммарная задержка равна сумме всех fetch’ей в цепочке. Фикс — параллельная диспетчеризация: Promise.all у родителя для server-кода, useQueries для клиентских хуков, или sibling RSC async компоненты на сервере. Ускорение при N=5 параллельных fetch’ах vs последовательных — приблизительно 5x. Обнаружить waterfall в Chrome DevTools легко: ищи лесенку в Network panel, где каждый запрос начинается ровно тогда, когда завершился предыдущий.

Связанные уроки
встречается в178
Продолжить восхождение ↑React Server Components и Suspense streaming
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.