diff --git a/src/app/benchmarks/category/[cat]/page.tsx b/src/app/benchmarks/category/[cat]/page.tsx
index 8f22acbc..f001cc93 100644
--- a/src/app/benchmarks/category/[cat]/page.tsx
+++ b/src/app/benchmarks/category/[cat]/page.tsx
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
-import { getBenchmarksSafe } from "@/data/benchmarks";
+import { getBenchmarksSafe, toBenchmarkCardData } from "@/data/benchmarks";
import { BenchmarkGrid } from "@/components/benchmark-grid";
import { Breadcrumb } from "@/components/breadcrumb";
import { safeJsonLd, buildItemListJsonLd } from "@/lib/jsonld";
@@ -103,7 +103,7 @@ export default async function BenchmarkCategoryPage({
b.category)))}
/>
diff --git a/src/app/benchmarks/page.tsx b/src/app/benchmarks/page.tsx
index a92d6428..2a5bb6c8 100644
--- a/src/app/benchmarks/page.tsx
+++ b/src/app/benchmarks/page.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { getBenchmarksSafe } from "@/data/benchmarks";
+import { getBenchmarksSafe, toBenchmarkCardData } from "@/data/benchmarks";
import { BenchmarkGrid } from "@/components/benchmark-grid";
import { safeJsonLd, buildItemListJsonLd } from "@/lib/jsonld";
import { SITE } from "@/data/site";
@@ -50,7 +50,7 @@ export default async function BenchmarksPage() {
{DESCRIPTION}
-
+
);
}
diff --git a/src/components/benchmark-card.tsx b/src/components/benchmark-card.tsx
index 8848f0bc..3f0213c9 100644
--- a/src/components/benchmark-card.tsx
+++ b/src/components/benchmark-card.tsx
@@ -1,5 +1,5 @@
import Link from "next/link";
-import type { Benchmark } from "@/types/benchmark";
+import type { BenchmarkCardData } from "@/data/benchmarks";
import { Hint } from "@/components/hint";
import { MiniChart } from "@/components/mini-chart";
import { CATEGORY_COLOR } from "@/lib/category-colors";
@@ -11,8 +11,12 @@ import { fmtValue, unitSuffix } from "@/lib/format";
* MiniChart preview, and a 3-column footer (providers / samples / updated).
* Drafts render the same skeleton with an "Awaiting first run" placeholder
* in place of the chart and an em-dash where the headline number would be.
+ *
+ * Prop is the slim `BenchmarkCardData` projection, not the full
+ * Benchmark. Keeps the RSC payload for `/benchmarks` to the fields the
+ * card actually reads.
*/
-export function BenchmarkCard({ benchmark }: { benchmark: Benchmark }) {
+export function BenchmarkCard({ benchmark }: { benchmark: BenchmarkCardData }) {
const b = benchmark;
const isDraft = b.status === "draft";
const catColor = CATEGORY_COLOR[b.category] ?? "var(--color-ink-muted)";
diff --git a/src/components/benchmark-grid.tsx b/src/components/benchmark-grid.tsx
index cf055535..1a2bc261 100644
--- a/src/components/benchmark-grid.tsx
+++ b/src/components/benchmark-grid.tsx
@@ -3,7 +3,7 @@
import { useMemo, useState } from "react";
import Link from "next/link";
import { LayoutGrid, List, Search } from "lucide-react";
-import type { Benchmark } from "@/types/benchmark";
+import type { BenchmarkCardData } from "@/data/benchmarks";
import { BenchmarkCard } from "@/components/benchmark-card";
import { categorySlugFromLabel } from "@/lib/categories";
@@ -30,7 +30,7 @@ export function BenchmarkGrid({
lockedCategory = null,
allCategories,
}: {
- benchmarks: Benchmark[];
+ benchmarks: BenchmarkCardData[];
/** When set, force the grid to this category. The pill row still
* renders so users can jump to other category hubs via the same UI
* they had on the /benchmarks root. */
diff --git a/src/components/mini-chart.tsx b/src/components/mini-chart.tsx
index b107afc0..6154a60f 100644
--- a/src/components/mini-chart.tsx
+++ b/src/components/mini-chart.tsx
@@ -1,5 +1,4 @@
import { useMemo } from "react";
-import type { Benchmark } from "@/types/benchmark";
import { buildProviderColors } from "@/lib/series-colors";
/**
@@ -7,10 +6,21 @@ import { buildProviderColors } from "@/lib/series-colors";
* full TimeSeriesChart on the bench detail page. every provider gets a
* line in their signature color. but stripped of axes, hover and tabs.
* Reads `series24h` from the benchmark's `extras` payload.
+ *
+ * Structural prop type: accepts the full `Benchmark` (home table) and
+ * the slim `BenchmarkCardData` projection (hub grid) so the hub doesn't
+ * need to ship the full Benchmark shape (series7d, series30d,
+ * metricPanels, editorial copy, ...) per card in the RSC payload.
*/
+type MiniChartBenchmark = {
+ results: { slug: string; name: string; ms: { p50: number } }[];
+ higherIsBetter: boolean;
+ extras: { series24h: Record };
+};
+
type Props = {
- benchmark: Benchmark;
+ benchmark: MiniChartBenchmark;
/** Internal viewBox width used for path math. Visual width is 100% of parent. */
viewBoxWidth?: number;
height?: number;
diff --git a/src/data/benchmarks.ts b/src/data/benchmarks.ts
index c02f4f92..82bbca0f 100644
--- a/src/data/benchmarks.ts
+++ b/src/data/benchmarks.ts
@@ -16,6 +16,63 @@ import {
loadBenchmark,
} from "@/lib/spec";
+/**
+ * Card-shaped projection used by the hub grid and category pages. The
+ * full Benchmark object carries ~30 fields per bench (extras.series7d,
+ * series30d, seriesByRegion*, metricPanels, methodology, abstract, faq,
+ * findings, perChainExplainer, seoIntro, bestPerChain, worstPerChain,
+ * cellRanks, providersPerChain, ...) that the card never reads. Passing
+ * the full shape to `` (a client component) baked the
+ * whole thing into the RSC payload — 3.2 MB of HTML for 37 cards, which
+ * is what made `/benchmarks` feel like an 8 s page even though the
+ * server TTFB is sub-second.
+ *
+ * Project once at the page boundary so the wire payload only carries
+ * what the card + the grid's search/filter use.
+ */
+export type BenchmarkCardData = {
+ slug: string;
+ title: string;
+ subtitle: string;
+ category: Benchmark["category"];
+ status: Benchmark["status"];
+ unit: Benchmark["unit"];
+ higherIsBetter: boolean;
+ sampleSize: number;
+ lastRunAt: string;
+ metric: string;
+ results: {
+ slug: string;
+ name: string;
+ ms: { p50: number };
+ }[];
+ extras: { series24h: Record };
+};
+
+/** Strip a Benchmark to the fields the hub card actually renders. Keeps
+ * `results` in its original sort order so the card's own sort
+ * (best-leader heuristic) stays correct. */
+export function toBenchmarkCardData(b: Benchmark): BenchmarkCardData {
+ return {
+ slug: b.slug,
+ title: b.title,
+ subtitle: b.subtitle,
+ category: b.category,
+ status: b.status,
+ unit: b.unit,
+ higherIsBetter: b.higherIsBetter,
+ sampleSize: b.sampleSize,
+ lastRunAt: b.lastRunAt,
+ metric: b.metric,
+ results: b.results.map((r) => ({
+ slug: r.slug,
+ name: r.name,
+ ms: { p50: r.ms.p50 },
+ })),
+ extras: { series24h: b.extras?.series24h ?? {} },
+ };
+}
+
/**
* Strict loader. Throws AllBenchmarksDraftError when every bench has
* collapsed to draft (Prom blackout, cold start with no KV snapshot).