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.
## 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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Four smaller server-side items from the performance review.
scripts/precompress.mjsruns at the end ofnpm run build -w web. It writes.br(quality 11) and.gz(level 9) copies beside every compressible file over 1 KB.static.tsserves the best copy the request'sAccept-Encodingallows, honoringq=0, withContent-EncodingandVary: Accept-Encoding. It ignores any copy older than its file.Content-Encoding.index.htmlcarries a hash ETag, and every other static file a weak size-plus-mtime ETag.If-None-Matchgets a 304. The shell,sw.jsand the manifest areno-cache, so every load revalidates them, and before this each revalidation downloaded the whole file./api/blobforwards a plainbytes=Rangeheader to Stalwart, and relays a 206 withContent-Range, a 416, andAccept-Ranges.Rangeanswers 200 with the whole file, which is what happened before.private, max-age=31536000, immutable, since a blob id names its content. Other devices still getno-store.sessionCacheandinfoCacheinupstream.tsonly 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
Rangeon 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:q=0;/and/sw.jsreturn 304 for their own ETag and 200 for a different one.account.test.ts, through the mock:bytes 0-4/11, andhello;Rangeisn't forwarded;npm test -w server(249 tests) andtscare clean.Build:
npm run build -w webprecompressed 55 files, 2,694 KB down to 669 KB with Brotli.Built server, live requests for the main chunk:
Accept-Encodingbr, gzipgzipA revalidation of
/with its ETag returned 304.Merged 2026-09-16 as coffey-labs/ihasmail@c63fd0dfe0
Rebuilt from: git history, session transcript.