Привет, Хабр!
Сегодня мы поговорим о крайне важной, но порой недооцененной теме — кешировании в браузере.
Кеширование — это процесс сохранения копий файлов в локальном хранилище браузера, чтобы в последующем загружать их оттуда, а не с сервера. Так можно избежать лишних задержек и снизить нагрузку на сервер, т.к большинство ресурсов, таких как 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)
tema4p
01.07.2024 08:28А как же Web API Сache? Без этого это не только "кратко" но и далеко "не полно". https://developer.mozilla.org/en-US/docs/Web/API/Cache
pae174
01.07.2024 08:28Для заголовка ответа cache-control не раскрыта тема директивы stale-while-revalidate. Она разрешает клиенту использовать закэшированный контент даже после истечения max-age и при этом обновлять его в фоновом режиме. Поддерживается пока не везде.
monochromer
01.07.2024 08:28res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
Про
must-revalidate
не рассказали. А есть ли смысл использовать эти значенияno-cache
иno-store
вместе?
gun_dose
По localStorage у вас не указано самое главное - на клиентской части он доступен только через javascript. Это значит, что обратиться к данным из localStorage можно только после того, как загрузится HTML-код страницы, загрузится и инициализируется скрипт. Это очень важный момент, т.к. к обычному кэшу браузер обращается непосредственно перед запросом, пока HTML документ ещё не существует в браузере.
А ещё не затронута тема кэширования через serviceWorker'ы