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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user