Prefetching данных

Prefetching — механизм предварительной загрузки данных в кеш до момента, когда компоненту реально понадобятся эти данные. Главная цель — сократить задержки интерфейса и убрать эффект «пустого состояния» при переходе между страницами, открытии модальных окон, вкладок и сложных интерфейсных блоков.

Вместо ожидания загрузки после рендера компонента приложение может заранее поместить данные в кеш. Когда компонент вызовет useQuery, данные уже будут находиться в памяти QueryClient.

Наиболее частые сценарии:

  • предварительная загрузка страницы пагинации;
  • hover-навигация;
  • preload роутов;
  • загрузка данных модального окна;
  • прогрев кеша после авторизации;
  • SSR и hydration;
  • бесшовные переходы между страницами.

Основной принцип работы

TanStack Query хранит данные внутри QueryCache. Prefetching выполняет запрос заранее и записывает результат в кеш под указанным queryKey.

Когда позже выполняется:

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
})

библиотека сначала проверяет наличие данных в кеше.

Если данные уже существуют и не считаются устаревшими:

  • повторный запрос не выполняется;
  • интерфейс получает данные мгновенно;
  • состояние isLoading может вообще не появиться.

Метод prefetchQuery

Основной API для prefetching:

queryClient.prefetchQuery()

Пример:

import { useQueryClient } from '@tanstack/react-query'

function App() {
    const queryClient = useQueryClient()

    const prefetchPosts = async () => {
        await queryClient.prefetchQuery({
            queryKey: ['posts'],
            queryFn: fetchPosts
        })
    }

    return (
        <button onMouseEn ter={prefetchPosts}>
            Открыть статьи
        </button>
    )
}

После наведения курсора данные загружаются в кеш.

При переходе на страницу:

const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
})

данные уже будут доступны.


Отличие prefetchQuery от fetchQuery

Многие разработчики путают эти методы.

prefetchQuery

await queryClient.prefetchQuery(...)

Особенности:

  • загружает данные в кеш;
  • не возвращает результат;
  • ошибки не пробрасываются в UI автоматически;
  • используется именно для прогрева кеша.

fetchQuery

const data = await queryClient.fetchQuery(...)

Особенности:

  • выполняет запрос;
  • возвращает данные;
  • используется как полноценный запрос вне React-компонентов;
  • чаще применяется в loaders, SSR и серверной логике.

Prefetching и staleTime

Предварительно загруженные данные всё равно подчиняются правилам устаревания.

Пример:

await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 1000 * 60
})

Если в течение минуты вызвать useQuery с тем же queryKey, новый запрос не будет выполнен.

Без staleTime данные почти сразу считаются устаревшими, поэтому useQuery может инициировать повторный refetch.


Prefetching при наведении курсора

Один из самых популярных паттернов.

function PostLink({ id }) {
    const queryClient = useQueryClient()

    const prefetchPost = () => {
        queryClient.prefetchQuery({
            queryKey: ['post', id],
            queryFn: () => fetchPost(id),
            staleTime: 1000 * 60 * 5
        })
    }

    return (
        <Link
            to={`/posts/${id}`}
            onMouseEn ter={prefetchPost}
        >
            Статья
        </Link>
    )
}

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

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

  • переход становится мгновенным;
  • пропадает loading spinner;
  • улучшается perceived performance.

Prefetching страниц пагинации

Очень важный сценарий для UX.

Загрузка следующей страницы заранее

const { data } = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page)
})

После получения текущей страницы можно прогреть следующую:

useEffect(() => {
    if (data?.hasMore) {
        queryClient.prefetchQuery({
            queryKey: ['posts', page + 1],
            queryFn: () => fetchPosts(page + 1)
        })
    }
}, [data, page, queryClient])

Когда пользователь нажимает «Следующая страница», данные уже находятся в кеше.


Prefetching модальных окон

Частая проблема интерфейсов — задержка при открытии модалки.

Prefetching решает проблему заранее.

function UserRow({ user }) {
    const queryClient = useQueryClient()

    const prefetchUser = () => {
        queryClient.prefetchQuery({
            queryKey: ['user', user.id],
            queryFn: () => fetchUser(user.id)
        })
    }

    return (
        <div
            onMouseEn ter={prefetchUser}
        >
            {user.name}
        </div>
    )
}

После открытия модального окна:

const { data } = useQuery({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id)
})

информация отображается мгновенно.


Prefetching при роутинге

Современные SPA часто загружают данные во время перехода между страницами.

Пример с роутером:

const openProfile = async (id) => {
    await queryClient.prefetchQuery({
        queryKey: ['profile', id],
        queryFn: () => fetchProfile(id)
    })

    navigate(`/profile/${id}`)
}

Маршрут открывается уже с готовыми данными.


Prefetching и React Router loaders

TanStack Query хорошо интегрируется с loaders.

export const profileLoader =
    (queryClient) =>
    async ({ params }) => {

        await queryClient.prefetchQuery({
            queryKey: ['profile', params.id],
            queryFn: () => fetchProfile(params.id)
        })

        return null
    }

Компонент:

function ProfilePage() {
    const { data } = useQuery({
        queryKey: ['profile'],
        queryFn: fetchProfile
    })

    return <div>{data.name}</div>
}

Данные уже готовы к моменту рендера.


PrefetchInfiniteQuery

Для infinite-запросов используется отдельный API.

queryClient.prefetchInfiniteQuery()

Пример:

await queryClient.prefetchInfiniteQuery({
    queryKey: ['feed'],
    queryFn: fetchFeed,
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor
})

Такой prefetching полезен для:

  • infinite scroll;
  • лент новостей;
  • чатов;
  • виртуализированных списков.

Prefetching и SSR

На сервере prefetching особенно важен.

Типичный поток:

  1. сервер выполняет prefetch;
  2. данные попадают в кеш;
  3. кеш сериализуется;
  4. клиент получает hydrated state;
  5. повторного запроса не происходит.

Пример:

await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
})

После этого:

const dehydratedState = dehydrate(queryClient)

На клиенте:

<Hydrate state={dehydratedState}>
    <App />
</Hydrate>

Это основа SSR в TanStack Query.


Prefetching и hydration

Hydration восстанавливает кеш, созданный на сервере.

Prefetching здесь играет ключевую роль:

  • сервер заранее загружает данные;
  • клиент получает уже заполненный кеш;
  • интерфейс появляется сразу с контентом.

Без hydration браузер выполнил бы запрос повторно.


Prefetching и cache warming

Иногда prefetching называют cache warming — прогрев кеша.

Пример после авторизации:

await Promise.all([
    queryClient.prefetchQuery({
        queryKey: ['profile'],
        queryFn: fetchProfile
    }),

    queryClient.prefetchQuery({
        queryKey: ['notifications'],
        queryFn: fetchNotifications
    }),

    queryClient.prefetchQuery({
        queryKey: ['messages'],
        queryFn: fetchMessages
    })
])

После входа интерфейс становится значительно быстрее.


Предзагрузка нескольких запросов

TanStack Query не ограничивает количество prefetch-запросов.

Пример:

await Promise.all([
    queryClient.prefetchQuery({
        queryKey: ['posts'],
        queryFn: fetchPosts
    }),

    queryClient.prefetchQuery({
        queryKey: ['users'],
        queryFn: fetchUsers
    }),

    queryClient.prefetchQuery({
        queryKey: ['settings'],
        queryFn: fetchSettings
    })
])

Такой подход полезен для dashboard-интерфейсов.


Prefetching и initialData

Важно понимать разницу.

initialData

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    initialData: []
})
  • данные задаются вручную;
  • в кеше нет настоящего запроса;
  • источник данных — локальный код.

Prefetching

await queryClient.prefetchQuery(...)
  • выполняется реальный HTTP-запрос;
  • данные попадают в кеш;
  • полностью работает lifecycle query.

Prefetching и placeholderData

placeholderData создаёт временное отображение до прихода настоящих данных.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    placeholderData: []
})

Prefetching отличается тем, что реальные данные уже существуют к моменту рендера.


Ошибки при prefetching

Ошибки не отображаются автоматически в интерфейсе.

Пример:

await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
})

Если запрос завершится неудачно:

  • данные не попадут в кеш;
  • useQuery позже выполнит обычный запрос;
  • ошибка не вызовет UI state сама по себе.

Для обработки:

try {
    await queryClient.prefetchQuery({
        queryKey: ['posts'],
        queryFn: fetchPosts
    })
} catch (error) {
    console.error(error)
}

Влияние на производительность

Prefetching способен радикально улучшить UX, но при неправильном использовании создаёт проблемы:

  • лишний сетевой трафик;
  • ненужная нагрузка на API;
  • рост потребления памяти;
  • прогрев данных, которые пользователь никогда не откроет.

Когда prefetching особенно полезен

Навигация между страницами

Переходы становятся мгновенными.

Админ-панели

Множество параллельных данных можно загрузить заранее.

Таблицы и пагинация

Следующая страница уже находится в кеше.

Медленные API

Prefetching скрывает задержки сети.

SSR-приложения

Позволяет избежать двойных запросов.


Когда prefetching вреден

Большие объёмы данных

Не стоит заранее загружать мегабайты информации.

Низкая вероятность перехода

Если пользователь редко открывает раздел, prefetching только расходует ресурсы.

Ограниченные мобильные сети

Предварительные запросы могут ухудшить UX.

Часто изменяющиеся данные

Если данные устаревают за секунды, prefetching теряет смысл.


Prefetching и gcTime

После prefetch данные остаются в кеше до garbage collection.

Пример:

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            gcTime: 1000 * 60 * 10
        }
    }
})

Через 10 минут неиспользуемый кеш будет удалён.


Prefetching и invalidateQueries

После инвалидирования prefetch-данные могут стать устаревшими.

queryClient.invalidateQueries({
    queryKey: ['posts']
})

Следующий useQuery выполнит refetch.


Prefetching и background refetch

Даже если данные были prefetched, TanStack Query может обновить их в фоне.

Например:

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    refetchOnWindowFocus: true
})

После возврата во вкладку произойдёт background refetch.


Архитектурные рекомендации

Prefetch только вероятных переходов

Наиболее эффективен prefetch с высокой вероятностью использования.

Использование staleTime

Без staleTime prefetch часто теряет смысл из-за мгновенного refetch.

Избегание агрессивного прогрева

Не стоит prefetch-загружать всё приложение сразу.

Prefetch рядом с UX-событиями

Лучшие триггеры:

  • hover;
  • focus;
  • preload route;
  • nearing viewport;
  • подготовка навигации.

Типичный production-паттерн

function ProductCard({ id }) {
    const queryClient = useQueryClient()

    const prefetchProduct = () => {
        queryClient.prefetchQuery({
            queryKey: ['product', id],
            queryFn: () => fetchProduct(id),
            staleTime: 1000 * 60 * 5
        })
    }

    return (
        <Link
            to={`/products/${id}`}
            onMouseEn ter={prefetchProduct}
        >
            Открыть товар
        </Link>
    )
}

После открытия страницы:

function ProductPage({ id }) {
    const { data } = useQuery({
        queryKey: ['product', id],
        queryFn: () => fetchProduct(id)
    })

    return <div>{data.title}</div>
}

Интерфейс получает данные практически мгновенно благодаря заранее прогретому кешу.