Производительность фронтенда может быть сложной для оптимизации. Даже в хорошо оптимизированных приложениях наиболее распространенной проблемой являются водопады запросов между клиентом и сервером. При разработке Next.js App Router мы хотели решить эту проблему. Для этого нам нужно было перенести клиент-серверные REST-запросы на сервер с использованием React Server Components в одном цикле взаимодействия. Это означало, что сервер иногда должен быть динамическим, жертвуя отличной начальной производительностью загрузки Jamstack. Мы создали частичный предварительный рендеринг (partial prerendering), чтобы решить этот компромисс и получить лучшее из обоих миров.
Однако по пути страдал опыт разработчика из-за настроек кэширования по умолчанию и предоставленного контроля. По умолчанию fetch()
стал кэшироваться для повышения производительности, но это создало проблемы для быстрого прототипирования и высокодинамичных приложений. Мы не предоставили достаточного контроля для доступа к локальным базам данных, которые не использовали fetch()
. У нас был unstable_cache()
, но он не был эргономичным. Это привело к необходимости сегментных конфигураций, таких как export const dynamic, runtime, fetchCache, dynamicParams, revalidate = ...
, в качестве аварийного выхода.
Конечно, мы продолжим поддерживать это для обратной совместимости. Но на мгновение забудьте обо всём этом. У нас есть идея для чего-то более простого.
Мы работаем над новым экспериментальным режимом, основанным всего на двух концепциях: <Suspense>
и use cache
.
Выберите свой путь
Первое, что вы заметите — при добавлении данных в компоненты теперь будет возникать ошибка.
async function Component() {
return fetch(...) // ошибка
}
export default async function Page() {
return <Component />
}
Для использования данных, cookies, заголовков, текущего времени или случайных значений теперь есть выбор: хотите ли вы кэшировать данные (на сервере или клиенте) или выполнять их при каждом запросе? Я использую fetch()
в качестве примера, но это применимо к любым асинхронным Node API, таким как базы данных или таймеры.
Динамический режим
Если вы всё ещё итерируете или создаёте высокодинамичную панель управления, можно обернуть компонент в границу <Suspense>
. <Suspense>
включает динамическую загрузку данных и потоковую передачу.
async function Component() {
return fetch(...) // нет ошибки
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
Это можно сделать и в корневом layout, или использовать loading.tsx
.
Это гарантирует, что оболочка приложения остаётся мгновенной. Вы можете продолжать добавлять данные в свою страницу, зная, что по умолчанию всё будет динамическим. Ничего не кэшируется по умолчанию. Больше никаких скрытых кэшей.
Статический режим
Если вы создаёте что-то статическое и не хотите использовать динамические функции, можно использовать новую директиву use cache
.
"use cache"
export default async function Page() {
return fetch(...) // нет ошибки
}
Пометка страницы use cache
указывает, что весь сегмент должен кэшироваться. Это означает, что любые данные, которые вы получаете, теперь могут быть закэшированы, позволяя странице рендериться статически. Для статического контента не используется граница <Suspense>
. Вы можете добавлять больше данных на страницу, и всё будет кэшироваться.
Смешанный режим
Можно также комбинировать подходы. Например, добавить use cache
в корневой layout, чтобы гарантировать его кэширование. Каждый layout или страница могут кэшироваться независимо.
"use cache"
export default async function Layout({ children }) {
const response = await fetch(...)
const data = await response.json()
return <html>
<body>
<div>{data.notice}</div>
{children}
</body>
</html>
}
При этом используя динамические данные внутри конкретной страницы:
import { Suspense } from 'react'
async function Component() {
return fetch(...) // нет ошибки
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
Кэшированные функции
При использовании гибридного подхода может быть удобнее добавлять кэширование ближе к вызовам API.
Можно добавить use cache
к любой асинхронной функции, как и use server
. Представьте это как Server Action, но вместо вызова сервера вы вызываете кэш. Поддерживаются те же богатые типы аргументов и возвращаемых значений, не только JSON. Ключ кэша автоматически включает любые аргументы и замыкания, поэтому не нужно указывать его вручную.
async function getNotice() {
"use cache"
const response = await fetch(...)
const data = await response.json()
return data.notice;
}
export default async function Layout({ children }) {
return <html>
<body>
<h1>{await getNotice()}</h1>
{children}
</body>
</html>
}
Поскольку в этом layout не использовались другие данные, он может оставаться статическим. Преимущество такого подхода в том, что если вы случайно добавите новые динамические данные в layout, это вызовет ошибку при сборке, заставляя сделать новый выбор. Если добавить use cache
ко всему layout, он будет закэширован без ошибки. Какой подход выбрать — зависит от вашего случая использования.
Тегирование кэша
Если нужно явно очистить запись кэша по тегу, можно использовать новый API cacheTag()
внутри функции с use cache
.
import { cacheTag } from 'next/cache';
async function getNotice() {
'use cache';
cacheTag('my-tag');
}
Затем просто вызовите revalidateTag('my-tag')
из Server Action, как и раньше.
Поскольку этот API можно вызывать после загрузки данных, теперь можно использовать данные для тегирования записей кэша.
import { unstable_cacheTag as cacheTag } from 'next/cache';
async function getBlogPosts(page) {
'use cache';
const posts = await fetchPosts(page);
for (let post of posts) {
cacheTag('blog-post-' + post.id);
}
return posts;
}
Определение времени жизни кэша
Чтобы контролировать, как долго конкретная запись или страница должна храниться в кэше, можно использовать API cacheLife()
:
"use cache"
import { unstable_cacheLife as cacheLife } from 'next/cache'
export default async function Page() {
cacheLife("minutes")
return ...
}
По умолчанию принимаются следующие значения:
"seconds"
(секунды)"minutes"
(минуты)"hours"
(часы)"days"
(дни)"weeks"
(недели)"max"
(максимально)
Выберите примерный диапазон, который лучше всего подходит для вашего случая. Не нужно указывать точное число и вычислять, сколько секунд (или это миллисекунд?) в неделе. Однако можно также указать конкретные значения или настроить собственные именованные профили кэша.
В дополнение к revalidate
, этот API может контролировать время устаревания (stale
) клиентского кэша, а также срок действия (expire
), который определяет, когда страница должна перестать быть актуальной, если долго не было трафика.
Экспериментальный режим
Это всё ещё экспериментальный проект. Он не готов для продакшена, в нём есть недостающие функции и баги. В частности, нам нужно улучшить стек ошибок для этого нового типа ошибок. Однако, если вы чувствуете себя авантюрно, мы будем рады ранним отзывам.
Мы опубликуем более подробный путь обновления. Помимо ранних ошибок, основное критическое изменение здесь — отмена кэширования по умолчанию для fetch()
. Тем не менее, на этой ранней экспериментальной стадии мы рекомендуем экспериментировать только с новыми проектами. Если всё пойдёт хорошо, мы надеемся выпустить опциональную версию в минорном релизе и сделать её по умолчанию в будущем мажорном.
Чтобы попробовать, необходимо использовать canary
-версию Next.js:
npx create-next-app@canary
Также нужно включить экспериментальный флаг dynamicIO в next.config.ts
:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
}
};
export default nextConfig;
Подробнее о use cache
, cacheLife
и cacheTag
читайте в нашей документации.