Skip to content

feat(www): automate edge bundle-size benchmark with segmented toggle - #1744

Open
yamcodes wants to merge 1 commit into
v1from
feat/benchmark-edge-bundle-size
Open

feat(www): automate edge bundle-size benchmark with segmented toggle#1744
yamcodes wants to merge 1 commit into
v1from
feat/benchmark-edge-bundle-size

Conversation

@yamcodes

@yamcodes yamcodes commented Sep 2, 2026

Copy link
Copy Markdown
Owner

What

Replaces the static, hardcoded bar chart in the "Optimized for the edge" homepage section with a live, reproducible benchmark driven by a committed artifact.

Why

The previous chart showed made-up percentages (8%, 28%) and stale numbers (1.5 kB, 7.4 kB) with no way to verify them. Edge-conscious developers are rightly skeptical of static marketing numbers — this PR turns the chart into a verifiable, build-time fact.

How

Benchmark script (scripts/benchmark-bundle-size.ts)

Runs at build time via prebuild in apps/www/package.json:

esbuild  ·  platform: neutral  ·  target: es2022  ·  minify: true  ·  treeShaking: true

This measures minified, uncompressed bytes — the exact payload V8 has to read and compile during an isolate cold start. Not gzipped (that's a transfer metric), not install size (irrelevant post-bundle).

Two modes:

Mode What's measured
Full edge payload Adapter + validator bundled together — true end-to-end parse cost
Adapter engine only Pure wrapper footprint (peer deps externalized) — apples-to-apples adapter comparison

Competitors (varlock, @t3-oss/env-core) are not in the workspace — the script falls back to hardcoded constants for them and logs esbuild resolution warnings (exits 0).

Baseline artifact (apps/www/lib/benchmark/benchmark.json)

Committed so the site builds offline / in CI without running the script first.

@arkenv/standard @arkenv/core @t3-oss/env-core varlock
Full edge payload 23.3 kB (+Valibot) 156.0 kB (+ArkType) 325.0 kB (+Zod) 28.4 kB
Adapter only 10.0 kB 6.3 kB 14.2 kB

ArkType's heavy weight in the full payload actually strengthens the story: ArkType is 149 kB of compile-time validator — still less than half of the Zod+t3-env ecosystem (325 kB).

UI (RuntimeBloatShowcase)

  • "use client"useState + useEffect toggle
  • URL-backed state via window.history.replaceState (?view=adapter) — zero layout shift, SSR always renders the full default for crawlers
  • Bar widths computed from real byte counts (bytes / maxBytes × 100%)
  • Metric subtitle: "Minified, uncompressed JS evaluated during V8 isolate cold starts."
  • Each name strips the + Validator suffix for the npmx link href
  • Benchmark receipts footer: esbuild · platform: neutral · target: es2022 · View benchmark script ↗

CSS (aurora.css)

New Aurora token-based rules: home-aurora__telemetry-heading-row flex, toggle pill + aria-pressed active state (accent bottom border), subtitle ink-2, footer hairline.

Tests

6 tests — all passing (208 total across the suite):

  • Heading + metric subtitle present
  • Toggle group with two buttons
  • Default full view data (23.3 kB, 156.0 kB, 325.0 kB, 28.4 kB)
  • Adapter view switch via userEvent.click (10.0 kB, 6.3 kB, 14.2 kB; no + ArkType/+ Zod)
  • npmx link hrefs for package base names
  • Benchmark receipts link URL
  • beforeEach resets window.location to prevent replaceState leaks across tests

- Add scripts/benchmark-bundle-size.ts: esbuild (platform:neutral, target:es2022)
  measures raw minified uncompressed bytes — the true V8 isolate parse cost.
  Full edge payload mode bundles adapter+validator. Adapter-only mode externalizes
  peer deps (arktype/@ark/* for core, zod for t3) to isolate wrapper footprint.
  Hardcoded fallbacks for competitors not in workspace (varlock, @t3-oss/env-core).

- Add apps/www/lib/benchmark/benchmark.json: committed baseline artifact with
  real measured sizes (full: standard+valibot=23.3kB, core+arktype=156.0kB,
  varlock=28.4kB, t3+zod=325.0kB; adapter: standard=10.0kB, core=6.3kB, t3=14.2kB).

- Hook into prebuild: tsx ../../scripts/benchmark-bundle-size.ts regenerates the
  artifact on every production build so numbers stay fresh.

- Rewrite RuntimeBloatShowcase (use client): URL-backed segmented toggle
  (?view=adapter via window.history.replaceState), bar widths computed from real
  byte counts, metric subtitle 'Minified, uncompressed JS evaluated during V8
  isolate cold starts', benchmark receipts footer linking to the script.

- Add Aurora CSS: heading-row flex, toggle pill + aria-pressed state, subtitle
  ink-2, footer hairline with receipt link styles.

- Rewrite tests (6 tests, all passing): heading/subtitle, toggle buttons, default
  full view data, adapter view switch via userEvent.click, npmx link hrefs,
  receipts link URL. beforeEach resets window.location to prevent replaceState
  leaks across tests.
@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6352700

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added www Improvements or additions to arkenv.js.org tests This issue or PR is about adding, removing or changing tests labels Sep 2, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

This chart's whole selling point is that the numbers are now a verifiable, build-time fact, but two gaps undercut that: the esbuild receipt is not true for 3 of the 7 bars (they're hardcoded bundlephobia constants that can never resolve in this workspace), and nothing guards the committed artifact against the silent drift that the new prebuild regeneration introduces.

Reviewed changes

  • Benchmark script (scripts/benchmark-bundle-size.ts) — esbuild-bundles 7 cases (full vs adapter) into benchmark.json, with hardcoded fallbacks for varlock and @t3-oss/env-core (not workspace packages).
  • Committed artifact (apps/www/lib/benchmark/benchmark.json) — shipped so the site builds offline/CI without running the script first.
  • UI (RuntimeBloatShowcase) — now a client component with a segmented full/adapter toggle, URL-backed state (?view=adapter) via replaceState, and a benchmark-receipt footer.
  • Tests — rewrote the spec into 6 cases, including a click-through view switch; beforeEach resets window.location to isolate replaceState.
  • Build hook (apps/www/package.json) — prebuild now runs the benchmark on every next build.
  • CSS (aurora.css) — toggle pill, heading row, subtitle, and footer rules on existing Aurora tokens.

⚠️ The esbuild receipt overstates provenance for 3 of 7 bars

The footer (esbuild · platform: neutral · target: es2022) and the metric subtitle present every bar as the output of this script's esbuild run. That is not true: varlock (29,082 B → "28.4 kB") and both @t3-oss/env-core rows (332,800 B → "325.0 kB", 14,541 B → "14.2 kB") can never resolve in this workspace (confirmed: neither package exists in the lockfile or workspace), so those numbers are permanently the hardcoded fallbackBytes constants on the catch path. On a chart whose credibility is verifiability, that's a misattribution readers could check. Mark each row's data source in the JSON (e.g. "source": "esbuild" | "bundlephobia") and reflect non-esbuild rows in the receipt so the claim is honest, or wire the competitors in as real measurements.

⚠️ No drift gate between the committed artifact and prebuild regeneration

prebuild regenerates and overwrites the committed benchmark.json at every production build, but the tests pin the committed values ("23.3 kB", "156.0 kB", "325.0 kB", "28.4 kB"). Any bump to @arkenv/core, @arkenv/standard, valibot, or esbuild shifts the deployed bars while the repo and the tests keep the old numbers — with no CI check comparing them, staleness is invisible until someone manually re-runs the script and commits. Since the artifact is now a footgun in two directions (build overwrites it, tests depend on it), a CI step that runs the script and fails on git diff --exit-code apps/www/lib/benchmark/benchmark.json would turn drift into a test failure instead of silent marketing rot.

ℹ️ Nitpicks

  • Biome: benchmark.json lacks a trailing newline; the script uses 2-space indentation (repo uses tabs), omits the node: protocol on the fs/path/url imports, and uses interface where type is required — autofix will sweep most of these, but catch (e: any) (noExplicitAny) and the div role="group" (a11y/useSemanticElements) are not auto-fixable, so pnpm check stays red until those two are hand-fixed.
  • kb duplicates bytes / 1024 in the JSON; a single source of truth (compute at render, or drop bytes) would prevent a hand-edited artifact from disagreeing with its own bar widths.
  • The aria-pressed segmented control is fine as-is; a radiogroup/tablist would also be defensible, but not required.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

external: t.external ?? [],
});
bytes = res.outputFiles[0].contents.length;
} catch (e: any) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A non-fallback failure silently drops the row, overwrites the committed benchmark.json with a truncated file, prints a misleading success line, and exits 0. For the two ArkEnv cases there is no fallbackBytes, so a broken/missing packages/*/dist at build time ships the chart missing its own bars right when the artifact claims to be generated. Fail the build for any non-fallback case (or leave the existing artifact untouched on partial failure).

Suggested change
} catch (e: any) {
} catch (e: unknown) {
if (t.fallbackBytes) {
bytes = t.fallbackBytes;
console.warn(
`Using fallback for ${t.name}: ${e instanceof Error ? e.message : e}`,
);
} else {
throw new Error(
`Benchmark case ${t.name} failed (no fallback available): ${
e instanceof Error ? e.message : e
}`,
);
}
}

const adapterBtn = screen.getByRole("button", {
name: /Adapter engine only/i,
});
await userEvent.click(adapterBtn);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL-backed state is the headline feature (?view=adapter deep links), but no test asserts it: after this click, window.location.search is never checked, and the mount-from-?view=adapter path — our only SSR/crawler consideration — is untested. The beforeEach reset only exists because replaceState leaks across tests, so a regression in the URL sync would go unnoticed. Cheap additions: assert window.location.search === "?view=adapter" after the click (and back to "" when switching back to Full), and one case that renders with ?view=adapter preset and expects the adapter rows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests This issue or PR is about adding, removing or changing tests www Improvements or additions to arkenv.js.org

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant