Alfa Brain

Кеширование в веб приложениях. Часть 2. Заголовок Expires.

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

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

По правде говоря я не с того начал. Начать стоило с заголовка Expires. Заголовок Expires появился раньше и до сих пор используется некоторыми веб-сайтами. В данной статье я расскажу историю этого заголовка и отличия от Cache-Control

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

Заголовок Expires появился в HTTP 1.0 в 1996 году. Он был предназначен для указания даты и времени, после которых ресурс считается устаревшим и должен быть повторно загружен с сервера. Это позволяло уменьшить нагрузку на сервер и ускорить загрузку страницы, так как браузер мог использовать закэшированный ресурс, если он еще не устарел. Однако, в HTTP 1.1 был введен более гибкий заголовок Cache-Control (его мы разбирали в прошлой статье), который позволяет более точно управлять кэшированием ресурсов. Несмотря на это, заголовок Expires все еще поддерживается и используется во всех новых версиях протокола HTTP для обратной совместимости.

Заголовок выглядит как Expires: HTTP-date где HTTP-date один из 3-х форматов даты в формате GMT. Но это уже детали.

Отлично. Давайте приступим к практике. Создадим сервер использую NodeJS и Hapi (Вы можете использовать любой фреймворк). Так же сразу добавим отправку на клиента заголовок Expires (34-я строчка):

response.header('Expires', 'Sat, 1 Oct 2050 01:00:00 GMT');

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

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

// При запуске сервера создаем ложную дату последней модификации файла
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);

        // Добавляем заголовок Expires.
        response.header('Expires', 'Sat, 1 Oct 2050 01:00:00 GMT');
    
        return response;
    }
});

server.route({
    method: 'GET',
    path:'/hello',
    handler: function (request, h) {
        const content = 'Это страница hello.';
        
        // Создаем объект ответа.
        const response = h.response(content);
        
        // Добавляем заголовок Expires.
        response.header('Expires', 'Sat, 1 Oct 2050 01:00:00 GMT');
        
        return response;
    }
});

Запустим сервис и проверим в Postman.

Заголовки Expires и Cache-Control
Заголовки Expires и Cache-Control

В ответе от сервера видим одновременно два заголовка: Expires и Cache-Control
Дело в том, что большинство серверов по умолчанию уже используют некоторые политики кеширования и в частности сервер HapiJS по умолчанию отправляет заголовок Cache-Control со значением no-cache. В случае когда клиент получает два заголовка, решение всегда в пользу более нового - то есть старый Expires учитываться не будет. Нам такой вариант не подходит. Отключим стандартное поведение сервера для роутов специальной опцией.

options: {
    cache: false // отключение кэширования
}

Должен получиться следующий код:

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

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

// При запуске сервера создаем ложную дату последней модификации файла
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('Expires', 'Sat, 1 Oct 2050 01:00:00 GMT');
    
        return response;
    },
    options: {
        cache: false // отключение кэширования
    }
});

server.route({
    method: 'GET',
    path:'/hello',
    handler: function (request, h) {        
        const content = 'Это страница hello.';
        
        // Создаем объект ответа.
        const response = h.response(content);
        
        // Добавляем заголовок Expires.
        response.header('Expires', 'Sat, 1 Oct 2050 01:00:00 GMT');
        
        return response;
    },
    options: {
        cache: false // отключение кэширования
    }
});

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

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

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

start();

Перезапускаем сервер и проверяем.

Только один заголовок Expires - Кешируем контент до 2050 года.
Только один заголовок Expires - Кешируем контент до 2050 года.

Отлично. Теперь видим что заголовок всего один и тот что нам нужен. Напомню, что запросы в таких программах как Postman или прямая перезагрузка страницы всегда будет делать запрос на сервер, проверять не изменился ли файл на сервере.
Как раз, для того чтобы проверить, работает ли заголовок Expires мы делали несколько роутов. Откроем адрес htttps://localhost:3000/ в браузере, так же откроем DevTools и попробуем перейти по ссылкам.

Кеширование работает! Заголовок Expires применим.
Кеширование работает! Заголовок Expires применим.

Контент кешируется - отлично! Обратите внимание - при прямой перезагрузке страницы браузер делает принудительный запрос контента, но при кликах по ссылкам наше случайное число больше не меняется, а это значит что контент взят из кеша и никакие запросы на сервер не выполнялись. Так же это можно понять по колонке Size в DevTools - вы увидите надпись Disk Cache или Memory Cache.

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

Disk Cache - это кэш, который хранится на жестком диске компьютера. Кэш на диске медленнее, чем кэш в оперативной памяти, но он может хранить большие объемы данных, такие как видео и аудио файлы.

Когда вы открываете веб страницу, браузер загружает ресурсы и кэширует их в Memory Cache или Disk Cache, в зависимости от того, какой тип кэша лучше подходит для каждого ресурса. Разные браузеры имеют собственные политики занесения данных в кеш.

Так выглядят кешированные запросы в Safari
Так выглядят кешированные запросы в Safari
Firefox предпочитает не сообщать где именно кеширован контент.
Firefox предпочитает не сообщать где именно кеширован контент.

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

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

// Добавляем в заголовок только одну опцию и ни слова про время жизни кеша.
response.header('Cache-Control', 'public');

Не забудьте добавить эти заголовки в оба роута.

Перезагружаем сервер, проверяем. И все работает. Старинца загружена из кеша.

Два заголовка в Safari.
Два заголовка в Safari.

Я проверил в 3-х браузерах: Chrom, Firefox и Safari. Само наличие заголовка Cache-Control не перечеркивает работу заголовка Expires. Это происходит в том случае, если заголовок Cache-Control имеет запрещающие опции, например no-cache или no-store, а так же если Cache-Control переопределяет время жизни кеша в абсолютных секундах: Cache-Control: max-age=30

В последнем случае при использовании двух заголовков кеш буден храниться 30 секунд, а не до 50-го года.

Кеш обновился спустя 30 секунд, а не через десятки лет.
Кеш обновился спустя 30 секунд, а не через десятки лет.

Пока писал статью, ответил еще на несколько вопросов.

Какое максимальное время жизни для кеша?

В rfc2616 сказано так: To mark a response as "never expires," an origin server sends an Expires date approximately one year from the time the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more than one year in the future. (Ответ нашел тут)

Какие есть проблемы при использовании заголовка Expires?

Expires работает только с датами в формате GMT, что может привести к проблемам, если на сервере и клиенте установлены разные часовые пояса. Cache-Control использует относительное время, что позволяет избежать этих проблем. 

Кроме того, некоторые браузеры и прокси-серверы могут игнорировать заголовок Expires, поэтому его использование не всегда гарантирует правильное кэширование ресурсов.

Заключение

Таким образом нужно помнить, что заголовок Expires устарел и нужно использовать Cache-Control. Но Expires все еще работает, и например, в случае невозможности использовать протокол HTTP1.1 а только HTTP1.0 то заголовок Expires будет полезен.

Сам по себе заголовок Expires очень простой и не требует ответных заголовков от клиента.

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

Если статья была полезной, не забудь поделиться с другом! Удачи!

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

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

Спецификация HTTP1.0 (https://www.w3.org/Protocols/HTTP/1.0/spec#Expires)

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

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



Поделиться: