Dev Highlights

Доки по разработке

This project is maintained by teniryte

13. Рендер-оптимизации и Request Waterfalls

Эта глава закрывает две частые production-проблемы:

Structural sharing

По умолчанию TanStack Query старается сохранить старые ссылки там, где данные фактически не изменились.

Что это даёт:

Это работает лучше всего с JSON-compatible данными.

Tracked properties

Query result использует Proxy и ререндерит компонент только если поменялось реально прочитанное поле.

Хорошо:

const query = useQuery(todoOptions(id))

return <TodoView todo={query.data} loading={query.isFetching} />

Плохо:

const { data, ...rest } = useQuery(todoOptions(id))

Object rest destructuring ломает tracked-properties optimization.

select

select помогает подписаться только на нужный срез:

const todoCountQuery = useQuery({
  ...todosOptions(),
  select: (data) => data.length,
})

Если список изменился, но длина нет, компонент не будет ререндериться из-за остальных деталей.

Важно: стабилизируйте select, если он зависит от ссылок.

const selectVisibleTodos = useCallback(
  (data: Todo[]) => data.filter((todo) => todo.visible),
  [],
)

notifyOnChangeProps

По умолчанию tracked props уже дают хороший результат. Но иногда нужно тонко управлять:

useQuery({
  ...todosOptions(),
  notifyOnChangeProps: ['data', 'error'],
})

Используйте только если действительно понимаете, что оптимизируете.

Referential identity

Важно помнить:

Поэтому не стоит мемоизировать сам объект query, если вам важны именно данные.

Что такое request waterfall

Waterfall возникает, когда запросы стартуют не параллельно, а цепочкой:

  1. компонент смонтировался
  2. запрос A завершился
  3. только потом запрос B
  4. только потом запрос C

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

Где чаще всего возникает waterfall

Как бороться с waterfall

1. Prefetch заранее

await Promise.all([
  queryClient.ensureQueryData(userOptions(userId)),
  queryClient.ensureQueryData(statsOptions(userId)),
  queryClient.ensureQueryData(activityOptions(userId)),
])

2. Параллелить независимые запросы

Либо несколькими useQuery, либо useQueries.

3. Поднимать загрузку в роутер или серверный слой

Особенно в Next.js App Router, Remix, TanStack Router.

4. Не делать chained requests без настоящей зависимости

Если projectId и statsId уже известны, не надо ждать detail query ради их запуска.

Хорошая стратегия для экранов

Для каждой страницы задайте себе три вопроса:

  1. Какие запросы реально независимы?
  2. Что можно прогреть до первого рендера?
  3. Какие данные нужны для первого meaningful paint, а какие можно догрузить позже?

Практические правила