Каждый фронтендер мечтает о мгновенной навигации в веб-приложении: кликаешь по карточке — и страница открывается без малейшей задержки, словно нативное мобильное приложение. Но когда ты пытаешься реализовать это в лоб через агрессивный prefetching, инфраструктура быстро преподносит неприятный сюрприз.
Инженер Аврора Шарфф из команды генеративного сервиса v0 (Vercel) поделилась поучительным кейсом оптимизации интерфейса при переходе на Next.js 16.4. Их опыт наглядно показывает, где пролегает тонкая грань между идеальным пользовательским опытом и перегрузкой бэкенда.
Ловушка бездумного prefetch в плотных сетках
В сервисе v0 есть популярный раздел /templates — плотная сетка сотен готовых шаблонов интерфейсов. Разработчики включили атрибут prefetch={true} на карточках ссылок, рассчитывая, что браузер заранее загрузит страницы в фоновом режиме.
Результат оказался обескураживающим:
- Количество фоновых HTTP-запросов к серверу подскочило в 2.9 раза;
- Серверы захлебывались от повторного рендеринга динамических персонализированных блоков;
- Если же предзагрузку отключали вовсе, пользователи при клике упирались в мерцающие скелетоны (skeleton screens), ждущие ответа бэкенда.
Решение пришло с релизом Next.js 16.4, где модель частичной предзагрузки (Partial Prefetching) стала работать в неразрывной связке со стандартными Cache Components — архитектурный сдвиг этой версии мы анализировали в статье Next.js 16.4: модель Cache Components стала стандартом и ускоряет SSR по умолчанию.
Двухфазная модель: статическая оболочка и потоковые данные
Суть Partial Prefetching заключается в четком разделении контента на две независимые фазы:
- Фаза предзагрузки (Prefetch Phase): когда ссылка попадает в зону видимости или под курсор мыши, браузер скачивает только статическую оболочку разметки (Layout Shell), общие навигационные панели и кэшируемые сегменты. Это микроскопический объем данных, который сервер отдает мгновенно из краевого кэша.
- Фаза динамического стриминга (Navigation Phase): персонализированные данные (статус пользователя, избранное, свежие комментарии) запрашиваются потоково только после реального клика по ссылке. Каркас страницы уже нарисован, а динамические слоты заполняются по технологии React Suspense без задержки отрисовки первого экрана.
Активация в next.config.ts:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
Для проверки того, что переход действительно происходит без задержек и мерцания заглушек, команда написала Playwright-тест с утилитой instant():
import { test, expect } from '@playwright/test';
import { instant } from '@vercel/next-testing-helpers';
test('Мгновенная навигация в карточку шаблона без скелетона', async ({ page }) => {
await page.goto('/templates');
const card = page.getByTestId('template-card-link').first();
await card.hover();
await instant(page, async () => {
await card.click();
await page.waitForURL('**/templates/*');
// Проверяем, что промежуточный скелетон даже не появлялся в DOM
await expect(page.getByTestId('template-detail-skeleton')).toHaveCount(0);
await expect(page.getByTestId('template-detail-header')).toBeVisible();
});
});
А в компонентах ссылок предзагрузка была оптимизирована с явным указанием триггеров:
import Link from 'next/link';
export function TemplateCard({ id, title }: { id: string; title: string }) {
return (
<Link href={"/templates/" + id} prefetch={true} data-testid="template-card-link">
<h3>{title}</h3>
</Link>
);
}
В плотных мобильных списках не включайте prefetch для всех видимых элементов экрана одновременно. Лучше активировать предзагрузку по событию touchstart с небольшой задержкой (дебаунс 80 мс), отсекая случайные свайпы при прокрутке.
Опыт v0 доказывает: идеальная отзывчивость веб-приложений достигается не грубой силой серверов, а грамотным разделением статических и динамических границ рендеринга.
