Precompress the bundle, validate the shell, and pass byte ranges on
The web build now writes a Brotli and a gzip copy of each compressible file, and the static handler serves the best one the browser accepts. The bundle was gzipped again for every request and Brotli was never offered; the main chunk is 122 KB with Brotli against 144 KB gzipped. index.html and every static file carry an ETag, and a matching If-None-Match gets a 304. The shell and the worker are revalidated on every load and were downloaded whole each time. Attachment downloads pass a plain byte Range to Stalwart and relay a 206, so a PDF viewer or a video element can read in pieces where the server allows it. On the reader's own device a blob is cached as immutable, since its id names its content. The upstream session and account-info caches drop entries past their age on a timer; they lost an entry only on sign-out or refusal, not when a session expired. The mock answers byte ranges.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Write a Brotli and a gzip copy beside every compressible file in a web build.
|
||||
*
|
||||
* The server used to gzip the bundle again on every request that asked for it,
|
||||
* at a level chosen for speed. These are made once, at the level chosen for
|
||||
* size, and `server/src/static.ts` hands one out when the browser accepts it.
|
||||
* Brotli at 11 is about 15% smaller than gzip for this bundle, and too slow to
|
||||
* do per request, which is why it was never offered.
|
||||
*
|
||||
* node scripts/precompress.mjs web/dist
|
||||
*/
|
||||
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join, extname } from "node:path";
|
||||
import { brotliCompressSync, constants, gzipSync } from "node:zlib";
|
||||
|
||||
const COMPRESSIBLE = new Set([".js", ".mjs", ".css", ".html", ".svg", ".json", ".webmanifest", ".txt", ".wasm"]);
|
||||
// Below this, the encoding costs more than it saves.
|
||||
const MIN_BYTES = 1024;
|
||||
|
||||
function* files(dir) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, entry.name);
|
||||
if (entry.isDirectory()) yield* files(p);
|
||||
else yield p;
|
||||
}
|
||||
}
|
||||
|
||||
const root = process.argv[2];
|
||||
if (!root) {
|
||||
console.error("usage: precompress.mjs <dir>");
|
||||
process.exit(2);
|
||||
}
|
||||
let count = 0;
|
||||
let before = 0;
|
||||
let after = 0;
|
||||
for (const p of files(root)) {
|
||||
if (!COMPRESSIBLE.has(extname(p)) || statSync(p).size < MIN_BYTES) continue;
|
||||
const data = readFileSync(p);
|
||||
const br = brotliCompressSync(data, { params: { [constants.BROTLI_PARAM_QUALITY]: 11, [constants.BROTLI_PARAM_SIZE_HINT]: data.length } });
|
||||
writeFileSync(`${p}.br`, br);
|
||||
writeFileSync(`${p}.gz`, gzipSync(data, { level: 9 }));
|
||||
count++;
|
||||
before += data.length;
|
||||
after += br.length;
|
||||
}
|
||||
console.log(`precompressed ${count} files: ${(before / 1024).toFixed(0)} KB -> ${(after / 1024).toFixed(0)} KB brotli`);
|
||||
Reference in New Issue
Block a user