perf(server): gzip + immutable caching for the embedded SPA#582
Merged
Conversation
The single binary serves the embedded SPA directly (no NGINX), but the handler shipped raw bytes with no compression and no cache headers, so in production the JS/CSS bundles downloaded uncompressed and the browser re-fetched every hashed asset on each navigation — the reported slowness. TLS and HTTP/2 are already handled by the binary (ListenAndServeTLS, h2 auto-enabled over TLS), so the only gap vs an NGINX/Caddy front was static delivery. Close it in-process so the deployment stays a single binary: - gzip-encode compressible assets when the client sends Accept-Encoding: gzip (Vary: Accept-Encoding), identity otherwise. Compression is computed ONCE at startup (the embedded FS is fixed at build time) and cached, so it is never paid per request. - Vite's content-hashed assets/ files -> Cache-Control: public, max-age=31536000, immutable (browser never re-requests them). - index.html (the SPA shell) -> no-cache, so a new deploy is picked up immediately. An ETag on every response makes revalidation a cheap 304. - Serving is now a precomputed-table lookup (no per-request FS access, so no path-traversal surface). Scoped to the SPA handler only — /api and the SSE stream are never gzip-wrapped. Spec system-http-server -> v1.1.0: C-11, AC-15 (gzip), AC-16 (caching). Does NOT change the auth/redirect behaviour. The 'no redirect on session expiry' symptom is tracked separately (top suspect: Secure cookies require HTTPS end-to-end; the 401->refresh-cookie->/login flow already exists in the frontend client).
This was referenced Jun 16, 2026
# Conflicts: # internal/server/spa_test.go # specs/system/http-server.spec.yaml
internal/server/spa/ is gitignored, so the assets/app-abc123.js fixture only existed locally — CI's Makefile stub created index.html alone, leaving the static-delivery tests (system-http-server AC-15 gzip, AC-16 immutable caching) with no asset to serve. They passed locally and failed in CI. Extend the stub target to also stage assets/app-abc123.js: >=256 bytes and compressible so the handler's gzip path engages, containing console.log (the gzip test decodes and checks for it). Verified by wiping spa/ and regenerating via the target, then running the suite — AC-15/16/17 all pass.
remyluslosius
added a commit
that referenced
this pull request
Jun 17, 2026
…#587) Promote the changelog [Unreleased] section to [0.2.0-rc.8] Eyrie (2026-06-17) per docs/runbooks/RELEASING.md and bump packaging/version.env. - CHANGELOG: add the two fixes that merged after the section was first drafted (SPA gzip + immutable caching #582; expired-session redirect to /login #583), plus a migration note (0030 through 0036 land this cycle; the one-command upgrade applies them with a pre-upgrade backup). Fresh empty [Unreleased] retained. - README + install guide: current-version strings to 0.2.0-rc.8. specter check clean (108 specs); release-changelog test green; the 100% AC coverage gate is enforced green by go-ci on main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The single
openwatchbinary serves the embedded SPA directly (no NGINX), butnewSPAHandlershipped raw bytes with no compression and no cache headers. In production that means JS/CSS bundles download uncompressed, and the browser re-fetches every content-hashed asset on each navigation — the reported slowness.TLS and HTTP/2 are already handled by the binary (
ListenAndServeTLS, h2 auto-enabled over TLS), so the only thing an NGINX/Caddy front would have added is static-delivery optimization. This closes that gap in-process, so the deployment stays a single self-contained binary.What changed (
internal/server/spa.go)Accept-Encoding: gzip(withVary: Accept-Encoding), identity otherwise. Compression is computed once at startup and cached — never paid per request.assets/files →Cache-Control: public, max-age=31536000, immutable.index.htmlstaysno-cacheso a new deploy is seen immediately; an ETag makes revalidation a cheap 304./apiand the SSE stream are never gzip-wrapped.Tests / spec
system-http-server→ v1.1.0: adds C-11, AC-15 (gzip on/off + Vary + decodes to original), AC-16 (immutable assets, ETag→304, indexno-cache).specter checkclean;go vet/build clean.Not in scope
Does not touch auth/redirect. The "no redirect on session expiry" symptom is separate — the
401 → refresh-cookie → /loginflow already exists insrc/api/client.ts; top suspect is thatSecurecookies require HTTPS end-to-end. Can chase that next once you confirm HTTPS and whatrefresh-cookiereturns on expiry.