Skip to content

Commit 5a26cda

Browse files
committed
use brotli
1 parent 0f3488a commit 5a26cda

8 files changed

Lines changed: 183 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ WebAssembly threads, service workers, and cross-origin isolation require a secur
245245

246246
If assets are hosted on another origin, that origin must allow CORS and send `Cross-Origin-Resource-Policy: cross-origin` or an equivalent policy accepted by the embedding page.
247247

248+
Production builds retain every uncompressed runtime file and also create Brotli
249+
sidecars for everything under `wasm/` and for other JavaScript files of at least
250+
128 KiB. Configure the web server to serve `file.br` for a request to `file` when
251+
the client advertises `Accept-Encoding: br`, with the original content type,
252+
`Content-Encoding: br`, and `Vary: Accept-Encoding`. Vite preview does this
253+
automatically. The uncompressed files remain the source of truth and the fallback
254+
for hosts that do not support precompressed sidecars.
255+
248256
## Build and test
249257

250258
Installing dependencies downloads the latest verified prebuilt clangd artifacts

‎public/_headers‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,18 @@
44

55
/wasm/*.wasm
66
Content-Type: application/wasm
7+
8+
/assets/*.js.br
9+
Content-Encoding: br
10+
Content-Type: text/javascript
11+
Vary: Accept-Encoding
12+
13+
/wasm/*.js.br
14+
Content-Encoding: br
15+
Content-Type: text/javascript
16+
Vary: Accept-Encoding
17+
18+
/wasm/*.wasm.br
19+
Content-Encoding: br
20+
Content-Type: application/wasm
21+
Vary: Accept-Encoding

‎public/coi-serviceworker.js‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@ self.addEventListener("activate", (event) => {
55
});
66
self.addEventListener("fetch", (event) => {
77
if (new URL(event.request.url).origin !== self.location.origin) return;
8-
if (
9-
event.request.cache === "only-if-cached" &&
10-
event.request.mode !== "same-origin"
11-
)
12-
return;
8+
// COOP and COEP are document policies. Same-origin subresources already
9+
// satisfy COEP, so leaving them alone also avoids reload-time cancellation
10+
// noise for stylesheets and modules.
11+
if (event.request.mode !== "navigate") return;
1312
event.respondWith(
1413
(async () => {
15-
const response = await fetch(event.request);
14+
let response;
15+
try {
16+
response = await fetch(event.request);
17+
} catch (_) {
18+
// A navigation/reload can cancel in-flight subresource requests. Resolve
19+
// respondWith instead of turning that ordinary cancellation into an
20+
// unhandled service-worker rejection in the browser console.
21+
return Response.error();
22+
}
1623
if (response.status === 0) return response;
1724
const headers = new Headers(response.headers);
1825
headers.set("Cross-Origin-Embedder-Policy", "require-corp");

‎scripts/build-package.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ await run(
4646
],
4747
{ label: "copy-runtime-assets" },
4848
);
49+
50+
await run(
51+
process.execPath,
52+
[
53+
path.join(projectRoot, "scripts", "compress-runtime-assets.mjs"),
54+
`--outdir=${outdir}`,
55+
],
56+
{ label: "compress-runtime-assets" },
57+
);

‎scripts/clangd/build.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ wasi_include="$CLANGD_BUILD_ROOT/wasi-sysroot-$WASI_SDK_VERSION/include"
146146
cp -R "build/lib/clang/$LLVM_MAJOR/include/." "$wasi_include/"
147147

148148
## Re-link the production build with the sysroot embedded.
149-
configure_clangd "-pthread -s ENVIRONMENT=worker -s NO_INVOKE_RUN -s EXIT_RUNTIME -s INITIAL_MEMORY=256MB -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=2GB -s STACK_SIZE=256kB -s EXPORTED_RUNTIME_METHODS=FS,callMain -s MODULARIZE -s EXPORT_ES6 -s WASM_BIGINT -s ASSERTIONS -s ASYNCIFY -s PTHREAD_POOL_SIZE='Math.max(navigator.hardwareConcurrency, 8)' --embed-file=$wasi_include@/usr/include"
149+
configure_clangd "-pthread -s ENVIRONMENT=worker -s NO_INVOKE_RUN -s EXIT_RUNTIME -s INITIAL_MEMORY=256MB -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=2GB -s STACK_SIZE=256kB -s EXPORTED_RUNTIME_METHODS=FS,callMain -s MODULARIZE -s EXPORT_ES6 -s WASM_BIGINT -s ASSERTIONS -s ASYNCIFY -s PTHREAD_POOL_SIZE='Math.max(navigator.hardwareConcurrency, 4)' --embed-file=$wasi_include@/usr/include"
150150
cmake --build build --target clangd
151151

152152
mkdir -p "$clangd_wasm_dir"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { brotliCompress } from "node:zlib";
2+
import { promisify } from "node:util";
3+
import { readdir, readFile, writeFile } from "node:fs/promises";
4+
import path from "node:path";
5+
import { constants as zlibConstants } from "node:zlib";
6+
import { parseArguments, projectChild } from "./lib/project.mjs";
7+
8+
const compress = promisify(brotliCompress);
9+
const minimumJavaScriptSize = 128 * 1024;
10+
const { options } = parseArguments();
11+
const outdir = options.get("outdir") ?? "dist";
12+
const outputDirectory = projectChild(outdir, "output directory");
13+
14+
async function visit(directory) {
15+
const entries = await readdir(directory, { withFileTypes: true });
16+
await Promise.all(
17+
entries.map(async (entry) => {
18+
const filename = path.join(directory, entry.name);
19+
if (entry.isDirectory()) return visit(filename);
20+
if (!entry.isFile() || filename.endsWith(".br")) return;
21+
22+
const isWasm = filename.endsWith(".wasm");
23+
const isJavaScript = filename.endsWith(".js");
24+
if (!isWasm && !isJavaScript) return;
25+
26+
const contents = await readFile(filename);
27+
const relativePath = path.relative(outputDirectory, filename);
28+
const isWasmRuntime = relativePath.split(path.sep)[0] === "wasm";
29+
if (
30+
isJavaScript &&
31+
!isWasmRuntime &&
32+
contents.length < minimumJavaScriptSize
33+
)
34+
return;
35+
36+
const compressed = await compress(contents, {
37+
params: {
38+
[zlibConstants.BROTLI_PARAM_QUALITY]: 11,
39+
[zlibConstants.BROTLI_PARAM_MODE]: isWasm
40+
? zlibConstants.BROTLI_MODE_GENERIC
41+
: zlibConstants.BROTLI_MODE_TEXT,
42+
},
43+
});
44+
await writeFile(`${filename}.br`, compressed);
45+
}),
46+
);
47+
}
48+
49+
await visit(outputDirectory);

‎src/clangd/worker.ts‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,38 @@ self.addEventListener("message", (event) => {
3434
else pendingWorkspaceFiles.push(files);
3535
});
3636

37-
async function download(url: URL): Promise<Uint8Array> {
37+
function unsignedLeb128(value: number): number[] {
38+
const bytes = [];
39+
do {
40+
let byte = value & 0x7f;
41+
value >>>= 7;
42+
if (value) byte |= 0x80;
43+
bytes.push(byte);
44+
} while (value);
45+
return bytes;
46+
}
47+
48+
function emptySourceMapSection(): Uint8Array {
49+
const encoder = new TextEncoder();
50+
const name = encoder.encode("sourceMappingURL");
51+
const url = encoder.encode(
52+
'data:application/json,{"version":3,"sources":[],"names":[],"mappings":""}',
53+
);
54+
const contents = new Uint8Array([
55+
...unsignedLeb128(name.length),
56+
...name,
57+
...unsignedLeb128(url.length),
58+
...url,
59+
]);
60+
return new Uint8Array([0, ...unsignedLeb128(contents.length), ...contents]);
61+
}
62+
63+
async function download(url: URL, suffix?: Uint8Array): Promise<Uint8Array> {
3864
const response = await fetch(url);
3965
if (!response.ok || !response.body) {
4066
throw new Error(`Unable to download clangd.wasm (${response.status})`);
4167
}
4268

43-
const headerSize =
44-
Number(response.headers.get("content-length")) || undefined;
4569
const chunks: Uint8Array[] = [];
4670
let loaded = 0;
4771
const reader = response.body.getReader();
@@ -51,16 +75,17 @@ async function download(url: URL): Promise<Uint8Array> {
5175
if (value) {
5276
chunks.push(value);
5377
loaded += value.length;
54-
self.postMessage({ type: "progress", loaded, total: headerSize });
78+
self.postMessage({ type: "progress", loaded });
5579
}
5680
}
5781

58-
const bytes = new Uint8Array(loaded);
82+
const bytes = new Uint8Array(loaded + (suffix?.length ?? 0));
5983
let offset = 0;
6084
for (const chunk of chunks) {
6185
bytes.set(chunk, offset);
6286
offset += chunk.length;
6387
}
88+
if (suffix) bytes.set(suffix, loaded);
6489
return bytes;
6590
}
6691

@@ -76,7 +101,10 @@ async function start(): Promise<void> {
76101
const modulePromise = import(/* @vite-ignore */ clangdJsUrl.href) as Promise<{
77102
default: ClangdFactory;
78103
}>;
79-
const wasmBinary = await download(clangdWasmUrl);
104+
// Firefox asks every instantiated Wasm module for a source-map URL. Give it
105+
// a valid, empty map instead of letting DevTools try to resolve `null` against
106+
// the synthetic URL assigned to an ArrayBuffer-instantiated module.
107+
const wasmBinary = await download(clangdWasmUrl, emptySourceMapSection());
80108
self.postMessage({ type: "starting" });
81109

82110
const { default: Clangd } = await modulePromise;

‎vite.config.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,66 @@
11
import { defineConfig } from "vite";
22
import importMetaUrlPlugin from "@codingame/esbuild-import-meta-url-plugin";
3+
import { existsSync } from "node:fs";
4+
import path from "node:path";
35

46
const crossOriginIsolationHeaders = {
57
"Cross-Origin-Embedder-Policy": "require-corp",
68
"Cross-Origin-Opener-Policy": "same-origin",
79
};
810

11+
const contentTypes = new Map([
12+
[".js", "text/javascript"],
13+
[".wasm", "application/wasm"],
14+
]);
15+
16+
function precompressedPreview() {
17+
return {
18+
name: "precompressed-preview",
19+
configurePreviewServer(server) {
20+
server.middlewares.use((request, response, next) => {
21+
if (
22+
!request.url ||
23+
!/\bbr\b/.test(request.headers["accept-encoding"] ?? "")
24+
) {
25+
return next();
26+
}
27+
28+
const pathname = decodeURIComponent(
29+
new URL(request.url, "http://localhost").pathname,
30+
);
31+
const extension = path.extname(pathname);
32+
const contentType = contentTypes.get(extension);
33+
if (!contentType) return next();
34+
35+
const relativePath = pathname.replace(/^\/+/, "");
36+
const candidate = path.resolve(
37+
server.config.root,
38+
server.config.build.outDir,
39+
`${relativePath}.br`,
40+
);
41+
const outputRoot = path.resolve(
42+
server.config.root,
43+
server.config.build.outDir,
44+
);
45+
if (
46+
!candidate.startsWith(`${outputRoot}${path.sep}`) ||
47+
!existsSync(candidate)
48+
) {
49+
return next();
50+
}
51+
52+
request.url = `${pathname}.br`;
53+
response.setHeader("Content-Encoding", "br");
54+
response.setHeader("Content-Type", contentType);
55+
response.setHeader("Vary", "Accept-Encoding");
56+
next();
57+
});
58+
},
59+
};
60+
}
61+
962
export default defineConfig({
63+
plugins: [precompressedPreview()],
1064
base: "./",
1165
define: {
1266
__CLANGD_WASM_BASE__: JSON.stringify(process.env.CLANGD_WASM_BASE ?? ""),

0 commit comments

Comments
 (0)