From b85a9e1b9b8be8cada70fc8a5b68a40ff0b53b0a Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 19 May 2026 10:11:09 +0200 Subject: [PATCH] fix(dev-deploy): explicit Cache-Control on the UI surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caddy's `file_server` did not set Cache-Control on the SvelteKit build, so browsers fell back to heuristic caching keyed off Last-Modified. On the long-lived dev environment the heuristic window leaves the previous deploy's `index.html` cached for minutes-to-hours, and Safari combined that with stale conditional requests into a visible multi-second freeze on every reload (the reproduction was "private window reloads instantly, normal window hangs; clearing Safari caches restores normal speed"). Push delivery itself works — heartbeat keeps the SubscribeEvents stream alive — but the bundle path stalls behind the browser revalidating a chain of stale chunks. Mirror the standard SvelteKit cache split inside both Caddyfiles: - `_app/immutable/*` — hash-named JS/CSS chunks Vite emits with content-addressed file names — `Cache-Control: public, max-age=31536000, immutable`. Safe to cache forever because the name changes whenever the content does, so the next deploy serves new files under new URLs. - Everything else (`index.html` fallback via `try_files`, `env.js`, `version.json`, `core.wasm`, `wasm_exec.js`, `favicon.svg`) — `Cache-Control: no-cache, must-revalidate`. The browser still uses the cached body when the ETag matches, but it always asks first; a fresh deploy reaches the user on the next reload without a manual cache clear. Smoke-tested locally: a docker-run Caddy with this config returns the immutable header only for `/_app/immutable/*` and the no-cache header for `/index.html`, `/env.js`, and the SPA-fallback path `/some/route`. The Caddyfile passes `caddy validate` in both `Caddyfile.dev` and `Caddyfile.prod`; the pre-existing formatting warning on line 7 is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/dev-deploy/Caddyfile.dev | 17 +++++++++++++++++ tools/dev-deploy/Caddyfile.prod | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/tools/dev-deploy/Caddyfile.dev b/tools/dev-deploy/Caddyfile.dev index eb949a1..b882736 100644 --- a/tools/dev-deploy/Caddyfile.dev +++ b/tools/dev-deploy/Caddyfile.dev @@ -13,6 +13,23 @@ @frontend host www.galaxy.lan handle @frontend { root * /srv/galaxy-ui + + # SvelteKit emits hash-named JS/CSS chunks under + # `_app/immutable/`; the file name changes whenever the + # content changes, so the browser can cache them forever. + # Without an explicit Cache-Control, Caddy falls back to + # heuristic caching that revalidates on every reload — + # measurably slow on Safari + the long-lived dev stack + # when the cache is warm. Everything else (index.html + # fallback, env.js, version.json, core.wasm, + # wasm_exec.js, favicon.svg) must revalidate so a fresh + # deploy lands without the user having to clear the + # cache by hand. + @immutable path /_app/immutable/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + @dynamic not path /_app/immutable/* + header @dynamic Cache-Control "no-cache, must-revalidate" + try_files {path} /index.html file_server encode zstd gzip diff --git a/tools/dev-deploy/Caddyfile.prod b/tools/dev-deploy/Caddyfile.prod index 5784c28..45895f1 100644 --- a/tools/dev-deploy/Caddyfile.prod +++ b/tools/dev-deploy/Caddyfile.prod @@ -5,6 +5,16 @@ www.galaxy.com { root * /srv/galaxy-ui + + # Mirrors the cache policy `Caddyfile.dev` documents in detail: + # SvelteKit's hash-named `_app/immutable/*` is safe to cache + # forever; everything else must revalidate so a deploy reaches + # the browser without a manual cache clear. + @immutable path /_app/immutable/* + header @immutable Cache-Control "public, max-age=31536000, immutable" + @dynamic not path /_app/immutable/* + header @dynamic Cache-Control "no-cache, must-revalidate" + try_files {path} /index.html file_server encode zstd gzip