Precompress the bundle, validate the shell, and pass byte ranges on #386

Closed
opened 2026-09-16 17:54:30 +00:00 by jcoffey-dev · 0 comments
Owner

Summary

Four smaller server-side items from the performance review.

  • Precompressed assets.
    • A new scripts/precompress.mjs runs at the end of npm run build -w web. It writes .br (quality 11) and .gz (level 9) copies beside every compressible file over 1 KB.
    • static.ts serves the best copy the request's Accept-Encoding allows, honoring q=0, with Content-Encoding and Vary: Accept-Encoding. It ignores any copy older than its file.
    • Files with no copy, and requests that accept neither encoding, behave as before. The compress middleware already skips responses that carry Content-Encoding.
    • Before, the bundle was gzipped again for every request and Brotli was never offered.
  • ETags and 304s.
    • index.html carries a hash ETag, and every other static file a weak size-plus-mtime ETag.
    • A matching If-None-Match gets a 304. The shell, sw.js and the manifest are no-cache, so every load revalidates them, and before this each revalidation downloaded the whole file.
  • Byte ranges on attachment downloads.
    • /api/blob forwards a plain bytes= Range header to Stalwart, and relays a 206 with Content-Range, a 416, and Accept-Ranges.
    • A server that ignores Range answers 200 with the whole file, which is what happened before.
    • On the reader's own device a blob is now private, max-age=31536000, immutable, since a blob id names its content. Other devices still get no-store.
  • Upstream caches expire. sessionCache and infoCache in upstream.ts only dropped an entry on sign-out or a 401, not when a session simply expired. A timer now removes entries past their age.

Mock: the download endpoint answers single byte ranges, including 416, and advertises Accept-Ranges.

Not verified against a real server: whether Stalwart honors Range on its download endpoint. The code path is the same either way; only whether a 206 ever comes back depends on it.

Related issues

None.

Translations

Adds none.

Testing

  • New static-precompressed.test.ts:

    • Brotli is served when accepted, and gzip when Brotli has q=0;
    • identity is served when neither is accepted;
    • a file without copies is still compressed on the fly;
    • a stale copy is ignored;
    • / and /sw.js return 304 for their own ETag and 200 for a different one.
  • account.test.ts, through the mock:

    • a range request returns 206 with bytes 0-4/11, and hello;
    • a full request returns 200;
    • an out-of-range request returns 416;
    • a non-byte Range isn't forwarded;
    • after a simulated hour, the upstream caches are empty.
  • npm test -w server (249 tests) and tsc are clean.

  • Build: npm run build -w web precompressed 55 files, 2,694 KB down to 669 KB with Brotli.

  • Built server, live requests for the main chunk:

    Accept-Encoding Served as Size
    br, gzip Brotli 122 KB
    gzip gzip 144 KB
    none identity 460 KB

    A revalidation of / with its ETag returned 304.

Merged 2026-09-16 as coffey-labs/ihasmail@c63fd0dfe0

Rebuilt from: git history, session transcript.

## Summary Four smaller server-side items from the performance review. - **Precompressed assets.** - A new `scripts/precompress.mjs` runs at the end of `npm run build -w web`. It writes `.br` (quality 11) and `.gz` (level 9) copies beside every compressible file over 1 KB. - `static.ts` serves the best copy the request's `Accept-Encoding` allows, honoring `q=0`, with `Content-Encoding` and `Vary: Accept-Encoding`. It ignores any copy older than its file. - Files with no copy, and requests that accept neither encoding, behave as before. The compress middleware already skips responses that carry `Content-Encoding`. - Before, the bundle was gzipped again for every request and Brotli was never offered. - **ETags and 304s.** - `index.html` carries a hash ETag, and every other static file a weak size-plus-mtime ETag. - A matching `If-None-Match` gets a 304. The shell, `sw.js` and the manifest are `no-cache`, so every load revalidates them, and before this each revalidation downloaded the whole file. - **Byte ranges on attachment downloads.** - `/api/blob` forwards a plain `bytes=` `Range` header to Stalwart, and relays a 206 with `Content-Range`, a 416, and `Accept-Ranges`. - A server that ignores `Range` answers 200 with the whole file, which is what happened before. - On the reader's own device a blob is now `private, max-age=31536000, immutable`, since a blob id names its content. Other devices still get `no-store`. - **Upstream caches expire.** `sessionCache` and `infoCache` in `upstream.ts` only dropped an entry on sign-out or a 401, not when a session simply expired. A timer now removes entries past their age. **Mock:** the download endpoint answers single byte ranges, including 416, and advertises `Accept-Ranges`. **Not verified against a real server:** whether Stalwart honors `Range` on its download endpoint. The code path is the same either way; only whether a 206 ever comes back depends on it. ## Related issues None. ## Translations Adds none. ## Testing - **New `static-precompressed.test.ts`:** - Brotli is served when accepted, and gzip when Brotli has `q=0`; - identity is served when neither is accepted; - a file without copies is still compressed on the fly; - a stale copy is ignored; - `/` and `/sw.js` return 304 for their own ETag and 200 for a different one. - **`account.test.ts`, through the mock:** - a range request returns 206 with `bytes 0-4/11`, and `hello`; - a full request returns 200; - an out-of-range request returns 416; - a non-byte `Range` isn't forwarded; - after a simulated hour, the upstream caches are empty. - `npm test -w server` (249 tests) and `tsc` are clean. - **Build:** `npm run build -w web` precompressed 55 files, 2,694 KB down to 669 KB with Brotli. - **Built server, live requests** for the main chunk: | `Accept-Encoding` | Served as | Size | |---|---|---| | `br, gzip` | Brotli | 122 KB | | `gzip` | gzip | 144 KB | | none | identity | 460 KB | A revalidation of `/` with its ETag returned 304. **Merged** 2026-09-16 as coffey-labs/ihasmail@c63fd0dfe0d6 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.