From b2b750115fcd61a108467cdcc8cad3e9a7891472 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:46:49 +0100 Subject: [PATCH] perf(sprites): serve grid-sized thumbnails from an immutable URL space Every grid cell downloaded the full detail sprite. Generate a smaller /sprites-grid/v1/ set at build time and let the grid ask for those first, falling back through the existing detail URLs when a thumbnail is missing so detail resolution is unchanged. The new prefix is versioned, so it can be cached forever, and the service worker recognises it alongside the other sprite roots. The placeholder is now an empty box rather than a spinner: a thousand spinners cost layout work and announced nothing useful. --- .gitignore | 2 + .prettierignore | 3 ++ _headers | 3 ++ package.json | 15 ++++---- scripts/grid-thumbnails.mjs | 35 ++++++++++++++++++ src/lib/components/PokemonSprite.svelte | 49 ++++++++++++++++++++----- src/lib/utils/spriteUrl.ts | 11 ++++++ static/offline-worker.js | 2 +- tests/unit/gridThumbnails.test.ts | 24 ++++++++++++ vite.config.ts | 4 +- 10 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 _headers create mode 100644 scripts/grid-thumbnails.mjs create mode 100644 tests/unit/gridThumbnails.test.ts diff --git a/.gitignore b/.gitignore index 9b3096a..7ee49b9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ vite.config.ts.timestamp-* coverage playwright-report test-results + +/static/sprites-grid/ diff --git a/.prettierignore b/.prettierignore index 751bbc4..3361d36 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,6 @@ static/sprites-small/manifest.json # Machine-local editor and tool settings. **/*.local.json + +# Generated grid artwork. +static/sprites-grid/ diff --git a/_headers b/_headers new file mode 100644 index 0000000..02303f4 --- /dev/null +++ b/_headers @@ -0,0 +1,3 @@ + +/sprites-grid/v1/* + Cache-Control: public, max-age=31536000, immutable diff --git a/package.json b/package.json index f6223f3..efc1df4 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,12 @@ "dev-generate-suppress-w": "GENERATE_SW=true SUPPRESS_WARNING=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev", "sprites:build": "node scripts/optimize-sprites.mjs", "sprites:manifest": "node scripts/sprite-manifest.mjs", - "build-generate-sw": "npm run tailwind && GENERATE_SW=true vite build", - "build-generate-sw-node": "npm run tailwind && NODE_ADAPTER=true GENERATE_SW=true vite build", - "build": "npm run tailwind && vite build", - "build-inject-manifest": "npm run tailwind && vite build", - "build-inject-manifest-node": "npm run tailwind && NODE_ADAPTER=true vite build", - "build-self-destroying": "npm run tailwind && SELF_DESTROYING_SW=true vite build", + "build-generate-sw": "npm run sprites:grid && npm run tailwind && GENERATE_SW=true vite build", + "build-generate-sw-node": "npm run sprites:grid && npm run tailwind && NODE_ADAPTER=true GENERATE_SW=true vite build", + "build": "npm run sprites:grid && npm run tailwind && vite build", + "build-inject-manifest": "npm run sprites:grid && npm run tailwind && vite build", + "build-inject-manifest-node": "npm run sprites:grid && npm run tailwind && NODE_ADAPTER=true vite build", + "build-self-destroying": "npm run sprites:grid && npm run tailwind && SELF_DESTROYING_SW=true vite build", "preview": "vite preview --port=4173", "preview-node": "PORT=4173 node build", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", @@ -44,7 +44,8 @@ "supabase:studio": "supabase studio", "migrate:convert-tsv": "node scripts/convert-tsv-to-sql.js", "dev:local": "./scripts/dev-local.sh && npm run dev", - "dev:supabase": "supabase start && npm run dev" + "dev:supabase": "supabase start && npm run dev", + "sprites:grid": "node scripts/grid-thumbnails.mjs" }, "devDependencies": { "@lhci/cli": "^0.15.1", diff --git a/scripts/grid-thumbnails.mjs b/scripts/grid-thumbnails.mjs new file mode 100644 index 0000000..fae062e --- /dev/null +++ b/scripts/grid-thumbnails.mjs @@ -0,0 +1,35 @@ +import { readdir, mkdir, stat, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import sharp from 'sharp'; + +// Bump the URL version whenever dimensions, quality or source artwork changes. +const source = 'static/sprites-small/home'; +const destination = 'static/sprites-grid/v1/home'; +const files = []; +async function walk(relative = '') { + for (const entry of await readdir(path.join(source, relative), { withFileTypes: true })) { + const name = path.join(relative, entry.name); + if (entry.isDirectory()) await walk(name); + else if (entry.name.endsWith('.webp')) files.push(name); + } +} +await walk(); +const manifest = []; +for (const relative of files.sort()) { + const target = path.join(destination, relative); + await mkdir(path.dirname(target), { recursive: true }); + try { + await stat(target); + } catch { + await sharp(path.join(source, relative)) + .resize(128, 128, { fit: 'inside', withoutEnlargement: true }) + .webp({ quality: 80 }) + .toFile(target); + } + manifest.push({ path: relative.split(path.sep).join('/'), bytes: (await stat(target)).size }); +} +await writeFile( + 'static/sprites-grid/v1/manifest.json', + JSON.stringify({ version: 1, width: 128, quality: 80, files: manifest }) +); +console.log(`Prepared ${files.length} versioned grid thumbnails; originals preserved.`); diff --git a/src/lib/components/PokemonSprite.svelte b/src/lib/components/PokemonSprite.svelte index 98e5c65..dcac88a 100644 --- a/src/lib/components/PokemonSprite.svelte +++ b/src/lib/components/PokemonSprite.svelte @@ -1,7 +1,40 @@