perf(pokedex): send the box grid as packed rows with the page

The server load fetched a full combined-data page at a 9999 item page size,
carrying detail text and ownership fields the grid never renders, and the client
re-fetched the same payload after hydration.

Load a trimmed grid row instead and pack it as positional tuples so field names
are not repeated for every one of a thousand-plus entries. Entry detail is
fetched on demand from the new per-entry endpoint when a cell is opened, and the
grid marks itself interactive so other page-start work can queue behind it.
This commit is contained in:
Josh Creek
2026-09-15 17:46:19 +01:00
parent 4c268ba15c
commit acaf760b36
16 changed files with 1382 additions and 444 deletions
@@ -0,0 +1,18 @@
import { error, json } from '@sveltejs/kit';
import { requireAuth } from '$lib/utils/auth';
import PokedexRepository from '$lib/repositories/PokedexRepository';
import { loadPokedexEntryDetail } from '$lib/services/PokedexGridService';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async (event) => {
const userId = await requireAuth(event);
const entryId = Number(event.params.entryId);
if (!Number.isSafeInteger(entryId) || entryId < 1) throw error(400, 'Invalid entry');
const pokedex = await new PokedexRepository(event.locals.supabase, userId).findById(
event.params.id
);
if (!pokedex) throw error(404, 'Pokédex not found');
const detail = await loadPokedexEntryDetail(event.locals.supabase, userId, pokedex, entryId);
if (!detail) throw error(404, 'Entry not found');
return json(detail, { headers: { 'cache-control': 'private, no-store' } });
};
@@ -0,0 +1,29 @@
import { packGrid } from '$lib/models/PokedexGridRow';
import { error, json } from '@sveltejs/kit';
import { requireAuth } from '$lib/utils/auth';
import PokedexRepository from '$lib/repositories/PokedexRepository';
import { loadPokedexGrid } from '$lib/services/PokedexGridService';
import { PokedexPerformance } from '$lib/server/pokedexPerformance';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async (event) => {
const timings = new PokedexPerformance();
const userId = await timings.measure('auth', () => requireAuth(event));
const pokedex = await timings.measure('ownership', () =>
new PokedexRepository(event.locals.supabase, userId).findById(event.params.id)
);
if (!pokedex) throw error(404, 'Pokédex not found');
const grid = await loadPokedexGrid(event.locals.supabase, userId, pokedex, timings);
const packed = timings.prepare(() => packGrid(grid));
timings.recordAuth(event.locals.pokedexAuthMs);
const timing = timings.finish();
return json(
{ grid: packed },
{
headers: {
'cache-control': 'private, no-store',
...(timing ? { 'server-timing': timing } : {})
}
}
);
};