← Назад к вопросам
Как управлять кэшом на уровне запросов в браузере
2.0 Middle🔥 131 комментариев
#Кэширование и NoSQL
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Управление кэшом на уровне запросов в браузере: полный разбор
Большинство современных приложений требуют оптимизации сетевых запросов. Кэширование на уровне браузера — это один из самых эффективных способов улучшить производительность. Давайте разберёмся, как это работает и как это настраивать из бэкенда.
1. HTTP Headers для управления кэшом
Cache-Control Header (Основной инструмент)
@RestController
@RequestMapping("/api/posts")
public class PostController {
@GetMapping("/{id}")
public ResponseEntity<Post> getPost(@PathVariable String id) {
Post post = postService.findById(id);
// Кэшируем на 1 час в браузере
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
.cachePublic())
.body(post);
}
}
Основные директивы Cache-Control:
Cache-Control: public, max-age=3600
- public — кэш может быть сохранён браузером и промежуточными прокси
- private — кэш только для браузера, не для прокси
- max-age=3600 — кэш действителен 3600 секунд (1 час)
- no-cache — проверять на сервере перед использованием
- no-store — не кэшировать совсем
- must-revalidate — после истечения max-age, обязательно проверить на сервере
Примеры конфигураций
@RestController
@RequestMapping("/api")
public class CacheExampleController {
// 1. Статические данные - кэшируем на длительный период
@GetMapping("/config/app")
public ResponseEntity<AppConfig> getAppConfig() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)
.cachePublic())
.body(appConfigService.getConfig());
}
// 2. Данные пользователя - приватный кэш
@GetMapping("/user/profile")
public ResponseEntity<User> getUserProfile() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
.cachePrivate())
.body(userService.getCurrentUser());
}
// 3. Чувствительные данные - не кэшируем
@GetMapping("/payment/card")
public ResponseEntity<PaymentCard> getPaymentCard() {
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore())
.body(paymentService.getCard());
}
// 4. Данные, часто обновляемые - проверяем валидность
@GetMapping("/stock/prices")
public ResponseEntity<StockPrices> getStockPrices() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)
.mustRevalidate())
.body(stockService.getPrices());
}
}
2. ETag и валидация кэша
ETag позволяет браузеру проверить, изменился ли контент, без загрузки всего ресурса.
@RestController
@RequestMapping("/api/posts")
public class PostController {
@GetMapping("/{id}")
public ResponseEntity<Post> getPost(
@PathVariable String id,
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
Post post = postService.findById(id);
String eTag = generateETag(post); // Хеш контента
// Если браузер отправил тот же ETag, контент не изменился
if (eTag.equals(ifNoneMatch)) {
// Возвращаем 304 Not Modified (без тела)
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(eTag)
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.build();
}
// Контент изменился, отправляем полный ответ
return ResponseEntity.ok()
.eTag(eTag)
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.body(post);
}
private String generateETag(Post post) {
// Эконом версия
return "\"" + post.getId().hashCode() + "\"";
// Или используйте: DigestUtils.md5DigestAsHex(post.toString().getBytes())
}
}
Как это работает:
- Браузер получает ETag:
ETag: "abc123" - Браузер кэширует контент
- При следующем запросе браузер отправляет:
If-None-Match: "abc123" - Если ETag совпадает, сервер возвращает 304 Not Modified
- Браузер использует кэшированный контент
3. Last-Modified и If-Modified-Since
@GetMapping("/{id}")
public ResponseEntity<Post> getPost(
@PathVariable String id,
@RequestHeader(value = "If-Modified-Since", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime ifModifiedSince) {
Post post = postService.findById(id);
LocalDateTime lastModified = post.getUpdatedAt();
// Если ресурс не был изменён после указанного времени
if (ifModifiedSince != null && !lastModified.isAfter(ifModifiedSince)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.lastModified(lastModified)
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.build();
}
return ResponseEntity.ok()
.lastModified(lastModified)
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.body(post);
}
4. Глобальная конфигурация кэширования
@Configuration
public class CacheConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Статические ресурсы (JS, CSS, шрифты) - кэшируем на долгий период
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)
.cachePublic()
.immutable()); // Контент никогда не изменится
// Изображения - кэшируем, но проверяем
registry.addResourceHandler("/images/**")
.addResourceLocations("classpath:/images/")
.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)
.cachePublic()
.mustRevalidate());
}
}
5. Более сложный пример с версионированием
@RestController
@RequestMapping("/api/v1")
public class ApiController {
// Версионированный контент можно кэшировать очень долго
@GetMapping("/data/v{version}")
public ResponseEntity<Data> getVersionedData(@PathVariable int version) {
Data data = dataService.findByVersion(version);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)
.cachePublic()
.immutable()) // URL изменится при обновлении версии
.body(data);
}
}
// В HTML файл подставляется правильная версия ресурса
// <script src="/js/app.v1.js"></script>
// <link rel="stylesheet" href="/css/style.v1.css">
6. Spring Boot application.properties
# Время кэширования статических ресурсов (в секундах)
spring.web.resources.cache.period=31536000
# Или используйте Duration
spring.web.resources.cache.cachecontrol.max-age=365d
spring.web.resources.cache.cachecontrol.cache-public=true
# Отключить кэширование в development
spring.web.resources.cache.cachecontrol.max-age=0
spring.web.resources.cache.cachecontrol.must-revalidate=true
7. JavaScript для работы с кэшом браузера
Не забывайте, что вы контролируете это из бэкенда через заголовки, но вот как браузер это использует:
// Браузер автоматически следит за Cache-Control
// Вы можете программно очистить кэш
if ('caches' in window) {
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
}
// Service Worker для расширенного кэширования
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
})
);
});
8. Когда кэшировать, а когда нет
| Тип данных | Cache-Control | Причина |
|---|---|---|
| JS/CSS/Шрифты | max-age=365d | Статические, версионируются в URL |
| Изображения | max-age=30d | Редко меняются |
| API данные | max-age=1h | Может быть устаревшей |
| Пользовательский профиль | max-age=1h, private | Персональные данные |
| Пароли/Платежи | no-store | Никогда не кэшировать |
| Динамический контент | no-cache, must-revalidate | Часто обновляется |
9. Отладка кэширования
# Проверить заголовки ответа
curl -i https://api.example.com/api/data
# Увидите:
# Cache-Control: public, max-age=3600
# ETag: "abc123"
# Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
Итоговая стратегия кэширования
- Статические ресурсы — max-age=365d (долгий период)
- API данные — max-age=1h + must-revalidate (проверка обновлений)
- Пользовательские данные — max-age=1h + private (в браузере только)
- Чувствительные данные — no-store (никогда)
- Часто меняющиеся — no-cache (всегда проверять)
Правильная стратегия кэширования может снизить нагрузку на сервер на 80% и ускорить приложение в 2-3 раза!