Хэширование имён файлов для долгосрочного кэширования

Принцип долгосрочного кэширования и роль хэшей

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

Ключевая идея:

URL файла = ключ к кэшу

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

В контексте сборщика Esbuild это реализуется через шаблоны имён выходных файлов, где в имя подставляются специальные токены:

  • [name] — исходное имя модуля
  • [ext] — расширение файла
  • [hash] — хэш содержимого

Базовый механизм хэширования в Esbuild

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

Основные параметры конфигурации:

  • entryNames
  • chunkNames
  • assetNames

Пример базовой конфигурации:

import * as esbuild from 'esbuild';

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  entryNames: '[name]-[hash]',
  chunkNames: 'chunks/[name]-[hash]',
  assetNames: 'assets/[name]-[hash]',
});

Поведение [hash]

Хэш в Esbuild:

  • вычисляется на основе содержимого выходного файла
  • меняется только при изменении содержимого
  • применяется к JS, CSS и другим ассетам (если они проходят через pipeline сборки)

Важное свойство:

Один и тот же входной код всегда даёт одинаковый хэш в рамках одинаковой сборки


Типы файлов и стратегии именования

1. Entry points

Entry-файлы — это точки входа приложения.

entryNames: '[name]-[hash]'

Пример результата:

dist/index-a1b2c3.js
dist/admin-d4e5f6.js

Здесь:

  • index и admin — имена entry points
  • a1b2c3, d4e5f6 — хэши содержимого

2. Code splitting и чанки

При включённом splitting: true Esbuild формирует динамические чанки:

esbuild.build({
  entryPoints: ['src/app.js'],
  bundle: true,
  splitting: true,
  format: 'esm',
  outdir: 'dist',
  chunkNames: 'chunks/[name]-[hash]',
});

Пример структуры:

dist/
  app-9f8e1c.js
  chunks/
    vendor-3aa21d.js
    utils-7c19bd.js

Особенности:

  • чанки формируются автоматически
  • имя чанка может быть неочевидным (Esbuild оптимизирует граф модулей)
  • хэш гарантирует корректную инвалидацию кэша при изменении зависимостей

3. Ассеты (изображения, шрифты, стили)

Ассеты, импортируемые через JS или CSS, также получают хэшированные имена:

assetNames: 'assets/[name]-[hash]'

Пример:

dist/assets/logo-91c2ab.png
dist/assets/font-12fe44.woff2

Особенности вычисления хэша в Esbuild

Хэш зависит от итогового содержимого

Esbuild формирует хэш не от исходного файла, а от финального результата:

  • инлайнинг импортов
  • tree shaking
  • minification
  • переименование переменных

Следствие:

Любое изменение в зависимостях может изменить хэш даже без изменения самого файла.


Стабильность хэшей

При стабильной сборке:

  • одинаковые зависимости
  • одинаковый порядок модулей
  • одинаковая конфигурация

дают стабильные хэши, что критично для CDN-кэширования.


Связь хэширования и tree shaking

Tree shaking напрямую влияет на итоговый хэш.

Пример:

// utils.js
export const a = 1;
export const b = 2;

// index.js
import { a } from './utils.js';
console.log(a);

После сборки:

  • b удаляется
  • итоговый файл содержит только a
  • хэш зависит только от a

Таким образом:

меньше кода → другой хэш → корректная инвалидизация кэша


Метаданные сборки (metafile) и контроль кэширования

Esbuild может генерировать метафайл сборки:

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  metafile: true,
});

Он содержит структуру:

  • входные файлы
  • выходные файлы
  • зависимости
  • размеры
  • граф связей

Это позволяет:

  • анализировать влияние изменений на хэши
  • строить собственные стратегии кэширования
  • отслеживать стабильность сборки

Инвалидация кэша через хэш: практическая модель

Система долгосрочного кэширования обычно разделяет ресурсы:

1. Файлы с хэшем (immutable)

app-3f2a1d.js
vendor-91c44e.js
styles-88bd11.css

Заголовки:

Cache-Control: public, max-age=31536000, immutable

2. Файл без хэша (entry HTML)

index.html

Он всегда обновляется и содержит ссылки на новые хэшированные файлы.


Связка с HTML генерацией

Esbuild сам по себе не генерирует HTML, но часто используется вместе с плагинами.

Пример логики:

<script src="app-3f2a1d.js"></script>

После пересборки:

<script src="app-7c91aa.js"></script>

HTML становится единственной точкой инвалидации кэша.


Проблема: пересоздание хэшей при малых изменениях

Даже незначительные изменения могут менять хэш большого бандла:

  • изменение порядка импортов
  • обновление зависимости
  • изменение минификации

Это связано с тем, что Esbuild генерирует единый граф и пересчитывает финальный результат.


Разделение кэша: vendor и application код

Для повышения стабильности используется разделение:

Vendor bundle

entryPoints: {
  vendor: ['react', 'react-dom']
}

или через splitting:

  • сторонние библиотеки попадают в отдельный чанк
  • меняются реже
  • имеют стабильный хэш

Application bundle

  • содержит бизнес-логику
  • часто меняется
  • хэш обновляется регулярно

Chunk hashing и динамический импорт

При использовании:

import('./module.js');

Esbuild создаёт отдельный чанк.

Поведение:

  • файл получает собственный хэш
  • имя зависит от содержимого модуля
  • изменения изолированы

Это позволяет:

  • обновлять части приложения независимо
  • минимизировать повторную загрузку

Asset hashing и кеширование медиа

Ассеты часто имеют самый стабильный хэш.

Пример:

import logo from './logo.png';

После сборки:

logo-82ad91.png

Особенности:

  • Esbuild копирует файл в output
  • хэш рассчитывается по бинарному содержимому
  • используется для долгосрочного CDN-кэша

Ограничения хэширования в Esbuild

Несмотря на встроенную поддержку, существуют ограничения:

  • нет встроенного контроля формата хэша (например, длины или алгоритма)
  • нет детального контроля стабильности chunk hashing
  • отсутствует встроенная интеграция с manifest-файлами (требуется metafile или сторонние инструменты)
  • нет автоматической HTML-интеграции

Использование metafile для построения манифеста

Из metafile можно извлечь карту:

  • исходный файл → выходной файл
  • путь → хэшированный путь

Пример логики:

src/index.js → dist/index-3f2a1d.js
src/utils.js → dist/chunks/utils-91c44e.js

Это используется для:

  • генерации manifest.json
  • SSR-интеграции
  • server-side подстановки URL

Кэширование в браузере и роль хэша

Браузер применяет стратегию:

  1. URL совпадает → берётся из кэша
  2. URL изменился → новая загрузка

Хэш в имени файла делает URL:

  • уникальным для каждой версии
  • неизменяемым для конкретной сборки

Практическая архитектура кэширования с Esbuild

Типичная схема:

  • index.html — без кэша или с коротким TTL
  • *.js, *.css, assets/* — immutable + long cache
  • хэши в именах обеспечивают версионирование

Результат:

  • минимизация сетевого трафика
  • отсутствие устаревших ресурсов
  • стабильность обновлений

Связь minification и хэширования

Minification напрямую влияет на хэш:

До минификации:

function sum(a, b) {
  return a + b;
}

После:

function s(n,r){return n+r}

Хэш изменяется, даже если логика не менялась.

Это важное свойство:

хэш отражает не исходник, а итоговую поставку


Стабильность сборки в CI/CD

В системах непрерывной доставки важно:

  • фиксировать версию Esbuild
  • фиксировать плагины
  • избегать недетерминированных трансформаций

Иначе:

  • одинаковый код → разные хэши
  • нестабильные деплои
  • невозможность долгосрочного кэширования