Alfa Brain

Кеширование в веб приложениях. Часть 1. Заголовок Cache-Control.

Алексей ВечкановАлексей Вечканов   

Кеширование является одной из наиболее распространенных техник оптимизации производительности веб приложений. В своей работе я использую сервер HapiJS. Это довольно надежный сервис, со своим большим комьюнити и обширными инструментариями. Но вы, конечно, можете использовать любой другой веб сервер. В HapiJS кеширование может быть реализовано как на стороне клиента, так и на стороне сервера.  В этой статье мы рассмотрим оба типа кеширования, а также одновременное использование клиентского и серверного кеширования. Статья будет полезна, не только разработчикам использующим Hapi фреймворк, но и всем остальным веб разработчикам, так как затрагивает описание и предназначение заголовков HTTP.

Изображение статьи

В статье я буду приводить примеры которые я загрузил на Github. Настоятельно рекомендую не просто читать статью, а скачивать примеры и пробовать описанное в статье самостоятельно. Так информация запоминается лучше и ваше время не пройдет зря. Я буду много экспериментировать, чтобы точно понять как работает та или иная технология.

Для начала вспомним, что означает слово "Кеш"? Согласно ресурсу etymonline, слово "Кеш" ("Cache") происходит от сленга франко-канадских звероловов и означает «укрытие для припасов и провизии» (1660 г.). Так же слово можно перевести как склад, тайник или схрон.

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

Отлично! С понятием разобрались. Как же нам ускорить наше приложение используя Кеш в веб приложении на HapiJS? Об этом ниже.

Кэширование на стороне клиента

Чтобы сэкономить на передаче данных между сервером и браузером, вы можете указать последнему сохранить ресурсы в памяти браузера до определенного времени.

Протокол HTTP определяет несколько заголовков HTTP, чтобы указать, как клиенты, такие как браузеры, должны кэшировать ресурсы. Чтобы узнать больше об этих заголовках и решить, какие из них подходят для вашего варианта использования, ознакомьтесь с этим полезным руководством, составленным Google.

В первой части этого этой статьи показано, как легко настроить hapi для отправки этих заголовков клиентам.

Cache-Control и Last-Modified

Заголовок Cache-Control сообщает браузеру и любым промежуточным кэшам (между браузером и сервером могут находиться промежуточные сервера и прокси), можно ли кэшировать ресурс и на какой срок. Например, Cache-Control:max-age=30, must-revalidate, private означает, что браузер может кэшировать ресурс в течение тридцати секунд, а private означает, что он не должен кэшироваться промежуточными кэшами, а только браузером. Директива ответа must-revalidate указывает, что ответ может быть сохранен в кеше и может повторно использоваться, пока он свежий. Если ответ устаревает, его необходимо проверить на исходном сервере перед повторным использованием.

Давайте напишем простой сервер на Hapi. Создайте файл index.js и напишите следующий код. Затем запустите командой node ./index.js

const Hapi = require('@hapi/hapi');

// Создаем сервер на порту 3000
const server = Hapi.server({
    host: 'localhost',
    port: 3000
});

// Создаем простой роут для теста
server.route({
    method: 'GET',
    path:'/',
    handler: function (request, h) {
        return 'Это корневая страница.';
    }
});

// Запускаем сервер
async function start() {

    try {
        await server.start();
    }
    catch (err) {
        console.log(err);
        process.exit(1);
    }

    console.log('Сервер запущен по адресу:', server.info.uri);
}

start();

После запуска откроем адрес http://localhost:3000 в браузере:

Наш тестовый сервер.
Наш тестовый сервер.

Давайте обновим корневой роут добавив опции кеширования:

// Создаем несколько простых роутов для теста
server.route({
    method: 'GET',
    path:'/',
    handler: function (request, h) {
        return 'Это корневая страница.';
    },
    options: {
        cache: {
            // Отправляем от сервера клиенту заголовок
            // cache-control: max-age=30, must-revalidate, private
            // Время указываем в миллисекундах, в самом заголовке время в секундах
            expiresIn: 30 * 1000,
            privacy: 'private'
        }
    }
});

Конечно же после каждого изменения перезапускаем сервер. Проверим в devtools браузера, действительно ли мы получаем в ответ от сервера нужный нам заголовок?

От сервера вернулся заголовок cache-control: max-age=30, must-revalidate, private
От сервера вернулся заголовок cache-control: max-age=30, must-revalidate, private

Да, видим заголовок, но почему тогда код ответа при запросе на этот адрес всегда 200? Напомню что статус 200 означает OK - запрос выполнен удачно. Причем в документации сказано, что по умолчанию, такие ответы кешируемые, если нет других инструкций.

Но если мы сделаем несколько запросов подряд, мы все еще видим статус 200, а не ожидаемый статус 304 NOT MODIFIED. Почему? Давайте разбираться вместе.

Каждый раз видим свежий ответ от сервера.
Каждый раз видим свежий ответ от сервера.

Сначала я обратил внимание на то, что клиенты тоже отправляют серверу заголовок Cache-Control. Этот заголовок от клиента так же может указать серверу свои собственные требования. Причем в разных браузерах политика по отправляемым значениям может быть разная.

Например Google Chrome отправляет заголовок Cache-Control: max-age=0.

Google Chrome отправляет заголовок Cache-Control: max-age=0
Google Chrome отправляет заголовок Cache-Control: max-age=0

А вот Mozilla Firefox отправляет заголовок Cache-Control: no-cache.

Mozilla Firefox отправляет заголовок Cache-Control: no-cache
Mozilla Firefox отправляет заголовок Cache-Control: no-cache

Safari так же отправляет заголовок Cache-Control: no-cache

Safari отправляет заголовок Cache-Control: no-cache
Safari отправляет заголовок Cache-Control: no-cache

Так же для экспериментов я буду использовать Postman. Эта программа позволяет удобно разрабатывать и тестировать ваши HTTP запросы.

Что же отправляет Postman? А все тот же Cache-Control: no-cache. Причем значение заблокировано и управляется клиентом (Postman-ом).

Postman отправляет заголовок Cache-Control: no-cache
Postman отправляет заголовок Cache-Control: no-cache

На что же влияет этот заголовок отправляемый с клиента серверу? Такой заголовок позволят указать дополнительные параметры кеширования на сервере.


Ок. На браузеры мы повлиять не можем. Но что же с Postman? Можно ли в нем отправку? Ведь это нам нужно для теста.

Заголовок Cache-Control используется для управления кешированием в разных точках сети: на стороне клиента (браузера) и на стороне сервера.

Когда клиент отправляет запрос на сервер, он может добавлять заголовок Cache-Control, чтобы указать, как должен обрабатываться этот запрос на стороне сервера. Например, он может указать, что сервер должен закешировать ответ на некоторое время. Также клиент может отправить другие инструкции, например, требование перезагрузки кеша, чтобы обеспечить получение самой свежей версии ресурса.

Когда сервер отправляет ответ клиенту, он может также добавлять заголовок Cache-Control, чтобы указать, как должен обрабатываться этот ответ на стороне клиента. Например, он может указать, что браузер должен закешировать ответ на определенное время, чтобы уменьшить количество запросов к серверу и ускорить загрузку страниц. Также сервер может отправить другие инструкции, например, требование проверки наличия новой версии ресурса на сервере перед использованием закешированной версии.

Таким образом, заголовок Cache-Control может использоваться как на стороне клиента, так и на стороне сервера, чтобы управлять кешированием и оптимизировать работу сети.

Отлично, с этим разобрались. К кешированию на сервере мы вернемся позже, пока мы разбираем кеширование на клиента. 

Давайте отключим заголовок Cache-Control отправляемым из Postman на сервер, ведь мы все равно пока не кешируем контент на сервере, а наличие этого заголовка в запросе лишь запутывает нас в наших экспериментах.

Дело в том, что Postman по умолчанию всегда отправляет заголовок Cache-Control: no-cache. Т.е. Postman говорит серверу - "не кешируй запросы на сервере, я тут пытаюсь протестировать API".

Вы можете отключить это поведение в настройках на уровне всего приложения, либо просто определив собственный заголовок на вкладке Headers.

Для тестов я отключу этот заголовок на уровне всего приложения:

Настройки запроса
Настройки запроса
Отключаем установку заголовка no-cache 
Отключаем установку заголовка no-cache 

Пробуем выполнить очередной запрос на сервер. Наконец видим что в запросе заголовка Cache-Control нет. Но этот заголовок есть в ответе, как и ожидалось.

Заголовок Cache-Control только в ответе. Указывает, что кеш должен храниться 30 секунд.
Заголовок Cache-Control только в ответе. Указывает, что кеш должен храниться 30 секунд.

Но в ответе все еще 200? То есть кеш на стороне клиента не используется? Почему? Что мы делаем не так?

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

Необходим заголовок Last-Modified (заголовок от сервера к клиенту) и его пара, заголовок If-Modified-Since (заголовок от клиента к серверу)

Давайте рассмотрим для чего нужны эти заголовки.

HTTP заголовок "Last-Modified" (Последнее изменение) используется для указания даты и времени последнего изменения ресурса (html документа, картинки и др.) на сервере. Это позволяет браузерам и другим клиентским приложениям узнать, был ли ресурс изменен с момента последнего запроса, и если да, то загрузить обновленную версию.

Данный заголовок обычно добавляется автоматически, сервером. Например, если вы отдаете с сервера статику (картинки, pdf документы и прочее), сервер сам может узнавать последнюю дату изменения файла и подставлять заголовок Last-Modified. 

Чаще всего, пользователи Hapi для раздачи статики используют @hapi/inert. Данный плагин автоматически проставит нужные заголовки.

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

// При запуске сервера создаем ложную дату последней модификации файла
const lastModified = new Date(); 

// Создаем простой роут для теста.
server.route({
    method: 'GET',
    path:'/',
    handler: function (request, h) {
        return h.response('Это корневая страница.').header('Last-Modified', lastModified.toUTCString());
    },
    options: {
        cache: {
            // Отправляем от сервера клиенту заголовок
            // cache-control: max-age=30, must-revalidate, private
            // Время указываем в миллисекундах, в самом заголовке время в секундах
            expiresIn: 30 * 1000,
            privacy: 'private'
        }
    }
});

После перезапуска сервера, проверим ответ в Postman:

В ответе от сервера видим заголовок Last-Modified
В ответе от сервера видим заголовок Last-Modified

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

Согласно стандарту HTTP, названия всех заголовков должны быть регистронезависимыми, то есть сервер должен быть способен обрабатывать их независимо от того, написаны они заглавными буквами или строчными.

Однако, по традиции, в HTTP-заголовках используется так называемый "kanonische Form".

"Kanonische Form" - это термин, который используется в контексте HTTP-заголовков для обозначения определенного стиля написания заголовков. Рекомендуется использовать для достижения единообразия и лучшей читаемости.

Термин "Kanonische Form"  происходит из немецкого языка, где он означает "каноническая форма". В контексте HTTP-заголовков он обычно относится к стилю написания заголовков, где первая буква каждого слова заголовка (кроме предлогов, союзов и т. д.) является заглавной, а все остальные буквы - строчными.

Например, в "Content-Type" первая буква каждого слова является заглавной, а в "Accept-Encoding" - только первая буква первого слова. Такой стиль написания заголовков упрощает чтение и облегчает восприятие информации, а также помогает обеспечить единообразие в их записи.

Хотя термин "Kanonische Form" официально не определен в стандарте HTTP, он широко используется в сообществе разработчиков и экспертов по протоколу HTTP.

Таким образом, хотя сервер может обрабатывать заголовки HTTP, написанные как заглавными, так и строчными буквами, рекомендуется использовать "Kanonische Form" для их записи и заглавные буквы для обозначения методов и версии протокола HTTP.

Продолжим. Мы вернули от сервера 2 заголовка: Cache-Control который просит браузер закешировать контент на 30 секунд, и заголовок Last-Modified который сообщает о том когда этот файл был последний раз изменен.

Но мы все еще видим статус 200? Что на этот раз теперь не так?

Да, в этот раз браузер закешировал данные веб страницы, но ведь при следующем запросе контент на сервере мог измениться, поэтому клиент все равно, повторно, создает соединение с сервером при очередном запросе. В этот раз уже сервер должен решить, что отправить клиенту, либо если кеш совпадаем с файлом который сервер желает отдать клиенту и тогда сервер просто отправляет клиенту статус 304 NOT MODIFIED, что разрешает браузеру использовать кеш, либо если кеш отличается от файла на сервере, и тогда сервер отправляет статус 200, контент и новые заголовки Cache-Control и Last-Modified.

Но как сервер узнает, отличается ли кеш на клиенте от файла на сервере? Для этого, ранее, мы отправляли в браузер заголовок Last-Modified.

Теперь в работу вступает его "брат", заголовок If-Modified-Since (Есть еще заголовок If-Unmodified-Since - но об этом в другой раз).

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

Добавим этот заголовок в запрос, со значением переданным с сервера.

В запросе заголовок If-Modified-Since со значением присланным от сервера.
В запросе заголовок If-Modified-Since со значением присланным от сервера.

Ура! Мы видим статус 304. Что же он означает? Он означает, что запрос от клиента был отправлен на сервер, обработчик роута на сервере выполнился, но сервер принял решение вернуть статус 304, так как по заголовку If-Modified-Since знает, что на клиенте есть Кеш, и так как дата модификации  ресурса не поменялась, говорит клиенту - "Используй свой Кеш, нет смысла отправлять файл обратно"

По идее, заголовок If-Modified-Since клиент должен отправлять самостоятельно, ведь теперь он хранит кеш, и должен сказать браузеру когда последний раз модифицировался этот файл. Но Postman этого не делает. Вообще все поведение которое связано с кешированием, в Postman довольно отличается от поведения браузера. Для сравнения взглянем на Chrome.

Статусы 304 в Chrome.
Статусы 304 в Chrome.
Браузер Chrome самостоятельно при запросе отправляет заголовок If-Modified-Since.
Браузер Chrome самостоятельно при запросе отправляет заголовок If-Modified-Since.

Мы видим что браузер Chrome самостоятельно при запросе отправляет заголовок If-Modified-Since.

Как насчет Firefox?

Firefox так же при запросе отправляет заголовок If-Modified-Since.
Firefox так же при запросе отправляет заголовок If-Modified-Since.

А Safari? И тоже работает?

Safari тоже отправил заголовок If-Modified-Since
Safari тоже отправил заголовок If-Modified-Since

Но подождите, если я сделаю еще несколько запросов, статусы снова становятся 200. Что же это творится?

Safari не отправляет заголовок If-Modified-Since - не используем данные из кеша.
Safari не отправляет заголовок If-Modified-Since - не используем данные из кеша.
Нет заголовка If-Modified-Since
Нет заголовка If-Modified-Since

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

Вот на что я наткнулся в поисках ответа на вопрос:

Nothing in the spec says that user agents MUST or SHOULD send If-Modified-Since, they just MAY.

У браузера Safari, как всегда, свое мнение на этот счет.

Ок. Это поведение не критично, но вот что действительно странно: Почему статус 304. Ведь мы сохранили данные в Кеш браузера, а это означает, что пока не истекло время жизни Кеша, браузер вовсе не должен делать запросы на сервер, браузер должен просто вернуть данные из кеша (это будет видно в devtools) и поставить статус 200. Запрос на сервер вообще не должен происходить.

В нашем же случае мы устанавливаем, время жизни Кеша в 30 секунд. Но несмотря на это браузера все равно делает запрос на сервер, как будто время жизни кеша вышло. Сервер сравнивает дату модификации ресурса, понимает что кеш валидный и отправляет сообщение клиенту 304 NOT MODIFIED - подсказывает браузеру что он может использовать сохраненный кеш, при этом тело ответа пустое. Т.е. браузер использует Кеш, но все равно постоянно создает соединение между клиентом и сервером, что отнимает драгоценные ресурсы.

Я потратил очень много времени, чтобы понять в чем дело. Все оказалось очень просто, но я кажется потратил треть своих нервных клеток. А дело вот в чем:

Если вы перезагружаете веб страницу при помощи кнопки "Reload Page" (обычно это круговая стрелка рядом с адресной строкой), то такая перезагрузка воспринимается браузером как принудительная, поэтому браузер игнорирует Кеш и создает подключение к серверу, для отправки http сообщения. Это работает во всех браузерах примерно одинаково с небольшими отличиями.

Для того чтобы удостовериться, что это действительно так, давайте немного исправим наш код. Добавим пару роутов и ссылки между ними.

// При запуске сервера создаем ложную дату последней модификации файла
const lastModified = new Date(); 

// Создаем несколько простых роутов для теста
server.route({
    method: 'GET',
    // Тут заложим роут / и /n где n не обязательный, любой, параметр
    path: '/{n?}',
    handler: function (request, h) {
        // Генерируем случайное число, по нему мы визуально поймем, поменялся ли контент на веб странице.
        const randomNum = Math.random();
        // Выводим число в консоль сервера, просто чтобы понять, происходил ли вызов handler
        console.log('randomNum: ', randomNum);

        // Создаем HTML контент с двумя перекрестными ссылками.
        const content = `
            <p>${  Math.random(randomNum) }</p>
            <a href="/"> Главная страница </a>
            <br/>
            <a href="/2"> Страница 2 </a>
        `

        // Создаем объект ответа.
        const response = h.response(content);

        // Добавляем заголовок последней модификации ресурса.
        response.header('Last-Modified', lastModified.toUTCString());
    
        return response;
    },
    options: {
        cache: {
            // Отправляем от сервера клиенту заголовок
            // cache-control: max-age=30, must-revalidate, private
            // Время указываем в миллисекундах, в самом заголовке время в секундах
            expiresIn: 30 * 1000,
            privacy: 'private'
        }
    }
});

Теперь в ответе от сервера приходят сгенерированные страницы на которых есть перекрестные ссылки. Попробуйте переходить на ту же страницу при помощи одной ссылки, или переходить между страницами.

На анимации ниже, я показываю, как принудительная перезагрузка в Хром игнорирует Кеш и отравляет запросы на сервер, при этом сервер отвечает 304. При этом в мы видим Request Headers в блоке информации о запросе.

Но если мы начинаем переходить по ссылке, на одну и ту же страницу, то тут же видим статус 200 и в колонке size надпись disk cached (кешировано на диске). При этом, если открыть информацию о запросе, мы не видим блока Request Headers. Ведь не было никакого запроса на сервер, мы видим блок Response Headers, благодаря этому можно сделать вывод, что кешируется не только сам контент, но и заголовки от сервера в кешируемом ресурсе, то есть контент берется из кеша и подставляются все заголовки из предыдущего запроса. Как будто запрос на сервер был, но на самом деле нет.

Разница между Принудительной загрузкой и обычными переходами по ссылкам.
Разница между Принудительной загрузкой и обычными переходами по ссылкам.

Обратите внимание, на то что, когда я выполняю клик в адресной строке и нажимаю клавишу Enter, браузером Chrome это воспринимается как принудительная перезагрузка страницы - мы видим в ответе 304-й статус.

А вот в Firefox поведение немного отличается. Перезагрузка на кнопку Reload this Page так же приводит к статусу 304, если переходим по ссылкам видим ответ 200 - то есть берем данные из Кеша, вот только в отличие от Chrome в блоке информации о кешированном запросе мы видим секцию Request Headers хотя запроса не было. С одной стороны это хорошо, так как мы можем посмотреть заголовки запроса, а с другой стороны может вводить в заблуждение.

Еще одним отличием является то, что при клике в адресной строке и перезагрузки страницы при помощи клавиши Enter не воспринимается браузером Firefox как принудительный запрос, и мы видим статус 200 - данные взяты из кеша.

Особенности Кеша в Firefox
Особенности Кеша в Firefox

Поведение в Safari так же отличается. Как я сказал ранее, при принудительной перезагрузке, то есть при нажатии кнопки Reload this Page, Safari не отправляет на сервер заголовок If-Modified-Since, что не позволяет сравнить даты модификации ресурса и заставляет сервер генерировать новый контент постоянно. Перезагрузка кликом в адресной строке так же воспринимается как принудительная перезагрузка страницы. Мы видим статусы 200 в ответах, а на веб странице постоянно новые случайные числа.

Если же мы начинаем переходить по заготовленным ссылкам, кеш начинает работать. Мы видим надпись Кешировано в колонке Передано и статус 200. При этом в секции Request, в информации о запросе, видим надпись - No request, served from the memory cache.

Что касается Postman, то его запросы так же не кешируются. Да, в нем могут быть получены заголовки и пересланы на сервер, как если бы кеш был, но по факту, каждый запрос является принудительным.

Вывод по заголовкам. Все эти заголовки от сервера, управляющие Кешем на стороне клиента, хоть и задают нужное поведение браузера, но являются рекомендательными, и никто не гарантирует как именно закешируются данные на клиенте. Производители браузеров сами решают, как именно обрабатывать такие заголовки, ведь в спецификации указано, что означают заголовки, но не указывается как именно они должны работать. К тому же производители браузеров неоднократно нарушали рекомендации, и поведение может измениться в будущем. Остается самостоятельно изучать и "Держать руку на пульсе".

Заключение

Давайте резюмируем.

Мы хотим указать клиенту (браузеру) закешировать ресурс.

Для этого, в ответе от сервера необходимо прислать заголовок Cache-Control: max-age=30, где 30 это время в секундах, когда кеш будет работать. 

Если вы будете запрашивать ресурс без принудительного запроса (не нажимать кнопку Reload This Page или перезагружать страницу через адресную строку), то тогда данные ресурса будут браться из кеша.

Ответ будет 200, и в devtools мы явно увидим, что ресурс взят из кеша.

После того как 30 секунд выйдут и вы снова произведете запрос за ресурсом, браузер выполнит запрос на сервер, получит ответ 200, получит новый ресурс в теле ответа и заголовок Cache-Control: max-age=30 установит новый Кеш.

Тут тоже можно сэкономить. Если при установке кеша, вместе с заголовком Cache-Control: max-age=30 установить заголовок Last-Modified: Sun, 09 Apr 2023 11:52:22 GMT (дата последней модификации ресурса) то клиент сохранит эту дату вместе с кешем.

Когда кеш протухнет, браузер сделает запрос на сервер за свежим ресурсом, при этом отправит дополнительный заголовок If-Modified-Since: Sun, 09 Apr 2023 11:52:22 GMT с тем же значением даты. Таким образом сервер может сравнить, дату модификации своего файла, и дату модификации в кеше. Если они одинаковы, то нет смысла пересылать тело запроса еще раз, ведь в Кеше браузера все еще есть подходящие данные (после истечения срока жизни кеша данные не удаляются из браузера - политики хранения кеша в браузерах - отдельная тема для изучения).
В таком случае сервер меняет статус ответа на 304. При этом отправляет остальные заголовки нетронутыми, в том числе заголовок Last-Modified, что позволяет снова продлить время жизни Кеша.

Из особенностей браузеров стоит отметить, что кнопка Reload This Page игнорирует свежий Кеш и делает принудительный запрос на сервер каждый раз. В Postman запросы всегда принудительные. Так же в браузерах отличается запрос при обновлении адреса в адресной строке.

На этом пока все. Продолжим наши страдания в следующей част

Ссылки на материалы:

Репозиторий с примерами: (https://github.com/Hydrock/article-hapi-caching/blob/main/examples/client-side/index.js)

Прекрасное видео о кешировании на сервере и клиенте (https://www.youtube.com/watch?v=HiBDZgTNpXY)

Кеширование в Hapi (https://hapi.dev/tutorials/caching/?lang=en_US)

Cache-Control MDN (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)

На этом видео показан Remix фреймворк, но тема видео связана с Кешем. (https://www.youtube.com/watch?v=3XkU_DXcgl0)



Поделиться: