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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user