diff --git a/public/logo.png b/public/logo.png index cff5742b..9972066b 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/src/app/globals.css b/src/app/globals.css index 97592e09..4c8aa6ed 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -266,6 +266,22 @@ table { animation: chart-pop-rise 2200ms cubic-bezier(0.16, 0.84, 0.32, 1) forwards; } +/* MiniChart live-pulse halo. Replaces per-circle SMIL pairs + (r 2->7->2, opacity .45->0->.45 over 1.8s): the hub grid renders one + halo per provider per bench, so SMIL added hundreds of + elements to the /benchmarks HTML for a single shared effect. + transform-box: fill-box keeps the scale centered on the data point. */ +@keyframes mini-chart-pulse { + 0% { transform: scale(1); opacity: 0.45; } + 50% { transform: scale(3.5); opacity: 0; } + 100% { transform: scale(1); opacity: 0.45; } +} +.mini-chart-pulse { + transform-box: fill-box; + transform-origin: center; + animation: mini-chart-pulse 1.8s linear infinite; +} + /* Pill - used for category/chain filters across the redesign. min-height bumps mobile tap target without disturbing desktop visual density. */ .pill { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b54c0699..ac4ae382 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -23,12 +23,17 @@ const interTight = Inter_Tight({ display: "swap", }); +// preload: false on the serif + mono families: they style prose/labels +// below the fold, and their 3 woff2 preloads competed with LCP-critical +// Inter/Inter Tight for bandwidth on first paint. display: swap still +// applies, so they load lazily without invisible text. const sourceSerif = Source_Serif_4({ variable: "--font-source-serif", subsets: ["latin"], weight: ["400", "500", "600"], style: ["italic", "normal"], display: "swap", + preload: false, }); const jetbrainsMono = JetBrains_Mono({ @@ -36,6 +41,7 @@ const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], weight: ["400", "500"], display: "swap", + preload: false, }); export const viewport: Viewport = { diff --git a/src/components/mini-chart.tsx b/src/components/mini-chart.tsx index 6154a60f..b2d4afa5 100644 --- a/src/components/mini-chart.tsx +++ b/src/components/mini-chart.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; +import { downsample, MINI_CHART_POINTS } from "@/lib/downsample"; import { buildProviderColors } from "@/lib/series-colors"; /** @@ -52,7 +53,7 @@ export function MiniChart({ .map((r) => ({ slug: r.slug, name: r.name, - values: downsample(benchmark.extras.series24h[r.slug] ?? [], 28), + values: downsample(benchmark.extras.series24h[r.slug] ?? [], MINI_CHART_POINTS), color: colors.get(r.slug) ?? "var(--color-ink-soft)", })) .filter((s) => s.values.length > 1); @@ -103,21 +104,17 @@ export function MiniChart({ vectorEffect="non-scaling-stroke" points={points} /> - {/* Live pulse halo. animated outward */} - - - - + {/* Live pulse halo. CSS keyframes (globals.css) instead of + SMIL : with 37 benches x N providers on the hub + grid the two children per halo added ~800 + elements of markup for an effect one shared class covers. */} + {/* Solid endpoint */} @@ -166,25 +163,3 @@ export function MiniChart({ ); } -// 48-px-tall sparkline can't render >~30 distinct points anyway. Bucket -// long series down so the rendered SVG path stays compact (each kept -// point is the bucket mean, preserving the shape that drove the polyline -// at full resolution). Cuts the /benchmarks HTML payload sharply when -// the grid is showing every bench's series at once. -function downsample(values: number[], target: number): number[] { - if (values.length <= target) return values; - const bucketSize = values.length / target; - const out: number[] = []; - for (let i = 0; i < target; i++) { - const start = Math.floor(i * bucketSize); - const end = Math.floor((i + 1) * bucketSize); - let sum = 0; - let n = 0; - for (let j = start; j < end && j < values.length; j++) { - sum += values[j]; - n++; - } - if (n > 0) out.push(sum / n); - } - return out; -} diff --git a/src/data/benchmarks.ts b/src/data/benchmarks.ts index ff2890aa..0808aaf5 100644 --- a/src/data/benchmarks.ts +++ b/src/data/benchmarks.ts @@ -9,6 +9,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import yaml from "js-yaml"; import { cache } from "react"; +import { downsample, MINI_CHART_POINTS } from "@/lib/downsample"; import type { Benchmark } from "@/types/benchmark"; import { loadAllBenchmarks, @@ -81,7 +82,18 @@ export function toBenchmarkCardData(b: Benchmark): BenchmarkCardData { availability: r.availability, ms: { p50: r.ms.p50 }, })), - extras: { series24h: b.extras?.series24h ?? {} }, + // Downsample at the projection boundary: the card's MiniChart caps + // rendering at MINI_CHART_POINTS anyway, so shipping the raw 24h + // series (hundreds of points x providers x benches) only bloated + // the RSC payload without changing a single drawn pixel. + extras: { + series24h: Object.fromEntries( + Object.entries(b.extras?.series24h ?? {}).map(([slug, values]) => [ + slug, + downsample(values, MINI_CHART_POINTS), + ]) + ), + }, }; } diff --git a/src/lib/downsample.ts b/src/lib/downsample.ts new file mode 100644 index 00000000..5c0566a5 --- /dev/null +++ b/src/lib/downsample.ts @@ -0,0 +1,28 @@ +/** + * Bucket-mean downsampling for sparkline series. Each kept point is the + * mean of its bucket, preserving the shape that drove the polyline at + * full resolution. Shared by the MiniChart renderer (client) and the + * BenchmarkCardData projection (server) so the hub can ship pre-shrunk + * series over the RSC wire without changing what the chart draws. + */ +export function downsample(values: number[], target: number): number[] { + if (values.length <= target) return values; + const bucketSize = values.length / target; + const out: number[] = []; + for (let i = 0; i < target; i++) { + const start = Math.floor(i * bucketSize); + const end = Math.floor((i + 1) * bucketSize); + let sum = 0; + let n = 0; + for (let j = start; j < end && j < values.length; j++) { + sum += values[j]; + n++; + } + if (n > 0) out.push(sum / n); + } + return out; +} + +/** Max points a 48-px MiniChart sparkline can usefully render. Series + * shipped to hub cards are capped to this at the projection boundary. */ +export const MINI_CHART_POINTS = 28;