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.
48 lines
1.8 KiB
JavaScript
48 lines
1.8 KiB
JavaScript
#!/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`);
|