Кеширование веб сайтов

Front-endApplication improvement

Системы кеширования уже давно укоренились в нашу жизнь. Это очень удобно. Нам не нужно снова вычислять ответ для пользователя или загружать картинку - сайты грузятся за доли секунды.

Но, для разработчика, это определённого рода беда. Классически, когда я оказываюсь на новом проекте, в ряду из первых задач стоит "установка кеширования". Я бы его устанавливал в самом конце разработки. Но, для менеджеров - это золотой кладезь. Сайт грузится быстрее, плюс ещё одна галочка для заказчика. Проблема в том, что установленную систему кеширования в 99% процентов не тестируют. Пробежались глазами, вроде всё "ок", и оставили как есть "навсегда".

Позже из кеша начинают лезть баги:

  • Закешированные данные не подходят к контрактам приложения и оно падает
  • Клиент недоумевает, почему, после загрузки обновлений у него старая страница и когда же наконец он увидит новую
  • Кеш работает, но не везде. Или не там где нужно. Или везде, даже где ненужно. Из-за чего вместо положительных результатов от использования кеша получаем тормоза

Кроме того, не будем забывать, что кеш - чёрный ящик, который крайне плохо тестируется.

Именно поэтому я решил написать статью про кеш. Чтобы как-то понять, что это и зачем.

Как работает кеш?

Представим что нам отправили запрос на наш back end. Любой. Совсем обычный запрос, как, например, "получить новости для главной страницы" - никакой фантастики. Это занимает некоторое время, правда? Ну и, нагрузка на сервер была. Окей. А теперь представим что несколько пользователей делают такой запрос. Теперь представим реально "много" пользователей. Рано или поздно ваш сервер закипит и схлопнется от нагрузки (или вы заплатите за неё, что, вероятно, будет ещё хуже).

Но, если новости не обновлялись, зачем же нам их запрашивать в базе данных снова и снова? Не проще ли получить их для одного пользователя сайта, где-то сохранить, и потом всем остальным отдавать уже готовый ответ. Вот "где-то хранить" это и есть Кеш.

Чтобы было ещё более наглядно, представим это в псевдокоде:

if request in cache {
  return cache[request]  // У нас уже есть закешированный ответ. Возвращаем его
} else {
  req = getDataFromDatabase() // Нет кеша? Делаем всё как обычно
  cache[request] = req        // Полученные данные кешируем
  return req                  // Отдаём ответ пользователю
}

Приблизительно так кеш и работает.

В каких случаях уместно использовать кеш?

Как уже стало очевидно, кеш - очень сильная технология, и используют её везде где могут. Но, если вы сомневаетесь, я подскажу, как определить, в каком месте вам нужен кеш.

Кеш нужен если:

  • Ответ на ваш запрос происходит медленно. Скорее всего, из-за сложных вычислений. Возможно, есть ошибка в коде или библиотеке. Возможно данные для вычислений берутся ещё откуда-то. В общем, если ответ долгий - это места для кеша.
  • Вычисления на запрос потенциально могут быть запущены несколько раз. То есть, один пользователь сделал запрос, потом второй, потом третий - вычисляется одно и то же, но уже какбы очередь ждёт ответа, нагрузка растёт.
  • Если ответ потенциально не изменяется. То есть, как я говорил выше, у вас выходит статья раз в неделю - нет смысла её каждый раз запрашивать список последних статей - они уже неизменятся. То же самое с локализациями, например - вы же не переименовываете кнопки каждый день.
  • Ваш доступ в базу данных платный. Тогда кешировать ответы на популярные запросы в базу данных - нормально.

Важно! Никогда не кешируйте данные относящиеся к личным данным пользователя. Так вы можете закешировать чужие кредитки, телефоны, фото и бог знает что ещё. Если не хотите попасть в новости - не делайте такое никогда. Не имейте даже привычки.

Введение в кеширование для сайтов

В википедии есть определение web cache, можете глянуть на досуге. Web cache (или http cache) - это система для оптимизации World Wide Web. Она реализована на двух сторонах - клиентской и серверной. Кеширование изображений и других файлов способствует более быстрой загрузке страниц.

Определяют две разновидности в подходах кеширования forward и reverse.

Forward - кеш находится сранужи сети веб сервера, то есть в клиентских браузерах и перед ними. С помощью него кешируются "тяжелодоступные" элементы страницы - не обязательно изображения, могут быть какие-то огромные таблицы с вычислениями и тп. При этом, прокси сервер находится между клиентом и серверной частью приложения, и, на основе хедеров запроса, определяет, нужно ли обновлять или кешировать контент.

Reverse - находится перед веб сервером/серверами, чтобы моментально отдавать ответы или отбрасывать излишние нагрузки. Обычно он базируется на CDN (content delivery network), которая получает кешированные копии контента в зависимости от точки расположения в сети. Особенно эффективен такой подход при отдаче изображений или скриптов, настроект стилей и тп. - можно сразу определить ближайший сервер для отдачи.

Типы кеша

В нашем случае их всего три

Серверный

Когда механизм для кеширования располагается на нашем сервере, как часть приложения, как отдельное приложение, или как уже готовый софт. Самыми популярными приложеними для кеширования на сервере, конечно же, являются redis и memcached.

Браузерный

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

Прокси

Располагается на proxy server или reverse proxy server. Как пример это кеш в NGINX или Apache. Возможно даже как часть вашего ISP (Internet Service Provider).

HTTP Headers

На любой запрос к вашему серверу, в ответе приходят headers, которые, кроме прочей информации несут нашему браузеру и инструкции как работать с браузерным кешем. Среди них есть два самых главных:

  1. Expires - содержит дату и время после которого закешированый ответ считается "просроченным" - то есть, требующи обновления. Если хэдек cache-control содержит директивы max-age или x-maxage, этот хидер будет проигнорен.
  2. Cache-Control - содержит директивы (инструкции) для кеширования, и может быть, как в запросе, так и в ответе. Если этот хидер есть в запросе - не обязательно что он придёт вам в ответе.
  • private - кешируется только в клиенте
  • public - кроме клиента кешируется и в прокси
  • no-store - контент не кешируется
  • no-cache - контент может быть закеширован, но должен пройти валидацию на сервере
  • max-age - число секунд, сколько контент будет в кеше

Чтобы определить, что кеш валиден, сервер посылает в ответе ещё один валидационный заголовок Etag. Это, в своём роде, уникальный идентификатор контента. Клиент посылает запрос с Etag и сервер понимает, был ли изменён контент, или нет.

Пример:

Cache-Control: max-age=600 Public no-cache
ETag: "werhwi3234hkdf"

Клиент закеширует изображение на 600 секунд. После прошествия этого времени. После клиент сделает запрос на сервер с заголовком If-None-Match и Etag со значением. Если Etag не совпал - сервер вернёт новый контент/ресурс и новый Etag. А если сопал - сервер венёт 304 Not Modified и обновит счётчик текущего кеша на 600 секунд. ETag разделяется на строгий и слабый.

Заголовок Last-Modified

Определяет дату и время последнего изменения контента. Если контент устарел, делается новый запрос с If-Modified-Since заголовком, который впоследствии используется сервером, чтобы вернуть или новый контент или статус 304 Not Modified.

Сервер отправил контент с заголовком

Last-Modified: Mon, 24 Mar 2021 11:15:30 GMT

Клиент проверил не устарел ли контент:

If-Modified-Since: Mon, 24 Mar 2021 11:15:30 GMT

По сути, это как вариация валидации кеша.

Стратегии кеширования

Считается, что нет "универсального" подхода в кешировании. Но есть два, которых выделяют.

Лёгкое кеширование - данные кешируются в браузере, но, чтобы использовать кешированные значение, кеш должен пройти валидацию на сервере. Таким образом клиент всегда имеет актуальные данные в кеше.

Cache-Control: Private, no-cache

Агрессивное кеширование - кешируется всё и надолго

Cache-Control: Public, max-age=87654321

Современные фреймворки на примере Nuxt.js

Конечно же, сегодня всё сделано намного удобнее чем 10 лет назад. Фреймворки решают большинство проблем разрабочика. Например, во фронтовых фреймворках теперь кешируют целые компоненты. Как это делает Nuxt описано в документации и добавить к этому нечего https://nuxtjs.org/docs/features/nuxt-components/#keep-alive

Middleware для кеширования в браузере

Чтобы браузер кешировал страницы нужно добавить мидлварь, которая будет добавлять Cache-Control в хидеры, и контент будет кешироваться браузером.

// helpers/cacheControl.js

const cacheControl = (values) => ({ res }) => {
  if (!process.server) return;

  const cacheControlValue = Object.entries(values)
    .map(([key, value]) => `${key}=${value}`)
    .join(',');

  res.setHeader('Cache-Control', cacheControlValue);
};

export default cacheControl;

Теперь достаточно вставить этот хэлпер в нашу страничку:

// Home.vue

export default {
  name: 'Home',
  middleware: cacheControl({
    'max-age': 60,
    'stale-when-revalidate': 5
  }),
  ...
}