Привет, Хабр!

Сегодня мы поговорим о крайне важной, но порой недооцененной теме — кешировании в браузере.

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

В статье рассмотрим несколько основных методов кеширования, таких как использование HTTP заголовков Cache-Control, ETag, и If-Modified-Since, а такжеLocalStorage.

Cache-Control

Cache-Control — это HTTP-заголовок, который используется для определения правил кеширования контента в браузерах и прокси-серверах. Он содержит несколько директив, которые указывают, как именно кешировать ответы сервера:

  • max-age: задает максимальное время в секундах, в течение которого кешированный ответ считается свежим. После истечения этого времени кеш считается устаревшим и должен быть обновлён.

  • s-maxage: похож на max-age, но применяется только к общим кешам, таким как CDN. Если эта директива присутствует, она имеет приоритет над max-age для общих кешей.

  • no-cache: позволяет кешировать ответ, но требует его проверки у сервера перед каждым использованием. Так пользователь будет всегда получать актуальные данные, если они изменились на сервере.

  • no-store: запрещает хранение любых частей ответа. Эта директива используется, когда конфиденциальность данных не позволяет их кеширование.

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

  • public: позволяет кешировать ответ в общих кешах, даже если он содержит учётные данные пользователя.

  • immutable: указывает, что ответ не изменится на стороне сервера и не требует повторной проверки до истечения срока его действия.

Реализуем Cache-Control на сервере с Node.js и фреймворком Express. Создадим простой веб-сервер, который отдает статические файлы с различными настройками кеширования:

const express = require('express');
const app = express();
const PORT = 3000;

// Middleware для установки Cache-Control для всех статических файлов
app.use((req, res, next) => {
  // устанавливаем Cache-Control для всех ответов
  res.set('Cache-Control', 'public, max-age=3600'); // кешировать на 1 час
  next();
});

// отдача статических файлов
app.use(express.static('public'));

// роут для демонстрации no-cache
app.get('/no-cache', (req, res) => {
  res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.send('Эта страница не будет сохраняться в кеше.');
});

// роут для демонстрации private cache
app.get('/private', (req, res) => {
  res.set('Cache-Control', 'private, max-age=3600');
  res.send('Эта страница кешируется только в приватном кеше пользователя.');
});

// роут для демонстрации immutable
app.get('/immutable', (req, res) => {
  res.set('Cache-Control', 'public, max-age=31536000, immutable');
  res.send('Эта страница кешируется на длительный срок, содержимое не изменяется.');
});

// запуск сервера
app.listen(PORT, () => {
  console.log(`Сервер запущен на порту ${PORT}`);
});

Первый middleware устанавливает заголовок Cache-Control для всех ответов сервера. Это оч. удобно, если вы хотите, чтобы по дефолту все ресурсы имели одинаковые настройки кеширования.

express.static используется для обслуживания статических файлов из папки public. Здесь же применяется ранее установленный заголовок Cache-Control.

По всем роутам см. комментарии в коде.

ETag и If-Modified-Since

ETag (Entity Tag) — это заголовок HTTP-ответа, представляющий собой уникальный идентификатор конкретной версии ресурса на сервере. Он позволяет оптимизировать кеширование, сокращая количество необходимых запросов к серверу и экономя пропускную способность. Если содержимое ресурса не изменилось, сервер может ответить с кодом 304 Not Modified, что позволяет клиенту использовать кешированную версию ресурса.

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

Для реализацииETag и If-Modified-Since будем также использовать Node.js и фреймворк Express. Создадим пример, где сервер будет реагировать на HTTP-запросы с этими заголовками для оптимизации кеширования контента:

const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
const PORT = 3000;

// функция для генерации ETag на основе содержимого файла
const generateETag = (content) => {
    return crypto.createHash('md5').update(content).digest('hex');
};

// Middleware для обработки If-Modified-Since и ETag
app.use((req, res, next) => {
    const url = req.url === '/' ? '/index.html' : req.url;
    const filePath = `./public${url}`;
    fs.readFile(filePath, (err, data) => {
        if (err) {
            res.status(404).send('File not found');
            return;
        }

        const fileETag = generateETag(data);
        const fileLastModified = fs.statSync(filePath).mtime.toUTCString();

        res.setHeader('Last-Modified', fileLastModified);
        res.setHeader('ETag', fileETag);

        // проверка If-None-Match для ETag
        if (req.headers['if-none-match'] === fileETag) {
            res.status(304).end();
            return;
        }

        // проверка If-Modified-Since для Last-Modified
        const ifModifiedSince = req.headers['if-modified-since'];
        if (ifModifiedSince && new Date(ifModifiedSince).getTime() >= new Date(fileLastModified).getTime()) {
            res.status(304).end();
            return;
        }

        res.locals.content = data;
        next();
    });
});

// отдача статических файлов с проверенными заголовками
app.use((req, res) => {
    res.send(res.locals.content);
});

// запуск сервера
app.listen(PORT, () => {
    console.log(`Сервер запущен на порту ${PORT}`);
});

Сначала создаем функцию generateETag, которая принимает содержимое файла и возвращает его MD5-хеш. Этот хеш используется как значение ETag.

В middleware сервер сначала пытается прочитать файл из папки public основываясь на URL запроса. Если файл найден, сервер генерирует ETag и получает дату последнего изменения файла. Затем проверяются заголовки If-None-Match и If-Modified-Since. Если условия совпадают, сервер возвращает статус 304, что означает, что клиент может использовать кешированную версию файла.

Если условия заголовков не совпали, то есть файл изменился или ETag не совпадает, содержимое файла отправляется клиенту.

LocalStorage

LocalStorage предоставляет возможность сохранения данных в виде пар ключ-значение прямо в браузере пользователя. Данные сохраняются без срока действия и доступны даже после перезапуска браузера. Однако есть ряд ограничений:

  • Емкость: обычно LocalStorage ограничен примерно 5MB на домен, что может варьироваться в зависимости от браузера.

  • Синхронность: API LocalStorage является синхронным, что может заблокировать основной поток, если операции с данными тяжелые или долгие.

  • Тип данных: LocalStorage может сохранять только строки. Для сохранения объектов и массивов необходимо использовать JSON.stringify() при сохранении и JSON.parse() при извлечении данных.

  • Безопасность: хранение чувствительных данных в LocalStorage может быть рискованным, т.к данные доступны любым скриптам на том же домене.

LocalStorage часто используется для хранения пользовательских настроек или данных форм, которые нужно сохранить между сессиями. Например, сохранение имени пользователя:

localStorage.setItem('username', 'IVAN');
const username = localStorage.getItem('username');
console.log(username); // Output: IVAN

Для работы с объектами необходимо использовать сериализацию и десериализацию:

const user = { name: 'IVAN', age: 30 };
localStorage.setItem('user', JSON.stringify(user));
const retrievedUser = JSON.parse(localStorage.getItem('user'));
console.log(retrievedUser); // Output: { name: "IVAN", age: 30 }

LocalStorage прост в использовании благодаря встроенному API браузера. Однако для улучшения управления данными и повышения удобства, можно реализовать обертку:

class LocalStorageService {
  static setItem(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  static getItem(key) {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  }

  static removeItem(key) {
    localStorage.removeItem(key);
  }

  static clear() {
    localStorage.clear();
  }
}

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

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


Научиться решениям, которые выдерживают большое количество запросов в секунду и правильно оптимизировать работоспособность серверов можно на онлайн-курсе "Highload Architect".

Комментарии (5)


  1. gun_dose
    01.07.2024 08:28
    +1

    По localStorage у вас не указано самое главное - на клиентской части он доступен только через javascript. Это значит, что обратиться к данным из localStorage можно только после того, как загрузится HTML-код страницы, загрузится и инициализируется скрипт. Это очень важный момент, т.к. к обычному кэшу браузер обращается непосредственно перед запросом, пока HTML документ ещё не существует в браузере.

    А ещё не затронута тема кэширования через serviceWorker'ы


  1. tema4p
    01.07.2024 08:28

    А как же Web API Сache? Без этого это не только "кратко" но и далеко "не полно". https://developer.mozilla.org/en-US/docs/Web/API/Cache


  1. pae174
    01.07.2024 08:28

    Для заголовка ответа cache-control не раскрыта тема директивы stale-while-revalidate. Она разрешает клиенту использовать закэшированный контент даже после истечения max-age и при этом обновлять его в фоновом режиме. Поддерживается пока не везде.


  1. monochromer
    01.07.2024 08:28

    res.set('Cache-Control', 'no-cache, no-store, must-revalidate');

    Про must-revalidate не рассказали. А есть ли смысл использовать эти значения no-cache и no-store вместе?


  1. alek0585
    01.07.2024 08:28

    Статья про кеширование на 3 абзаца... Дожили