Idea
staticMiddleware() 0.12 fixed the immediate Brotli-11 CPU amplification problem by using quality 4 on the request path, bounding the file sizes it compresses, and supporting precompressed .br/.gz variants. Those are good safeguards.
There is still avoidable steady-state work in the default configuration, though: encodings defaults to false and compress defaults to true, so every unconditional 200 for the same unchanged compressible file is encoded again at quality 4. A conditional request can avoid this with a 304, and a build-produced variant is the best case, but neither helps a new client/shared-cache miss when an integration doesn't ship variants.
Would a bounded, opt-in compressed-representation cache (or a cache adapter hook) be in scope for srvx/static?
This follow-up came out of a real high-CPU incident in a Vike production deployment. The worst confirmed incident path was the older srvx@0.11 Brotli-q11 behavior, tracked in universal-deploy#34; I am not claiming the safer 0.12 q4 path caused that incident. The production fallout is why eliminating repeated encoding, rather than only reducing its quality, matters here.
Prior art: Shrink Ray's two-stage compression
@nitedani/shrink-ray-current has used a small and effective pattern for years:
- A cache miss streams a fast encoding to the first requester (Brotli quality 4).
- The compressed bytes are stored in a process-scoped, size-bounded LRU, keyed by content coding, URL, and ETag.
- The cached entry is asynchronously re-encoded at maximum quality (Brotli 11; the current fork uses zlib level 9 for gzip/deflate).
- Warm requests reuse the compressed bytes and perform no new compression.
Relevant implementation:
On Node 24.14.0, with a real 126,324-byte minified JS file:
| response |
Brotli work |
wire bytes |
| first/cold |
q4 |
45,117 |
| later/warm |
none; cached q11 |
39,489 |
Instrumenting createBrotliCompress() showed [4, 11] for the cold request (fast response plus background promotion) and no additional calls for two warm requests. In the same raw Node middleware harness (240 requests, concurrency 8, three runs), the median was 951.5 req/s with the cache disabled versus 1,982.0 req/s with the q11 cache warm. Absolute rates are machine-specific; the important property is that warm requests do not spend compression CPU and serve the smaller representation.
A srvx implementation can improve on the old middleware model
I wouldn't copy Shrink Ray literally:
- Its generic Express interception happens after the downstream handler starts. A cache hit still executes the origin handler and discards the identity body. An integrated static server can check the representation cache after validating/statting the file but before reading its body.
- It prevents duplicate insertion, not duplicate cold work. Eight simultaneous cold requests in my probe created eight q4 encoders and one q11 promotion.
srvx can single-flight per representation.
- Background maximum-quality encodes should be concurrency-limited or worker-backed so a burst of distinct cold assets cannot occupy the same libuv pool used by filesystem operations.
- Its
req.url key lets query-string cardinality create duplicate entries. A static server can key the canonical resolved file identity instead.
A possible native shape:
- Keep precompressed files as the first choice; build-time compression is still the cheapest and best result.
- For the on-the-fly fallback, key a bounded LRU by canonical source identity/version plus negotiated content coding (for example, the same metadata used for the representation ETag).
- Coalesce concurrent misses for that key.
- Stream q4 to the first requester while retaining the output; serve it directly on subsequent hits.
- Optionally promote the entry to q11 in a bounded background queue.
- Preserve the existing validator, abort, backpressure, range, and
Vary behavior; set the encoded Content-Length on cache hits.
This could be disabled by default, use a modest byte limit, or be exposed as a cache hook so framework integrations can choose the policy.
Cross-repository context
The default self-hosted Vike +server path currently lands on srvx@0.11.22, so it still has the much worse Brotli-11-on-every-response behavior. The immediate dependency fix is tracked in:
After that upgrade, Vike/Universal Deploy still passes only { dir } to staticMiddleware, doesn't produce precompressed variants, and exposes prod.static only as boolean | string. Thus the current integration cannot opt into encodings: true, compress: false, maxAge, or immutable even though srvx supports them. That integration/build concern is separate, but it is what makes the on-the-fly fallback the effective production default there.
The 0.12 static work already calls out that repeated on-the-fly output isn't cached and recommends precompression. This issue is about making the generic runtime fallback efficient when there is no cooperating build step, using the same fast-first / best-later idea without the limitations of response interception.
Related ownership layers:
Idea
staticMiddleware()0.12 fixed the immediate Brotli-11 CPU amplification problem by using quality 4 on the request path, bounding the file sizes it compresses, and supporting precompressed.br/.gzvariants. Those are good safeguards.There is still avoidable steady-state work in the default configuration, though:
encodingsdefaults tofalseandcompressdefaults totrue, so every unconditional200for the same unchanged compressible file is encoded again at quality 4. A conditional request can avoid this with a304, and a build-produced variant is the best case, but neither helps a new client/shared-cache miss when an integration doesn't ship variants.Would a bounded, opt-in compressed-representation cache (or a cache adapter hook) be in scope for
srvx/static?This follow-up came out of a real high-CPU incident in a Vike production deployment. The worst confirmed incident path was the older
srvx@0.11Brotli-q11 behavior, tracked in universal-deploy#34; I am not claiming the safer 0.12 q4 path caused that incident. The production fallout is why eliminating repeated encoding, rather than only reducing its quality, matters here.Prior art: Shrink Ray's two-stage compression
@nitedani/shrink-ray-currenthas used a small and effective pattern for years:Relevant implementation:
On Node 24.14.0, with a real 126,324-byte minified JS file:
Instrumenting
createBrotliCompress()showed[4, 11]for the cold request (fast response plus background promotion) and no additional calls for two warm requests. In the same raw Node middleware harness (240 requests, concurrency 8, three runs), the median was 951.5 req/s with the cache disabled versus 1,982.0 req/s with the q11 cache warm. Absolute rates are machine-specific; the important property is that warm requests do not spend compression CPU and serve the smaller representation.A
srvximplementation can improve on the old middleware modelI wouldn't copy Shrink Ray literally:
srvxcan single-flight per representation.req.urlkey lets query-string cardinality create duplicate entries. A static server can key the canonical resolved file identity instead.A possible native shape:
Varybehavior; set the encodedContent-Lengthon cache hits.This could be disabled by default, use a modest byte limit, or be exposed as a cache hook so framework integrations can choose the policy.
Cross-repository context
The default self-hosted Vike
+serverpath currently lands onsrvx@0.11.22, so it still has the much worse Brotli-11-on-every-response behavior. The immediate dependency fix is tracked in:After that upgrade, Vike/Universal Deploy still passes only
{ dir }tostaticMiddleware, doesn't produce precompressed variants, and exposesprod.staticonly asboolean | string. Thus the current integration cannot opt intoencodings: true,compress: false,maxAge, orimmutableeven thoughsrvxsupports them. That integration/build concern is separate, but it is what makes the on-the-fly fallback the effective production default there.The 0.12 static work already calls out that repeated on-the-fly output isn't cached and recommends precompression. This issue is about making the generic runtime fallback efficient when there is no cooperating build step, using the same fast-first / best-later idea without the limitations of response interception.
Related ownership layers: