From 63602a2f47397e2212187ab74264bf63b5a0d717 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Tue, 2 Jun 2026 14:12:38 +0200 Subject: [PATCH 1/3] fix: switch header to position: sticky (iOS 26 WebKit fixed jitter bug) Web search confirmed user is not alone. WebKit bug 297779 reports that on iOS 26 Safari + WKWebView (Telegram in-app browser, Chrome iOS), any position: fixed element drifts 10-24 px when scroll direction reverses, exposing the page under the header through a thin gap. Mastodon and LinkedIn ship the same symptom. Apple acknowledged the bug, partially fixed it in iOS 26.1 but residual reports persist into Dec 2025. position: sticky uses a different render path in WebKit that does not trip the jitter heuristic. It is also simpler: - no spacer div needed (the element stays in normal flow) - no useRef + ResizeObserver to keep the spacer in sync - no headerH state Net: -25 lines on top of fixing the cosmetic gap. Desktop and standalone Safari behavior are identical, sticky just renders more reliably in WebViews. Refs: - https://bugs.webkit.org/show_bug.cgi?id=297779 - https://github.com/mastodon/mastodon/issues/36144 - https://github.com/TelegramMessenger/Telegram-iOS/issues/1748 --- src/components/site-header.tsx | 43 +++++++++------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index df56a851..138e641a 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -2,7 +2,7 @@ import { Menu, X } from "lucide-react"; import Link from "next/link"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { SiteLogoSwitcher } from "@/components/site-logo-switcher"; import { ThemeToggle } from "@/components/theme-toggle"; @@ -24,10 +24,6 @@ const NAV = [ export function SiteHeader() { const [open, setOpen] = useState(false); - const headerRef = useRef(null); - // Initial estimate matches the rendered header height so the spacer - // doesn't jump on first paint; ResizeObserver refines it after mount. - const [headerH, setHeaderH] = useState(64); // Close the mobile menu when the viewport crosses md so the dropdown // doesn't stick around as the desktop nav reappears. @@ -41,29 +37,16 @@ export function SiteHeader() { return () => mql.removeEventListener("change", onChange); }, [open]); - // Measure the fixed header so we can reserve the same height in the - // flow with a spacer. ResizeObserver picks up viewport resizes. - useEffect(() => { - const el = headerRef.current; - if (!el) return; - const update = () => setHeaderH(el.offsetHeight); - update(); - const ro = new ResizeObserver(update); - ro.observe(el); - return () => ro.disconnect(); - }, []); - return ( - <> -
+
@@ -148,10 +131,6 @@ export function SiteHeader() { )}
-
- {/* Spacer reserves the fixed header's height in the document flow - so page content does not start under it. */} -
- +
); } From 9689ce25fac1e1815b1ae7a09aa4ad09d41d8dd5 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Tue, 2 Jun 2026 14:24:27 +0200 Subject: [PATCH 2/3] ranked-bar+ledger: add Top-N selector and drop unscored rows --- src/components/ledger-table.tsx | 26 +++++++++------ src/components/ranked-bar-chart.tsx | 50 +++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/components/ledger-table.tsx b/src/components/ledger-table.tsx index cdefb410..863a9525 100644 --- a/src/components/ledger-table.tsx +++ b/src/components/ledger-table.tsx @@ -32,16 +32,22 @@ export function LedgerTable({ benchmark }: Props) { // "-" for providers that don't declare it). Used by Solana-native benches // where slot_delta is the canonical metric and ms is wall-clock derived. const hasSlots = results.some((r) => r.slots != null); - // Sort by p50 then push unavailable providers to the bottom. Without - // the secondary sort they'd land at rank #1 on lower-is-better benches - // because their placeholder p50 is 0 - which is what made 0slot, then - // cardano, show up as "fastest" in the recent SERP screenshots. - const sorted = [...results].sort((a, b) => { - const aOff = a.availability === "unavailable" ? 1 : 0; - const bOff = b.availability === "unavailable" ? 1 : 0; - if (aOff !== bOff) return aOff - bOff; - return benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50; - }); + // Drop unscored providers (availability=unavailable AND p50=0). They + // stay in the underlying spec so /products/ pages still resolve + // and SEO coverage holds, but they're noise in a "ranked by performance" + // ledger. Mirrors the filter the ranked-bar chart applies above so the + // two surfaces tell the same story. + // Sort by p50; unavailable rows with non-zero p50 (rare, e.g. cached + // values served while a brief Prom outage was recovering) still get + // pushed to the bottom. + const sorted = [...results] + .filter((r) => r.availability !== "unavailable" || r.ms.p50 > 0) + .sort((a, b) => { + const aOff = a.availability === "unavailable" ? 1 : 0; + const bOff = b.availability === "unavailable" ? 1 : 0; + if (aOff !== bOff) return aOff - bOff; + return benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50; + }); const colors = useMemo(() => buildProviderColors(results), [results]); const allSeries = Object.values(extras.series24h).flat(); diff --git a/src/components/ranked-bar-chart.tsx b/src/components/ranked-bar-chart.tsx index 636a48d7..bf1017b8 100644 --- a/src/components/ranked-bar-chart.tsx +++ b/src/components/ranked-bar-chart.tsx @@ -42,8 +42,16 @@ export function RankedBarChart({ [benchmark.results] ); - const rows = useMemo(() => { - const sorted = [...benchmark.results].sort((a, b) => + // Unscored providers (availability=unavailable AND no p50) are dropped + // up front: they're SEO-relevant for /products page coverage but they + // are pure visual noise on a "ranked by performance" view. The + // ledger below mirrors the same filter so the two surfaces tell the + // same story. + const allRows = useMemo(() => { + const scored = benchmark.results.filter( + (r) => r.availability !== "unavailable" || r.ms.p50 > 0 + ); + const sorted = scored.sort((a, b) => benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50 ); return sorted.map((r) => ({ @@ -57,6 +65,18 @@ export function RankedBarChart({ })); }, [benchmark, colors]); + // Top-N selector — sized off the registered cohort, mirroring the + // time-series chart. When the bench ships more than 10 providers the + // toolbar exposes Top 5 / 10 / 20 / All so a cluttered leaderboard + // can be focused without losing the option to widen. + const cohortSize = benchmark.results.length; + const TOP_N_DEFAULT = cohortSize > 20 ? 20 : null; + const [topN, setTopN] = useState(TOP_N_DEFAULT); + const rows = useMemo(() => { + if (topN == null) return allRows; + return allRows.slice(0, topN); + }, [allRows, topN]); + // The bar scale recomputes from visible rows only - excluding the // tail outliers gives the remaining bars more room to breathe. const visibleValues = rows @@ -107,6 +127,32 @@ export function RankedBarChart({ {headerActions}
+ {cohortSize > 10 && ( +
+ + Show + + {([5, 10, 20, null] as const).map((n) => { + const active = topN === n; + const label = n == null ? "All" : `Top ${n}`; + return ( + + ); + })} +
+ )}