Блог

Правильний конфіг для кешування SPA

July 02, 2022

Photo by Cris Ovalle

Всі знають, що кешування це важлива і необхідна для кожного сайту річ. Але як показує практика на цю річ завжди забивають. Навіть на великих проектах можна побачити просто дефолтні конфіги, чи якісь криві, не оптимальні конфіги, які ще згодом копіюють між всіма новими сервісами і ще більше розповсюджують цю заразу. Особливо не паряться коли мова йде про усілякі адмінки де дуже мало користувачів. Всі думають, що потім на дозвіллі налаштують, але це дозвілля так і не настає, бо це знову треба розбиратись в 50 відтінках кешу. В кінці-кінців продакшен рішення стандартизується і зводиться до “тричі натисни f5”, чи “відкрий в режимі інкогніто”.

На цю тему є дуже багато постів з детальними описами різних стратегій кешування, наприклад:

Мета мого поста не розжовувати деталі, а запропонувати вже готові і актуальні на сьогоднішній день налаштування nginx для корректного кешування SPA сайтів.

Я розраховую на проекти, які використовують бандлери типу webpack, які дозволяють генерувати файли з хешами контенту. З такими налаштуваннями ім’я файлу змінюється кожен раз, коли змінюється його контент, а отже ці файли можна агресивно кешувати і в вас ніколи не буде ситуації, що десь залип старий закешований файл.

Головне в цій схемі не так кешування скриптів і картинок, як правильні налаштування для точки входу — файл index.html. Якщо ваш сервер віддає цей файл на дефолтних налаштуваннях, то браузер буде сам обирати що кешувати, а що ні. І це є проблема, оскільки саме index.html вирішує чи ваш фронт запуститься з файлу app.abcd1234.js, чи з файлу app.dcba4321.js. Саме через цей файл і виникає необхідність натискати F5 декілька разів. Гарна новина в тому, що index.html зазвичай дуже маленький, тому достатньо просто наказати браузеру завжди перевіряти актуальність цього файлу.

Сподіваюсь минулі абзаци трохи описали суть проблеми, а тепер перейдемо до прикладів:

location / {
  # we will use only etag, so disable last modified
  add_header Last-Modified "";
  # browser must always check freshness using etag
  add_header Cache-Control "private, no-cache, must-revalidate";

  try_files $uri /index.html;
}

location ^~ /static/ {
  # we will use only etag, so disable last modified
  add_header Last-Modified "";
  # cache forever because we have hashed file names here
  add_header Cache-Control "public, max-age=31536000, immutable";
}

Перший блок стосується index.html. За замовчанням nginx вміє відправляти заголовки ETag та Last-Modified. Обидва заголовки переслідують однакову мету, але досягають її різними способами. ETag на мою думку кращий, оскільки залежить від контенту файлу, тому я в першому рядку вимикаю відправку заголовку Last-Modified.

Наступний рядок конфігурує поведінку кеша. Коротко про кожну директиву:

  • private — повідомляє, що цей файл не можна кешувати на CDN чи кешуючих проксі. В кешуванні на проміжних серверах немає сенсу, оскільки ми хочемо щоб браузер кожен раз переконувався в актуальності index.html, а отже нам завжди потрібно, щоб запит долітав до нашого origin серверу.
  • no-cache — ця директива трохи заплутує своєю назвою. В дійсності вона говорить, що кешувати можна, але треба кожного разу перевіряти актуальність файлу. Тобто ми просимо браузер кожного разу відправляти запит з ETag і якщо сервер відповість 304 Not Modified, тоді браузер може використати закешовану сторінку.
  • must-revalidate — в деяких випадках браузер може вирішити використати кеш “востаннє” незважаючи на те, що інформація об’єктивно застаріла. Ця директива говорить браузеру в жодному разі не використовувати кеш, якщо дані в ньому протухли.

Зверніть увагу. Якщо ви використовуєте add_header в блоці location це призводить до ігнорування всіх директив add_header, що були задані на верхньому рівні. Тому якщо вам потрібні глобальні налаштування заголовків, вам необхідно продублювати їх в location.

Рядок try_files додає фолбек на index.html, якщо не було знайдено відповідного до запиту файлу. Це та магія, яка дозволяє spa генерувати будь-які роути в браузері, які не відповідають жодному фізичному файлу на сервері.

Другий блок location спирається на те, що всі файли білда розміщені на сервері в одній директорії /static. Ми конфігуруємо кеш для всіх файлів з цієї директорії.

Ось що відбувається в Cache-Control:

  • public — дозволяємо кешування на CDN
  • max-age=31536000 — виставляємо максимально дозволенний специфікацією час життя для файлів (1 рік)
  • immutable — додатково повідомляємо, що ці файли ніколи не будуть змінюватися. Цю директиву підтримують не всі браузери, тому частина браузерів її просто проігнорує

Конфіг для випадку коли файли фронта лежать в корені

Якщо вам не пощастило і всі файли розміщені вперемішку в корені директорії білда, а часу переналаштовувати білд немає, можна замінити location ^~ /static на location ~* \.(jpg|jpeg|png|ico|svg|gif|js|css|woff|woff2)$. В цьому випадку замість того щоб матчити всі файли з певної директорії, ми намагаємось перерахувати усі розширення файлів, які мали були бути згенеровані webpack.

Зверніть увагу. Якщо на сервері є також статичні файли, які не проходили хешування під час білду, тоді ви не можете використовувати цю стратегію, оскільки це закешує нехешовані файли назавжди (на дуже великий час). Наприклад якщо у вас в корені є скрипт для service worker, то в жодному разі не використовуйте цей конфіг. Краще переналаштуйте webpack, щоб він складав статику в окрему директорію.