Briefly about the basic caching techniques in the browser

In this article, we will look at several basic caching methods, such as using HTTP headers. Cache-Control, ETagAnd If-Modified-SinceandLocalStorage.

Cache-Control

Cache-Control is an HTTP header that is used to define rules for caching content in browsers and proxy servers. It contains several directives that specify exactly how to cache server responses:

  • max-age: Specifies the maximum time in seconds that a cached response is considered fresh. After this time, the cache is considered stale and must be refreshed.

  • s-maxage: look like max-agebut only applies to shared caches such as CDNs. If this directive is present, it takes precedence over max-age for shared caches.

  • no-cache: allows caching the response, but requires it to be checked by the server before each use. This way the user will always receive up-to-date data if it has changed on the server.

  • no-store: Prevents storage of any parts of the response. This directive is used when the confidentiality of the data does not allow caching.

  • private: specifies that the response should be cached only in the user's local cache, which is good for personalized data that shouldn't be available to other users.

  • public: Allows the response to be cached in shared caches even if it contains user credentials.

  • immutable: Specifies that the response will not change on the server side and does not require re-validation before it expires.

We implement Cache-Control on a server with Node.js and framework Express. Let's create a simple web server that serves static files with different caching settings:

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}`);
});

The first middleware sets the title Cache-Control for all server responses. This is very good. convenient if you want all resources to have the same caching settings by default.

express.static used to serve static files from a folder public. The previously set header is also used here Cache-Control.

For all routes, see the comments in the code.

ETag and If-Modified-Since

ETag (Entity Tag) — is an HTTP response header that uniquely identifies a specific version of a resource on the server. It helps optimize caching by reducing the number of necessary requests to the server and saving bandwidth. If the content of the resource has not changed, the server can respond with the 304 Not Modified code, which allows the client to use the cached version of the resource.

If-Modified-Since — is an HTTP request header that is used to request a resource only if it has been modified since the date specified in the header. If the resource has not been modified, the server also responds with a 304 Not Modified code, allowing the client to use a cached copy.

For implementationETag And If-Modified-Since We will also use Node.js and the Express framework. Let's create an example where the server will respond to HTTP requests with these headers to optimize content caching:

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}`);
});

First we create a function generateETagwhich takes the contents of a file and returns its MD5 hash. This hash is used as the ETag value.

In middleware, the server first tries to read the file from the folder public based on the request URL. If the file is found, the server generates an ETag and receives the date the file was last modified. Then the headers are checked If-None-Match And If-Modified-SinceIf the conditions match, the server returns status 304, which means the client can use the cached version of the file.

If the header conditions do not match, i.e. the file has changed or the ETag does not match, the file contents are sent to the client.

LocalStorage

LocalStorage provides the ability to store data as key-value pairs directly in the user's browser. The data is saved without expiration and is available even after restarting the browser. However, there are a number of restrictions:

  • Capacity: LocalStorage is typically limited to approximately 5MB per domain, which may vary depending on the browser.

  • Synchronicity: The LocalStorage API is synchronous, which can block the main thread if the data operations are heavy or long.

  • Data type: LocalStorage can only store strings. To store objects and arrays, you must use JSON.stringify() while maintaining and JSON.parse() when retrieving data.

  • Safety: Storing sensitive data in LocalStorage can be risky because the data is accessible to any scripts on the same domain.

LocalStorage is often used to store user preferences or form data that needs to be saved between sessions. For example, saving the username:

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

To work with objects, you must use serialization and deserialization:

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 is easy to use thanks to its built-in browser API. However, to improve data management and convenience, you can implement a wrapper:

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();
  }
}

This way, you can simplify working with LocalStorage by adding methods for saving, retrieving, and deleting data, as well as completely clearing the storage.

Caching not only speeds up page loading, but also reduces data transfer costs. Therefore, each of these methods has its place and can be used depending on the needs.


You can learn solutions that can withstand a large number of requests per second and properly optimize server performance in the online course “Highload Architect”.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *