Skip to content

perf(server): gzip + immutable caching for the embedded SPA#582

Merged
remyluslosius merged 4 commits into
mainfrom
feat/spa-gzip-cache
Jun 17, 2026
Merged

perf(server): gzip + immutable caching for the embedded SPA#582
remyluslosius merged 4 commits into
mainfrom
feat/spa-gzip-cache

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Problem

The single openwatch binary serves the embedded SPA directly (no NGINX), but newSPAHandler shipped 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)

  • gzip compressible assets when the client sends Accept-Encoding: gzip (with Vary: Accept-Encoding), identity otherwise. Compression is computed once at startup and cached — never paid per request.
  • Immutable caching for Vite's content-hashed assets/ files → Cache-Control: public, max-age=31536000, immutable.
  • index.html stays no-cache so a new deploy is seen immediately; an ETag makes revalidation a cheap 304.
  • Serving is a precomputed-table lookup — no per-request filesystem access, no path-traversal surface. Scoped to the SPA handler only: /api and the SSE stream are never gzip-wrapped.

Tests / spec

  • system-http-serverv1.1.0: adds C-11, AC-15 (gzip on/off + Vary + decodes to original), AC-16 (immutable assets, ETag→304, index no-cache).
  • Existing SPA tests (AC-12/13/14) green; specter check clean; 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 → /login flow already exists in src/api/client.ts; top suspect is that Secure cookies require HTTPS end-to-end. Can chase that next once you confirm HTTPS and what refresh-cookie returns on expiry.

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).
# 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 remyluslosius merged commit 4ecbf90 into main Jun 17, 2026
13 checks passed
@remyluslosius remyluslosius deleted the feat/spa-gzip-cache branch June 17, 2026 03:53
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant