Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/benchmarks/category/[cat]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -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";
Expand DownExpand Up@@ -103,7 +103,7 @@ export default async function BenchmarkCategoryPage({
</p>
</header>
<BenchmarkGrid
benchmarks={benchmarks}
benchmarks={benchmarks.map(toBenchmarkCardData)}
lockedCategory={entry.label}
allCategories={Array.from(new Set(all.map((b) => b.category)))}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/app/benchmarks/page.tsx
Original file line numberDiff line numberDiff line change
@@ -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";
Expand DownExpand Up@@ -50,7 +50,7 @@ export default async function BenchmarksPage() {
{DESCRIPTION}
</p>
</header>
<BenchmarkGrid benchmarks={benchmarks} />
<BenchmarkGrid benchmarks={benchmarks.map(toBenchmarkCardData)} />
</article>
);
}
8 changes: 6 additions & 2 deletions src/components/benchmark-card.tsx
Original file line numberDiff line numberDiff line change
@@ -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";
Expand All@@ -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)";
Expand Down
4 changes: 2 additions & 2 deletions src/components/benchmark-grid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";

Expand All@@ -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. */
Expand Down
14 changes: 12 additions & 2 deletions src/components/mini-chart.tsx
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
import { useMemo } from "react";
import type { Benchmark } from "@/types/benchmark";
import { buildProviderColors } from "@/lib/series-colors";

/**
* Compact multi-line chart for bench cards. Same visual language as the
* 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<string, number[]> };
};

type Props = {
benchmark: Benchmark;
benchmark: MiniChartBenchmark;
/** Internal viewBox width used for path math. Visual width is 100% of parent. */
viewBoxWidth?: number;
height?: number;
Expand Down
57 changes: 57 additions & 0 deletions src/data/benchmarks.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 `<BenchmarkGrid />` (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<string, number[]> };
};

/** 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).
Expand Down
Loading