Зачем вообще нужен кэш
Представьте: пользователь открывает страницу товара. Ваш сервер должен сходить в базу, собрать HTML, отдать его - и так при каждом запросе. Если одновременно 1000 человек открывают одну и ту же страницу - сервер делает это 1000 раз.
Кэш решает эту проблему. Сервер собирает страницу один раз, сохраняет результат, и следующие 999 человек получают уже готовый ответ - быстро и без нагрузки на сервер.
Но в Next.js + Cloudflare кэш работает на нескольких уровнях одновременно, и если не разобраться как они взаимодействуют - получите проблемы. Я получил. Рассказываю что случилось и как починил.
Уровни кэширования
В моём стеке кэш живёт в трёх местах:
Браузер пользователя - файлы сохраняются прямо на компьютере или телефоне. Самый быстрый, но вы не можете его сбросить. Если пользователь закэшировал что-то - он будет получать это до истечения срока.
Cloudflare Edge - серверы Cloudflare по всему миру. Когда пользователь из Алматы открывает сайт - Cloudflare отдаёт ответ с ближайшего сервера, не гоняя запрос до вашего origin. Этот кэш вы можете сбросить через API.
Next.js на сервере - unstable_cache, ISR, встроенный кэш для fetch. Работает внутри вашего приложения до того как ответ дойдёт до Cloudflare.
Проблемы начинаются когда эти уровни конфликтуют. Например: Next.js говорит "кэшируй 1 год", Cloudflare говорит "нет, я буду кэшировать 4 часа", а браузер вообще делает что хочет.
Первая проблема: шрифты и иконки кэшировались на 4 часа
Я добавил в next.config.js такое правило:
{
source: '/:path*.(svg|ttf|woff|woff2)',
headers: [{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable' // 1 год
}],
}Lighthouse всё равно показывал 4 часа на шрифты и SVG иконки. Почему?
Оказывается, headers() в next.config.js работает только для роутов Next.js - то есть для страниц и API. Файлы из папки /public (шрифты, иконки, картинки) отдаются напрямую Node.js сервером, который про ваши правила ничего не знает. И он ставит дефолтные 4 часа.
Решение - настроить кэш для этих файлов прямо в Cloudflare через Cache Rules.
Как настроить Cache Rules в Cloudflare
Заходим: Cloudflare Dashboard → ваш домен → Caching → Cache Rules → Create rule.
Правило для Next.js чанков:
Expression: (starts_with(http.request.uri.path, "/_next/static/"))
Edge TTL: Ignore cache-control header → 1 year
Browser TTL: Override → 1 year
Почему можно смело ставить год? Потому что Next.js добавляет хэш в имя каждого файла: page-a1b2c3.js. При следующем деплое имя изменится на page-x9y8z7.js. Браузер никогда не запросит старый файл - он просто о нём не узнает.
Правило для публичной статики:
Expression:
(starts_with(http.request.uri.path, "/fonts/")) or
(ends_with(http.request.uri.path, ".svg")) or
(ends_with(http.request.uri.path, ".png")) or
(ends_with(http.request.uri.path, ".ico")) or
(ends_with(http.request.uri.path, ".webp"))
Edge TTL: Ignore cache-control header → 1 year
Browser TTL: Override → 1 year
Lighthouse перестал жаловаться на шрифты и иконки.
Browser TTL vs Edge TTL - в чём разница
Это два разных заголовка для двух разных получателей.
Edge TTL - говорит Cloudflare как долго держать файл на своих серверах. Если поставить 1 год - Cloudflare будет отдавать файл из кэша год, не обращаясь к вашему серверу.
Browser TTL - говорит браузеру пользователя как долго хранить файл локально. Если поставить 1 год - браузер не будет делать запрос к серверу вообще, даже к Cloudflare.
Важный момент: Cloudflare cache purge не очищает браузерный кэш. Если вы сбросили кэш на Cloudflare - пользователи со старым браузерным кэшем всё равно будут видеть старую версию до истечения Browser TTL.
Поэтому для HTML страниц лучше не ставить большой Browser TTL - иначе после деплоя пользователи будут получать устаревшие страницы и вы ничего не сможете с этим сделать.
Главная ловушка: Chunk Mismatch
Это самая неприятная проблема, с которой я столкнулся после подключения Cloudflare.
Что такое чанки
Next.js разбивает ваш JavaScript на маленькие файлы - чанки. Каждая страница имеет свой чанк: app/products/page-a1b2c3.js. При новом деплое хэш меняется: app/products/page-x9y8z7.js.
Как появляется проблема
До Cloudflare всё работало нормально: каждый запрос HTML страницы шёл напрямую на сервер, HTML всегда был свежим и содержал актуальные ссылки на чанки.
После подключения Cloudflare HTML страницы начали кэшироваться на edge. И вот что происходит:
- Пользователь открыл сайт → Cloudflare закэшировал HTML со ссылками на чанки билда №1
- Вы задеплоили обновление → на сервере теперь чанки билда №2
- Другой пользователь открывает сайт → получает старый HTML из кэша Cloudflare
- Браузер пытается загрузить
page-a1b2c3.js→ сервер отвечает 404, этого файла уже нет - Страница ломается с ошибкой "Loading chunk failed"
Loading chunk 9250 failed.
https://daribar.kz/_next/static/chunks/app/page-f232cc789c4e7096.js
Как это починить
Способ 1 - Purge кэша после каждого деплоя
Добавьте в CI/CD пайплайн шаг после деплоя:
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'zone_id и токен найдёте в Cloudflare Dashboard. Токену нужно дать право Cache Purge для вашей зоны.
Способ 2 - Error boundary для страховки
Даже с purge бывают случаи когда пользователь держал вкладку открытой во время деплоя. Добавьте обработчик ошибки в app/error.tsx:
'use client';
import { useEffect } from 'react';
export default function Error({ error }: { error: Error }) {
useEffect(() => {
if (error.message?.includes('Loading chunk')) {
// Просто перезагружаем страницу - пользователь получит свежий HTML
window.location.reload();
}
}, [error]);
return <div>Загружаем страницу...</div>;
}Пользователь увидит секундное мигание и страница загрузится нормально.
Способ 3 (лучший) - ISR вместо SSR
ISR (Incremental Static Regeneration) - это когда Next.js сам кэширует HTML на уровне сервера и перегенерирует его в фоне по расписанию. Тогда origin всегда отвечает быстро, а для HTML можно поставить Bypass в Cloudflare - chunk mismatch исчезает полностью.
// app/products/[id]/page.tsx
export const revalidate = 3600; // перегенерировать раз в часНо есть ограничение: если в компоненте используется cookies(), headers() или другие динамические API - ISR не включится, страница будет рендериться при каждом запросе. В таком случае перенесите чтение cookies глубже или на клиент.
Не кэшируйте 404
Ещё одна мелкая но важная деталь. Cloudflare может закэшировать 404 ответ - например на несуществующий старый чанк. Тогда пользователи продолжат получать 404 из кэша.
В Cache Rule для /_next/static/ добавьте Status Code TTL:
- Status code: 404 → Duration: No store
Итого: что и как кэшировать
| Что | Edge TTL | Browser TTL | Норм / не норм |
|---|---|---|---|
/_next/static/* | 1 год | 1 год | Хэши меняются при деплое - безопасно |
| Шрифты, SVG, PNG | 1 год | 1 год | Меняются редко, имена стабильные |
| HTML страниц | Bypass или 5 мин | Не кэшировать | Иначе chunk mismatch |
| Личные страницы (корзина, заказы) | Bypass | Bypass | Персональные данные |
Главное что нужно запомнить
headers() в next.config.js не работает для файлов из /public - используйте Cloudflare Cache Rules.
Хэши в именах чанков спасают только если HTML свежий. Старый закэшированный HTML + новый деплой = сломанные страницы.
Browser TTL нельзя сбросить удалённо - не ставьте большие значения для HTML.
Purge everything после каждого деплоя - обязательный шаг если кэшируете HTML на Cloudflare.
ISR - лучшее решение для страниц с относительно стабильным контентом. Меньше проблем с инвалидацией и быстрый origin.