perf(delivery): pre-compress static + cache headers + compress API responses (#346)

Cold load served ALL static + API responses uncompressed and without cache
headers (~3.7MB over the wire). Delivery only — feature behavior unchanged; no
DB/API-contract/MCP changes.

- apps/client/vite.config.ts: vite-plugin-compression2 emits .br + .gz next to
  each built asset (excludes index.html, which the server rewrites at boot with
  window.CONFIG — a precompressed copy would go stale). Build emits 187 .br /
  175 .gz under dist/assets.
- static.module.ts: @fastify/static `preCompressed: true` serves the .br/.gz
  neighbour; `setHeaders` sets `immutable` ONLY for content-hashed /assets/*,
  `no-cache` for index.html, and leaves non-hashed files (locales, vad, icons,
  manifest) on default etag/last-modified revalidation.
- main.ts: @fastify/compress (threshold 1024) compresses dynamic API JSON + the
  rewritten share-SEO HTML. SSE is safe on two counts: `text/event-stream` is not
  mime-db-compressible (allowlist skips it) AND the AI-chat stream hijacks the raw
  socket (pipeUIMessageStreamToResponse -> res.raw), bypassing the Fastify onSend
  lifecycle entirely. No double-compression with preCompressed static (compress
  skips already-Content-Encoding'd responses).
- docker-compose.yml: comment recommending an optional HTTP/2 + brotli reverse
  proxy (not required).

Deps: apps/client vite-plugin-compression2 2.5.3 (dev), apps/server
@fastify/compress 9.0.0 (matches fastify 5.8.5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
agent_coder
2026-07-04 21:29:54 +03:00
committed by agent_coder
parent 3085ec1b50
commit 6ee814b7f3
7 changed files with 183 additions and 3 deletions
@@ -72,6 +72,32 @@ export class StaticModule implements OnModuleInit {
await app.register(fastifyStatic, {
root: clientDistPath,
wildcard: false,
// Serve the build-time .br/.gz neighbour when the client accepts it
// (see vite-plugin-compression2 in apps/client/vite.config.ts).
preCompressed: true,
setHeaders: (res, filePath) => {
// Content-hashed files under /assets/ never change for a given URL,
// so they can be cached forever and skip revalidation entirely.
if (filePath.includes('/assets/')) {
res.setHeader(
'cache-control',
'public, max-age=31536000, immutable',
);
return;
}
// index.html is rewritten at boot (window.CONFIG injection) and on
// every deploy — it must be revalidated on every load.
if (filePath.endsWith('index.html')) {
res.setHeader(
'cache-control',
'no-cache, no-store, must-revalidate',
);
return;
}
// Everything else (locales, vad, icons, manifest) is NOT content-hashed
// and changes between deploys, so it keeps @fastify/static's default
// etag/last-modified revalidation — do NOT mark it immutable.
},
});
app.get(RENDER_PATH, (req: any, res: any) => {