perf: build-time precompression + startup metadata cache for static serving#641
Open
NathanDrake2406 wants to merge 6 commits intocloudflare:mainfrom
Open
perf: build-time precompression + startup metadata cache for static serving#641NathanDrake2406 wants to merge 6 commits intocloudflare:mainfrom
NathanDrake2406 wants to merge 6 commits intocloudflare:mainfrom
Conversation
Generate .br (brotli q11), .gz (gzip l9), and .zst (zstandard l19) files alongside compressible assets in dist/client/assets/ during vinext build. These are served directly by the production server, eliminating per-request compression overhead for immutable build output. Only targets assets/ (hashed, immutable). Public directory files still use on-the-fly compression since they may change between deploys.
StaticFileCache walks dist/client/ once at server boot and caches: - File metadata (path, size, content-type, cache-control, etag) - Pre-computed response headers per variant (original, br, gz, zst) - In-memory buffers for small files (< 64KB) for res.end(buffer) - Precompressed variant paths and sizes Per-request serving is Map.get() + res.end(buffer) with zero filesystem calls, zero object allocation, and zero header construction. Modeled after sirv's production mode but with in-memory buffering for small files which eliminates createReadStream fd overhead.
Refactor tryServeStatic to use StaticFileCache for the hot path: - Pre-computed response headers (zero object allocation per request) - In-memory buffer serving for small precompressed files - 304 Not Modified via ETag + If-None-Match - HEAD request optimization (headers only, no body) - Zstandard serving (zstd > br > gzip > original fallback chain) - Async filesystem fallback for non-cached files (replaces blocking existsSync + statSync) - Skip decodeURIComponent for clean URLs (no % in path) Wire StaticFileCache.create() into both startAppRouterServer and startPagesRouterServer at startup. Integrate precompressAssets() into the vinext build pipeline. CONTENT_TYPES is now a single source of truth exported from static-file-cache.ts (was duplicated in prod-server.ts).
eca889d to
1798289
Compare
commit: |
1798289 to
2fd407c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR implements a static file serving method that is better than Next.js 16.2.
Bugs fixed
existsSync+statSyncran on every static file request, blocking SSR responses behind synchronous filesystem calls. Now zero FS calls per request (metadata cached at startup).If-None-Matchsupport. Every repeat visit re-downloaded every asset in full. Now returns 304 (200 bytes) when the browser already has the asset.Optimizations
.br(brotli q11),.gz(gzip l9),.zst(zstd l19) generated at build time. Zero compression CPU per request.res.end(buffer)instead ofcreateReadStream().pipe(), eliminating file descriptor overhead..zstassets. 3-5x faster client-side decompression than brotli (Chrome 123+, Firefox 126+).fsp.stat()instead of blockingstatSync.Real-world impact
Benchmark
Averaged over 10 runs. 5000 requests per run, 10 concurrent connections, 5 × 50KB JS bundles. vinext NEW won every single run.
Throughput (Accept-Encoding: zstd, br, gzip)
sirv3.0.2send1.2.1304 Not Modified (conditional request with matching ETag)
sirv3.0.2Transfer size (5000 requests)
sirvsendvinext serves zstd when accepted (trades ~12% larger output for 3-5x faster client-side decompression). When serving brotli, vinext produces the smallest output of all — brotli q11 (93 B) vs old vinext's q4 (94 B).
Feature comparison
sirvsendstat)existsSync+statSync)Why vinext beats sirv
sirv always uses
createReadStream().pipe()— even for a 100-byte precompressed file, this opens a file descriptor, creates a ReadStream, sets up pipe plumbing, reads 100 bytes, and closes the fd. vinext buffers small files (< 64KB) in memory at startup and serves them withres.end(buffer)— a single write to the socket. For large files, it still streams. Best of both worlds.Architecture
New modules:
src/build/precompress.ts— build-time compression (brotli q11, gzip l9, zstd l19)src/server/static-file-cache.ts— startup cache with pre-computed headers + in-memory buffersprod-server.tsrefactoredtryServeStatic— async, cache-aware, precompressed variant servingTest plan
precompressAssets: generates .br/.gz/.zst, skips small files, skips non-compressible types, handles missing dirs, idempotent, correct decompression (13 tests)StaticFileCache: scan, lookup, HTML fallbacks, .vite/ blocking, etag, variant detection, nested dirs (20 tests)tryServeStatic: precompressed serving (zstd/br/gz fallback chain), 304 Not Modified, HEAD, Content-Length, Vary, extra headers, traversal protection (19 tests)