Skip to content

Add brotli/gzip compression for dynamic SSR and API responses#2409

Merged
gary149 merged 2 commits into
mainfrom
perf/compression-middleware
Jul 2, 2026
Merged

Add brotli/gzip compression for dynamic SSR and API responses#2409
gary149 merged 2 commits into
mainfrom
perf/compression-middleware

Conversation

@gary149

@gary149 gary149 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

Dynamic responses (SSR HTML and all API JSON) currently ship with no content encoding: the landing page document is ~318KB on the wire and gzips to ~33KB. Static assets are already precompressed by adapter-node, but nothing compresses what the SvelteKit handler generates at runtime.

This adds a small custom server (server.js) wrapping build/handler.js with polka and @polka/compression, following the pattern recommended by the SvelteKit adapter-node docs. The Dockerfile and entrypoint now run it instead of build/index.js.

Details worth calling out:

  • application/jsonl (token streaming) and text/event-stream (conversation updates) are explicitly excluded from compression. Both packages' default filters would match them, and compressing them buffers tokens inside the zlib transform instead of delivering them as produced.
  • Brotli level is pinned to 4. The package default (-1) silently resolves to quality 11, which is far too slow for dynamic responses.
  • Vary: Accept-Encoding is merged into response headers at write time. SvelteKit sets its own Vary: Accept on routes that have both a page and a co-located +server.ts, which would otherwise overwrite a header set up front by middleware (verified against the built manifest).
  • Graceful shutdown (SIGTERM drain with SHUTDOWN_TIMEOUT) and SOCKET_PATH/HOST/PORT handling are reimplemented, since a custom server does not inherit adapter-node's lifecycle behavior. The old -- --host --port CLI args in entrypoint.sh were dead code (the generated index.js only reads env vars).

Measured

Against a local production build with a real database:

Response Before After
Landing HTML 135.6KB 17.2KB (br)
Models API JSON 94.9KB 8.4KB (br)
Token stream untouched untouched, verified incremental (14 chunks over ~930ms, no Content-Encoding)

Verification

  • gzip and brotli bodies round-trip byte-identical to the uncompressed response
  • HEAD requests skip compression, conditional requests unaffected, ETags intact
  • Vary: Accept, Accept-Encoding present on page+endpoint routes (e.g. /conversation/[id])
  • Streaming exercised end to end through the real send pipeline with Accept-Encoding: gzip, br sent

Part of a performance series measured from a live profile of hf.co/chat. Each PR is file-disjoint and merges independently, in any order: #2409 (compression), #2410 (models payload), #2411 (markdown pipeline), #2412 (conversation switching), #2413 (stream update batching), #2414 (send handler DB), #2415 (sidebar hydration).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8167c33878

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server.js Outdated
const port = process.env.PORT ?? (path ? undefined : "3000");
const shutdownTimeout = parseInt(process.env.SHUTDOWN_TIMEOUT ?? "30", 10);

server.listen({ path, host, port }, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Omit port when listening on SOCKET_PATH

When SOCKET_PATH is configured, this call still passes a port property in the listen options (process.env.PORT if set, otherwise undefined). In Node, an options object with port: undefined is treated as a TCP listen on port 0 rather than a Unix-domain socket, so socket-based deployments bind a random TCP port while logging the socket path and never create the expected socket. Build the listen options without host/port whenever path is set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed empirically (a listen options object with host/port present binds a random TCP port even when path is set) and fixed in 11657b4: the socket path is now the only key in the listen options when SOCKET_PATH is configured. Verified end to end: the socket file is created and serves HTTP 200 over curl --unix-socket.

@gary149 gary149 merged commit 9a83f4f into main Jul 2, 2026
8 checks passed
@gary149 gary149 deleted the perf/compression-middleware branch July 2, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant