Make pages load fast on mobile

- Compress responses: page scripts and styles once (brotli or gzip, in
  memory) and cached immutable; pages and API JSON gzipped as they stream
- Service worker serves content-named assets cache-first
- Load the models modal's data when it opens (cached in localStorage)
  instead of shipping the model catalog in every conversation page
- Lazy-load the Tiptap composer and memoize thread messages
- Import lucide icons directly; twui 0.1.4 drops by-name icons from Button
- bun run start builds the pages minified before serving

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-20 13:41:45 +01:00
co-authored by Claude Opus 5.5
parent 3b355d68bc
commit c1a94fc9a5
36 changed files with 840 additions and 147 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test";
import isCompressibleType from "./is-compressible-type";
describe("isCompressibleType", () => {
test("takes text, scripts, JSON and SVG", () => {
expect(isCompressibleType({ content_type: "text/html; charset=utf-8" })).toBe(true);
expect(isCompressibleType({ content_type: "text/javascript;charset=utf-8" })).toBe(true);
expect(isCompressibleType({ content_type: "text/css" })).toBe(true);
expect(isCompressibleType({ content_type: "application/json" })).toBe(true);
expect(isCompressibleType({ content_type: "application/javascript" })).toBe(true);
expect(isCompressibleType({ content_type: "application/manifest+json" })).toBe(true);
expect(isCompressibleType({ content_type: "image/svg+xml" })).toBe(true);
});
test("leaves media, binaries and streams", () => {
expect(isCompressibleType({ content_type: "text/event-stream" })).toBe(false);
expect(isCompressibleType({ content_type: "audio/mpeg" })).toBe(false);
expect(isCompressibleType({ content_type: "image/png" })).toBe(false);
expect(isCompressibleType({ content_type: "application/octet-stream" })).toBe(false);
expect(isCompressibleType({ content_type: "font/woff2" })).toBe(false);
expect(isCompressibleType({ content_type: null })).toBe(false);
});
});
+18
View File
@@ -0,0 +1,18 @@
type Params = {
content_type?: string | null;
};
/**
* Whether a response of this content type is text worth compressing:
* pages, scripts, styles, JSON, SVG and the like. Media, fonts and
* streams (`text/event-stream`) are left as they are.
*/
export default function isCompressibleType({ content_type }: Params): boolean {
const type = content_type?.split(";")[0]?.trim().toLowerCase();
if (!type || type == "text/event-stream") return false;
if (type.startsWith("text/")) return true;
return /^application\/(javascript|json|manifest\+json|xml|[\w.-]+\+json|[\w.-]+\+xml)$/.test(type) ||
type == "image/svg+xml";
}
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, test } from "bun:test";
import isHashedAssetPath from "./is-hashed-asset-path";
describe("isHashedAssetPath", () => {
test("matches content-named page scripts and styles", () => {
expect(isHashedAssetPath({ pathname: "/.bunext/public/pages/chunk-AEHOZS24.js" })).toBe(true);
expect(isHashedAssetPath({ pathname: "/.bunext/public/pages/auth/login/S2GYKY3T.js" })).toBe(true);
expect(isHashedAssetPath({ pathname: "/.bunext/public/pages/auth/login/X4TQCLSA.css" })).toBe(true);
expect(
isHashedAssetPath({ pathname: "/.bunext/public/pages/admin/bots/%5Bbot_id%5D/VXCMP4GM.js" }),
).toBe(true);
});
test("leaves fixed names and everything else", () => {
expect(isHashedAssetPath({ pathname: "/.bunext/public/vendor/react.js" })).toBe(false);
expect(isHashedAssetPath({ pathname: "/.bunext/public/pages/index.js" })).toBe(false);
expect(isHashedAssetPath({ pathname: "/admin/bots" })).toBe(false);
expect(isHashedAssetPath({ pathname: "/public/AEHOZS24.js" })).toBe(false);
});
});
+16
View File
@@ -0,0 +1,16 @@
/**
* Page scripts and styles bunext names after their content
* (`chunk-AEHOZS24.js`, `admin/bots/H6VDJMBN.js`, `X4TQCLSA.css`): one
* at such a path never changes, so it can be cached for good. The
* vendor files (`/.bunext/public/vendor/react.js`) keep fixed names and
* are not. `public/sw.js` repeats this pattern.
*/
const HashedAssetPattern = /^\/\.bunext\/public\/pages\/(?:.*\/)?(?:chunk-)?[A-Z0-9]{8}\.(?:js|css)$/;
type Params = {
pathname: string;
};
export default function isHashedAssetPath({ pathname }: Params): boolean {
return HashedAssetPattern.test(pathname);
}
+29
View File
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import pickContentEncoding from "./pick-content-encoding";
describe("pickContentEncoding", () => {
test("prefers brotli", () => {
expect(pickContentEncoding({ accept_encoding: "gzip, deflate, br, zstd" })).toBe("br");
});
test("falls back to gzip", () => {
expect(pickContentEncoding({ accept_encoding: "gzip, deflate" })).toBe("gzip");
expect(pickContentEncoding({ accept_encoding: "*", offer: ["gzip"] })).toBe("gzip");
});
test("only offers what the caller can produce", () => {
expect(pickContentEncoding({ accept_encoding: "br, gzip", offer: ["gzip"] })).toBe("gzip");
expect(pickContentEncoding({ accept_encoding: "br", offer: ["gzip"] })).toBe(null);
});
test("is none without a supported coding", () => {
expect(pickContentEncoding({ accept_encoding: "identity" })).toBe(null);
expect(pickContentEncoding({ accept_encoding: "" })).toBe(null);
expect(pickContentEncoding({ accept_encoding: null })).toBe(null);
});
test("skips refused codings", () => {
expect(pickContentEncoding({ accept_encoding: "br;q=0, gzip;q=0.8" })).toBe("gzip");
expect(pickContentEncoding({ accept_encoding: "br;q=0.0, gzip;q=0" })).toBe(null);
});
});
+32
View File
@@ -0,0 +1,32 @@
export type ContentEncoding = "br" | "gzip";
type Params = {
accept_encoding?: string | null;
/**
* The encodings the caller can produce, preferred first.
*/
offer?: ContentEncoding[];
};
/**
* The encoding to compress a response with for this `Accept-Encoding`:
* the first of `offer` (brotli, then gzip) the client takes, else none. A
* coding listed with `q=0` is refused, and `*` takes anything.
*/
export default function pickContentEncoding({
accept_encoding,
offer = ["br", "gzip"],
}: Params): ContentEncoding | null {
if (!accept_encoding) return null;
const accepted = new Set(
accept_encoding
.toLowerCase()
.split(",")
.map((part) => part.trim().split(";").map((piece) => piece.trim()))
.filter(([, ...params]) => !params.some((param) => /^q=0(\.0*)?$/.test(param)))
.map(([coding]) => coding),
);
return offer.find((encoding) => accepted.has(encoding) || accepted.has("*")) ?? null;
}