feat: optimize asset delivery with build-time compression and encoding negotiation

This commit is contained in:
2026-01-29 13:56:10 -06:00
parent e008ee5a12
commit 4deeef2f00
12 changed files with 634 additions and 143 deletions
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env bun
/**
* Pre-compress static assets with maximum compression levels.
* Run after `bun run build`.
*
* Generates .gz, .br, .zst variants for compressible files ≥ MIN_SIZE bytes.
* These are embedded alongside originals by rust-embed and served via
* content negotiation in src/web/assets.rs.
*/
import { readdir, stat, readFile, writeFile } from "fs/promises";
import { join, extname } from "path";
import { gzipSync, brotliCompressSync, constants } from "zlib";
import { $ } from "bun";
// Must match COMPRESSION_MIN_SIZE in src/web/encoding.rs
const MIN_SIZE = 512;
const COMPRESSIBLE_EXTENSIONS = new Set([
".js",
".css",
".html",
".json",
".svg",
".txt",
".xml",
".map",
]);
// Check if zstd CLI is available
let hasZstd = false;
try {
await $`which zstd`.quiet();
hasZstd = true;
} catch {
console.warn("Warning: zstd not found, skipping .zst generation");
}
async function* walkDir(dir: string): AsyncGenerator<string> {
try {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(path);
} else if (entry.isFile()) {
yield path;
}
}
} catch {
// Directory doesn't exist, skip
}
}
async function compressFile(path: string): Promise<void> {
const ext = extname(path);
if (!COMPRESSIBLE_EXTENSIONS.has(ext)) return;
if (path.endsWith(".br") || path.endsWith(".gz") || path.endsWith(".zst")) return;
const stats = await stat(path);
if (stats.size < MIN_SIZE) return;
// Skip if all compressed variants already exist
const variantsExist = await Promise.all([
stat(`${path}.br`).then(
() => true,
() => false
),
stat(`${path}.gz`).then(
() => true,
() => false
),
hasZstd
? stat(`${path}.zst`).then(
() => true,
() => false
)
: Promise.resolve(false),
]);
if (variantsExist.every((exists) => exists || !hasZstd)) {
return;
}
const content = await readFile(path);
const originalSize = content.length;
// Brotli (maximum quality = 11)
const brContent = brotliCompressSync(content, {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
},
});
await writeFile(`${path}.br`, brContent);
// Gzip (level 9)
const gzContent = gzipSync(content, { level: 9 });
await writeFile(`${path}.gz`, gzContent);
// Zstd (level 19 - maximum)
if (hasZstd) {
try {
await $`zstd -19 -q -f -o ${path}.zst ${path}`.quiet();
} catch (e) {
console.warn(`Warning: Failed to compress ${path} with zstd: ${e}`);
}
}
const brRatio = ((brContent.length / originalSize) * 100).toFixed(1);
const gzRatio = ((gzContent.length / originalSize) * 100).toFixed(1);
console.log(`Compressed: ${path} (br: ${brRatio}%, gz: ${gzRatio}%, ${originalSize} bytes)`);
}
async function main() {
console.log("Pre-compressing static assets...");
// Banner uses adapter-static with output in dist/
const dirs = ["dist"];
let scannedFiles = 0;
let compressedFiles = 0;
for (const dir of dirs) {
for await (const file of walkDir(dir)) {
const ext = extname(file);
scannedFiles++;
if (
COMPRESSIBLE_EXTENSIONS.has(ext) &&
!file.endsWith(".br") &&
!file.endsWith(".gz") &&
!file.endsWith(".zst")
) {
const stats = await stat(file);
if (stats.size >= MIN_SIZE) {
await compressFile(file);
compressedFiles++;
}
}
}
}
console.log(`Done! Scanned ${scannedFiles} files, compressed ${compressedFiles} files.`);
}
main().catch((e) => {
console.error("Compression failed:", e);
process.exit(1);
});
+1 -1
View File
@@ -144,7 +144,7 @@ export class BannerApiClient {
return this.request<User[]>("/admin/users");
}
async setUserAdmin(discordId: string, isAdmin: boolean): Promise<User> {
async setUserAdmin(discordId: bigint, isAdmin: boolean): Promise<User> {
const response = await this.fetchFn(`${this.baseUrl}/admin/users/${discordId}/admin`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
+18 -18
View File
@@ -1,25 +1,25 @@
<script lang="ts">
export interface SearchMeta {
totalCount: number;
durationMs: number;
timestamp: Date;
}
export interface SearchMeta {
totalCount: number;
durationMs: number;
timestamp: Date;
}
let { meta }: { meta: SearchMeta | null } = $props();
let { meta }: { meta: SearchMeta | null } = $props();
let formattedTime = $derived(
meta
? meta.timestamp.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
: ""
);
let formattedTime = $derived(
meta
? meta.timestamp.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
: ""
);
let countLabel = $derived(meta ? meta.totalCount.toLocaleString() : "");
let resultNoun = $derived(meta ? (meta.totalCount !== 1 ? "results" : "result") : "");
let durationLabel = $derived(meta ? `${Math.round(meta.durationMs)}ms` : "");
let countLabel = $derived(meta ? meta.totalCount.toLocaleString() : "");
let resultNoun = $derived(meta ? (meta.totalCount !== 1 ? "results" : "result") : "");
let durationLabel = $derived(meta ? `${Math.round(meta.durationMs)}ms` : "");
</script>
{#if meta}
+1 -1
View File
@@ -43,7 +43,7 @@ const navItems = [
<div class="border-border border-b p-4">
<h2 class="text-lg font-semibold">Admin</h2>
{#if authStore.user}
<p class="text-muted-foreground text-sm">{authStore.user.username}</p>
<p class="text-muted-foreground text-sm">{authStore.user.discordUsername}</p>
{/if}
</div>
<nav class="flex-1 space-y-1 p-2">
+4 -4
View File
@@ -6,7 +6,7 @@ import { Shield, ShieldOff } from "@lucide/svelte";
let users = $state<User[]>([]);
let error = $state<string | null>(null);
let updating = $state<string | null>(null);
let updating = $state<bigint | null>(null);
onMount(async () => {
try {
@@ -52,14 +52,14 @@ async function toggleAdmin(user: User) {
{#each users as user}
<tr class="border-border border-b last:border-b-0">
<td class="flex items-center gap-2 px-4 py-3">
{#if user.avatarHash}
{#if user.discordAvatarHash}
<img
src="https://cdn.discordapp.com/avatars/{user.discordId}/{user.avatarHash}.png?size=32"
src="https://cdn.discordapp.com/avatars/{user.discordId}/{user.discordAvatarHash}.png?size=32"
alt=""
class="h-6 w-6 rounded-full"
/>
{/if}
{user.username}
{user.discordUsername}
</td>
<td class="text-muted-foreground px-4 py-3 font-mono text-xs">{user.discordId}</td>
<td class="px-4 py-3">