Хостинг во Франкфурте стоит недорого, а читатели сайта живут в Алматы, Минске и Ташкенте, и каждый сетевой круг до Европы отнимает у них 60-80 мс. Страница из сорока файлов проходит этот маршрут десятки раз, и латентность съедает внимание аудитории раньше, чем она доходит до текста. CDN с сетью edge-узлов переносит точку входа в город читателя: рукопожатие TLS проходит локально, статика отдаётся из кэша за доли миллисекунды, а до источника добираются только те запросы, которые кэшировать нельзя. На практике TTFB в 300 мс превращается в 40 мс, а полная загрузка ужимается в два-три раза. У медали есть обратная сторона: одна небрежная настройка кэша, и неделями после правок аудитория видит устаревшую главную страницу. Ниже разобраны цифры ускорения, поведение кэша по умолчанию, связки Cache Everything с TTL и приёмы, которые не дают кэшу хранить то, что ему хранить не положено.

Путь запроса через edge-узел и рукопожатие TLS в городе посетителя

Сеть Cloudflare держит несколько сотен точек присутствия в более чем ста странах, в том числе в Москве, Санкт-Петербурге, Минске, Алматы, Ташкенте, Тбилиси, Ереване, Варшаве и Франкфурте. Основа сети это anycast: один и тот же IP-адрес анонсируется из каждой точки, и протокол BGP сам приводит запрос туда, где узел ближе к посетителю. Настройки DNS и сервера менять не нужно, домен просто начинает резолвиться в сеть Cloudflare.

Сценарий без CDN выглядит так. Посетитель из Алматы открывает сайт, источник которого стоит во Франкфурте. Круговой путь занимает 70-80 мс. На установление TCP-соединения уходит один круг, на рукопожатие TLS 1.3 ещё один, итого 150-160 мс проходит до момента, когда браузер только отправит GET-запрос. Прибавляем 80 мс на передачу запроса, 150 мс на генерацию HTML и 80 мс на дорогу обратно, и TTFB дотягивает до 460 мс.

Через edge-узел в Алматы арифметика другая. До узла 2-4 мс, TCP и TLS вместе укладываются в 10 мс, а соединение между узлом и источником тёплое и переиспользуется запросами разных посетителей. Закэшированный объект собирается прямо на узле, и TTFB падает до 30-50 мс. Динамический HTML всё равно едет до источника, но узел экономит хендшейк и держит постоянный канал, отыгрывая 100-150 мс.

Сверх задержки узел терминирует HTTP/2 и HTTP/3: браузер гонит десятки файлов параллельно по одному соединению, повторные визиты по QUIC проходят в режиме 0-RTT без хендшейка, а сжатие Brotli выполняется на узле, и источник не тратит процессор на каждый ответ.

Реальная экономия миллисекунд для посетителей из разных городов

Типовой русскоязычный контентный сайт устроен так. Страница весит 1,8 МБ, в ней 42 статических файла на 1,65 МБ и один HTML, который источник генерирует за 150 мс. Узлы отдают статику из кэша, поэтому итог складывается из двух частей: сетевого выигрыша на TLS и полного выигрыша на закэшированных объектах.

Числовые сценарии для одного и того же сайта выглядят так. Посетитель из Новосибирска до включения CDN получает TTFB 380 мс и полную загрузку 4,3 секунды, после включения 45 мс и 1,6 секунды. Минск даёт 290 мс и 3,7 секунды против 40 мс и 1,4 секунды. Алматы показывает 340 мс и 4,0 секунды против 50 мс и 1,5 секунды. Ташкент: 420 мс и 4,6 секунды против 65 мс и 1,8 секунды. Владивосток, где один круг до Европы доходит до 180 мс, показывает самый драматичный разрыв: 4,8 секунды против 1,9, потому что каждый из 42 файлов раньше ехал через полматерика.

Динамический HTML остаётся честным: он по-прежнему едет до источника и генерируется там же, экономия на нём ограничена хендшейком и тёплым соединением, обычно 100-150 мс. Поэтому обещание ускорить всё в пять раз выполняется только для закэшированной доли страницы, а у контентного сайта эта доля доходит до 90% байтов. Отсюда практический вывод: чем выше доля статики и чем лучше hit ratio, тем ближе результат к верхней границе.

Что попадает в кэш по умолчанию и как на это влияют заголовки источника

По умолчанию кэшируются только файлы со статическими расширениями: css, js, jpg, jpeg, png, gif, webp, svg, ico, woff, woff2, ttf, otf, eot, mp4 и ряд других из внутреннего списка. Правило работает по расширению в URL, и для этих файлов зона закэширует ответ, даже если источник не прислал ни одного заголовка кэширования. HTML, JSON, XML и ответы API по умолчанию не кэшируются: каждый запрос к ним уходит до источника.

Заголовки источника влияют так. Cache-Control со значениями no-store, private или no-cache отключает кэширование даже для статических расширений. Max-age учитывается и задаёт срок жизни объекта на узле. Если источник промолчал, зона подставляет собственные значения: Edge Cache TTL 2 часа на узле и Browser Cache TTL 4 часа в браузере, если в панели не выбран режим уважения заголовков источника.

Проверить фактическое поведение можно без панели: открыть инструменты разработчика и найти заголовок cf-cache-status в ответе. Значение HIT говорит, что объект отдан из кэша узла. MISS показывает, что объекта не было и он поехал до источника. DYNAMIC сообщает, что зона не считает объект кэшируемым. BYPASS сигнализирует о сработавшем правиле обхода. EXPIRED говорит об устаревшей копии, которая обновляется.

Флаг Cache Everything и настройки Edge Cache TTL и Browser Cache TTL

Флаг Cache Everything расширяет кэш на HTML и остальные ответы, которые по умолчанию считаются динамическими. Включается он либо в старом интерфейсе Page Rules, либо в актуальных Cache Rules. Пример выражения для блога:

(http.host eq "www.example.com" and starts_with(http.request.uri.path, "/blog/"))

Для этого выражения включаются параметры Eligible for cache и Edge Cache TTL 1 час в режиме Ignore origin. Page Rule выглядит компактнее: адрес example.com/blog/* плюс Cache Everything и Edge Cache TTL 1 hour. Через час копия устареет, запрос уйдёт до источника, и посетитель получит обновлённый HTML.

Edge Cache TTL задаёт срок жизни объекта на узле: в режиме Ignore origin он перекрывает max-age источника, в режиме Respect origin подчиняется ему. Browser Cache TTL регулирует директиву для браузера и при числовом значении перекрывает то, что прислал источник. Для HTML числовой Browser Cache TTL опасен: поставив сутки, администратор отправляет страницу в кэш браузера, откуда её не достанет ни один сброс кэша на узле. Для статики числовое значение наоборот удобно: файлы меняются редко, и 30 дней Browser Cache TTL превращают повторные визиты в почти мгновенные.

Устаревший HTML в кэше после правок сайта и грамотный сброс

Классический инцидент. Редактор исправил опечатку в статье, а посетители ещё сутки видят старый текст. Причина в связке Cache Everything с Edge Cache TTL 4 часа и Browser Cache TTL, перекрывшего max-age в браузере. Первая половина проблемы лечится сбросом на узле, вторая требует правильных заголовков.

Сброс на узле бывает трёх видов. Purge Everything стирает весь кэш зоны мгновенно, но hit ratio падает до нуля, и источник в первые минуты ловит лавину промахов, что на слабом хостинге заметнее самой правки. Purge by URL точечно чистит до 30 адресов за один запрос и доступен на всех тарифах, включая бесплатный. Purge by prefix и purge by tag, где одна метка снимает сотни связанных объектов, остаются за старшим тарифом. Плагины кэширования для WordPress умеют дёргать API сами при публикации записи, и после каждого обновления статьи чистится только её адрес.

Браузерный кэш через панель не чистится, и это главный скрытый подвох. Сброс на узле выполнен, а часть аудитории сидит на копии в браузере, потому что ответ ушёл туда с max-age на сутки. Рецепт такой: для HTML держать Browser Cache TTL в режиме Respect Existing Headers, а источнику отдавать короткий Cache-Control. Хорошо работает связка:

Cache-Control: max-age=60, stale-while-revalidate=300

Директива stale-while-revalidate знакома узлам Cloudflare. Первый запрос после истечения max-age получает мгновенно ещё старую по содержанию копию и не ждёт источника, обновление идёт в фоне, и следующие посетители видят уже новый текст. Страница не висит часами устаревшей и не тормозит на каждом MISS, компромисс между свежестью и скоростью получается рабочим.

Кэш-ключ, query string и обход кэша для динамических страниц

Ключ кэша по умолчанию собирается из полного URL со всеми параметрами. Страница /blog/seo/ без параметров и та же страница с ?utm_source=newsletter лежат в кэше как два разных объекта. Добавятся utm_medium, utm_campaign и gclid, и на узле вырастет свалка копий, из которых рабочая одна. Hit ratio ползёт вниз, экономия тает. Лечится в Cache Rules вычёркиванием рекламных параметров из ключа, при условии что значимые для содержания параметры остаются в силе.

Обратная сторона симметрична: параметры, влияющие на содержание, из ключа убирать нельзя, иначе посетитель по адресу /catalog/?page=7 получит первую страницу каталога. Правило простое: пагинация, сортировка и фильтры остаются в ключе, рекламные метки уходят.

Персонализация требует зеркальных правил обхода. Авторизованный посетитель с куки wordpress_logged_in не должен видеть кэш, корзина магазина с куки woocommerce_items_in_cart тоже. Выражение правила обхода:

(http.cookie contains "wordpress_logged_in" or starts_with(http.request.uri.path, "/wp-admin") or http.request.uri.path in {"/cart" "/checkout" "/account"})

Значение Bypass cache для этого выражения отправляет такие запросы прямо до источника. PHP-сессии с куки PHPSESSID, личные кабинеты и формы оплаты добавляются в список по мере обнаружения, и пара вечеров с журналами доступа рисует полную карту того, что кэшировать нельзя.

Чтобы собрать безопасную конфигурацию для HTML, администраторы обычно проходят один и тот же маршрут:

  1. выбрать разделы с одинаковым контентом для всех, например блог и справочник, и включить для них кэш через Cache Rule с Edge Cache TTL в 5-10 минут;
  2. добавить обход кэша для авторизованных по куки wordpress_logged_in и для путей /wp-admin, /cart, /checkout и /account;
  3. переключить Browser Cache TTL на Respect Existing Headers, чтобы панель не перекрывала заголовки источника;
  4. вычеркнуть рекламные метки utm и gclid из кэш-ключа, оставив параметры пагинации и фильтров;
  5. приучить деплой к точечному Purge by URL по списку изменённых адресов вместо полного сброса;
  6. после каждого релиза проверять cf-cache-status на живом сайте и заголовок Cache-Control, ушедший в браузер.

Проверка результата по заголовкам ответа и метрикам панели

Финальная проверка делается одной командой:

curl -sI https://www.example.com/blog/post-1/

Заголовок cf-cache-status отвечает на вопрос, откуда пришёл ответ. HIT означает выдачу из кэша узла. MISS фиксирует промах с пополнением кэша. EXPIRED застает устаревшую копию в момент обновления. UPDATING показывает отдачу старой версии на время фоновой актуализации. DYNAMIC и BYPASS сигнализируют, что объект не кэшируется вовсе. Заголовок age сообщает возраст копии в секундах и мгновенно разоблачает слишком щедрый TTL, а cache-control в ответе показывает, что именно ушло в браузер.

В панели аналитики две метрики говорят больше прочих. Cache HIT ratio по статике стоит держать выше 90%, иначе кэш-ключ размыт лишними параметрами или правила обхода слишком щедрые. Bandwidth saved показывает долю трафика, которую источник не отдал. Tiered Cache собирает промахи нижних узлов на верхних: источник, живущий вдали от аудитории, дёргается в разы реже, и промах стоит не полный круг через океан, а короткий перегон внутри сети.

Мерить имеет смысл не только TTFB, но и Core Web Vitals на реальных посетителях. LCP контентных страниц обычно съезжает с 3,0-3,5 секунд к 1,2-1,5, и разница заметнее всего в вечерние часы, когда межрегиональные каналы нагружены. Замеры до и после из разных городов снимаются за один вечер и честнее любой панели показывают, кому именно сайт стал быстрее.

Хорошо настроенный CDN снимает сетевую половину проблемы скорости, дисциплина кэширования закрывает вторую. Короткие TTL на HTML, точечные правила обхода для персонализации, вычищенные рекламные параметры из ключа и прицельный сброс дают редкое сочетание: страницы открываются за полторы секунды из каждого города региона, и при этом читатели не застревают на вчерашних версиях текста.