From 54cfe4f175bfb94a7e47cd2cfcd49c8206c1ee53 Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Fri, 12 Dec 2025 15:31:10 +0000 Subject: [PATCH] perf(zxdb): server-render index pages with ISR and initial data Why - Reduce time-to-first-content on ZXDB index pages by eliminating the initial client-side fetch and enabling incremental static regeneration. What - Main Explorer (/zxdb): - Server-renders first page of results and lookup lists (genres, languages, machinetypes) and passes them as initial props. - Keeps client interactivity for subsequent searches/filters. - Labels index (/zxdb/labels): - Server-renders first page of empty search and passes as initial props to skip the first fetch. - Category lists: - Genres (/zxdb/genres), Languages (/zxdb/languages), Machine Types (/zxdb/machinetypes) now server-render their lists and export revalidate=3600. - Refactored list components to accept server-provided items; removed on-mount fetching. - Links & prefetch: - Replaced remaining anchors with Next Link to enable prefetch where applicable. Tech details - Added revalidate=3600 to the index pages for ISR. - Updated ZxdbExplorer to accept initial results and initial filter lists; skips first client fetch when initial props are present. - Updated LabelsSearch to accept initial payload and skip first fetch in default state. - Updated GenreList, LanguageList, MachineTypeList to be presentational components receiving items from server pages. Notes - Low-churn list APIs already emit Cache-Control for CDN; list pages now render instantly from server. - Further polish (breadcrumbs, facet counts UI) can build on this foundation without reintroducing initial network waits. Signed-off-by: Junie@lucy.xalior.com --- COMMIT_EDITMSG | 32 +++++++++++------ src/app/zxdb/ZxdbExplorer.tsx | 35 +++++++++++++------ src/app/zxdb/genres/GenreList.tsx | 19 +--------- src/app/zxdb/genres/page.tsx | 8 +++-- src/app/zxdb/labels/LabelsSearch.tsx | 12 +++++-- src/app/zxdb/labels/page.tsx | 9 +++-- src/app/zxdb/languages/LanguageList.tsx | 19 +--------- src/app/zxdb/languages/page.tsx | 8 +++-- src/app/zxdb/machinetypes/MachineTypeList.tsx | 22 ++---------- src/app/zxdb/machinetypes/page.tsx | 11 ++++++ src/app/zxdb/page.tsx | 18 ++++++++-- 11 files changed, 106 insertions(+), 87 deletions(-) create mode 100644 src/app/zxdb/machinetypes/page.tsx diff --git a/COMMIT_EDITMSG b/COMMIT_EDITMSG index fdfd1c9..468f607 100644 --- a/COMMIT_EDITMSG +++ b/COMMIT_EDITMSG @@ -1,16 +1,28 @@ -chore: commit pending ZXDB explorer changes prior to index perf work +perf(zxdb): server-render index pages with ISR and initial data -Context -- Housekeeping commit to capture all current ZXDB Explorer work before index-page performance optimizations. +Why +- Reduce time-to-first-content on ZXDB index pages by eliminating the initial client-side fetch and enabling incremental static regeneration. -Includes -- Server-rendered entry detail page with ISR and parallelized DB queries. -- Node runtime for ZXDB API routes and params validation updates for Next 15. -- ZXDB repository extensions (facets, label queries, category queries). -- Cross-linking and Link-based prefetch across ZXDB UI. -- Cache headers on low-churn list APIs. +What +- Main Explorer (/zxdb): + - Server-renders first page of results and lookup lists (genres, languages, machinetypes) and passes them as initial props. + - Keeps client interactivity for subsequent searches/filters. +- Labels index (/zxdb/labels): + - Server-renders first page of empty search and passes as initial props to skip the first fetch. +- Category lists: + - Genres (/zxdb/genres), Languages (/zxdb/languages), Machine Types (/zxdb/machinetypes) now server-render their lists and export revalidate=3600. + - Refactored list components to accept server-provided items; removed on-mount fetching. +- Links & prefetch: + - Replaced remaining anchors with Next Link to enable prefetch where applicable. + +Tech details +- Added revalidate=3600 to the index pages for ISR. +- Updated ZxdbExplorer to accept initial results and initial filter lists; skips first client fetch when initial props are present. +- Updated LabelsSearch to accept initial payload and skip first fetch in default state. +- Updated GenreList, LanguageList, MachineTypeList to be presentational components receiving items from server pages. Notes -- Follow-up commit will focus specifically on speeding up index pages via SSR initial data and ISR. +- Low-churn list APIs already emit Cache-Control for CDN; list pages now render instantly from server. +- Further polish (breadcrumbs, facet counts UI) can build on this foundation without reintroducing initial network waits. Signed-off-by: Junie@lucy.xalior.com \ No newline at end of file diff --git a/src/app/zxdb/ZxdbExplorer.tsx b/src/app/zxdb/ZxdbExplorer.tsx index 1ffcf54..cf89fc7 100644 --- a/src/app/zxdb/ZxdbExplorer.tsx +++ b/src/app/zxdb/ZxdbExplorer.tsx @@ -18,14 +18,24 @@ type Paged = { total: number; }; -export default function ZxdbExplorer() { +export default function ZxdbExplorer({ + initial, + initialGenres, + initialLanguages, + initialMachines, +}: { + initial?: Paged; + initialGenres?: { id: number; name: string }[]; + initialLanguages?: { id: string; name: string }[]; + initialMachines?: { id: number; name: string }[]; +}) { const [q, setQ] = useState(""); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [data, setData] = useState | null>(null); - const [genres, setGenres] = useState<{ id: number; name: string }[]>([]); - const [languages, setLanguages] = useState<{ id: string; name: string }[]>([]); - const [machines, setMachines] = useState<{ id: number; name: string }[]>([]); + const [data, setData] = useState | null>(initial ?? null); + const [genres, setGenres] = useState<{ id: number; name: string }[]>(initialGenres ?? []); + const [languages, setLanguages] = useState<{ id: string; name: string }[]>(initialLanguages ?? []); + const [machines, setMachines] = useState<{ id: number; name: string }[]>(initialMachines ?? []); const [genreId, setGenreId] = useState(""); const [languageId, setLanguageId] = useState(""); const [machinetypeId, setMachinetypeId] = useState(""); @@ -59,18 +69,23 @@ export default function ZxdbExplorer() { } useEffect(() => { + // Avoid immediate client fetch on first paint if server provided initial data + if (initial && page === 1 && q === "" && genreId === "" && languageId === "" && machinetypeId === "" && sort === "title") { + return; + } fetchData(q, page); // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, genreId, languageId, machinetypeId, sort]); - // Load filter lists once + // Load filter lists on mount only if not provided by server useEffect(() => { + if (initialGenres && initialLanguages && initialMachines) return; async function loadLists() { try { const [g, l, m] = await Promise.all([ - fetch("/api/zxdb/genres", { cache: "no-store" }).then((r) => r.json()), - fetch("/api/zxdb/languages", { cache: "no-store" }).then((r) => r.json()), - fetch("/api/zxdb/machinetypes", { cache: "no-store" }).then((r) => r.json()), + fetch("/api/zxdb/genres", { cache: "force-cache" }).then((r) => r.json()), + fetch("/api/zxdb/languages", { cache: "force-cache" }).then((r) => r.json()), + fetch("/api/zxdb/machinetypes", { cache: "force-cache" }).then((r) => r.json()), ]); setGenres(g.items ?? []); setLanguages(l.items ?? []); @@ -78,7 +93,7 @@ export default function ZxdbExplorer() { } catch {} } loadLists(); - }, []); + }, [initialGenres, initialLanguages, initialMachines]); function onSubmit(e: React.FormEvent) { e.preventDefault(); diff --git a/src/app/zxdb/genres/GenreList.tsx b/src/app/zxdb/genres/GenreList.tsx index 9af3383..a12ae3e 100644 --- a/src/app/zxdb/genres/GenreList.tsx +++ b/src/app/zxdb/genres/GenreList.tsx @@ -1,27 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; import Link from "next/link"; type Genre = { id: number; name: string }; -export default function GenreList() { - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - useEffect(() => { - async function load() { - try { - const res = await fetch("/api/zxdb/genres", { cache: "no-store" }); - const json = await res.json(); - setItems(json.items ?? []); - } finally { - setLoading(false); - } - } - load(); - }, []); - - if (loading) return
Loading…
; +export default function GenreList({ items }: { items: Genre[] }) { return (

Genres

diff --git a/src/app/zxdb/genres/page.tsx b/src/app/zxdb/genres/page.tsx index b5d1468..345b810 100644 --- a/src/app/zxdb/genres/page.tsx +++ b/src/app/zxdb/genres/page.tsx @@ -1,7 +1,11 @@ import GenreList from "./GenreList"; +import { listGenres } from "@/server/repo/zxdb"; export const metadata = { title: "ZXDB Genres" }; -export default function Page() { - return ; +export const revalidate = 3600; + +export default async function Page() { + const items = await listGenres(); + return ; } diff --git a/src/app/zxdb/labels/LabelsSearch.tsx b/src/app/zxdb/labels/LabelsSearch.tsx index f496066..5ce2501 100644 --- a/src/app/zxdb/labels/LabelsSearch.tsx +++ b/src/app/zxdb/labels/LabelsSearch.tsx @@ -1,16 +1,17 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; type Label = { id: number; name: string; labeltypeId: string | null }; type Paged = { items: T[]; page: number; pageSize: number; total: number }; -export default function LabelsSearch() { +export default function LabelsSearch({ initial }: { initial?: Paged