This repository compares three static implementations of the same marketing page:
- Astro + b/ui — Astro components with
data-slotJavaScript primitives - Astro + React + shadcn/ui + Base UI — static Astro markup plus explicit React islands
- Next.js + shadcn/ui + Base UI — a Server Component page with interactive Client Component boundaries
The benchmark is scoped to this page, component set, and the locked dependency versions in this repository. It is not a universal framework ranking.
Frameworks updated September 25, 2026: Astro 7.3.5 in both Astro demos, Next.js 16.3.6, and React / React DOM 19.3.0 in both React demos. Astro React uses @astrojs/react 7.0.0. All versions are pinned in the project manifests and lockfiles. Generated reports record installed versions in environment.versions.
Data-slot was updated to 1.0.2 on September 27. All ten b/ui primitives and their shared core use that release. The b/ui registry snapshot is pinned to 218f7621d596a3b4bd33b6c96d31ee3b0e332483, including the current accordion.
All three demos now include cookie preferences with switches and saved choices, a notifications popover, and a quick-settings dropdown. Open them in the Make it yours section near the footer. Cookie controls open on request and do not enable tracking.
All three demos use Nova: bejamas-nova for b/ui and shadcn's base-nova with Base UI 1.8.0 for both React demos. The September 26 alignment also matches badge, tab and navigation variants, tooltip markup, and contact form spacing. See the component snapshot and migration details. Shared presets do not imply identical generated CSS or runtime implementations.
Fresh production build measurements for /. Each compressible response is compressed independently. See the complete asset report.
| Metric | Astro + b/ui | Astro + React + shadcn Base UI | Next.js + shadcn Base UI |
|---|---|---|---|
| Route JS files | 11 | 29 | 7 |
| Route JS raw | 126.68 KiB | 533.57 KiB | 833.75 KiB |
| Route JS gzip | 46.33 KiB | 178.95 KiB | 249.93 KiB |
| Route JS Brotli | 41.44 KiB | 159.04 KiB | 212.09 KiB |
| HTML raw | 131.24 KiB | 70.90 KiB | 145.37 KiB |
| CSS raw | 91.08 KiB | 75.78 KiB | 77.83 KiB |
| CSS gzip | 14.10 KiB | 12.06 KiB | 12.60 KiB |
| Loaded font subset | 28.71 KiB | 28.71 KiB | 28.71 KiB |
| HTML + CSS + JS + font, gzip estimate | 103.04 KiB | 229.62 KiB | 307.24 KiB |
The current raw CSS gap between b/ui and Astro React is 15.30 KiB. This includes differences in component source, supported variants and selectors; matching the visual preset does not make the implementations byte-identical.
This September 27 baseline uses Nova in all three demos and data-slot 1.0.2 in b/ui, with the expanded component set. The previous Nova baseline is preserved in the archive.
Values are medians of five sequential Lighthouse 13.4.1 mobile runs against local production builds, served by one HTTP server with deterministic gzip. All three use their default CSS delivery for this baseline. Mobile viewport 412×823, 150 ms simulated RTT, 1,638.4 Kbps throughput, and 4× CPU slowdown.
| Metric | Astro + b/ui | Astro + React + shadcn Base UI | Next.js + shadcn Base UI |
|---|---|---|---|
| Performance score | 99 | 96 | 96 |
| FCP | 1.66 s | 1.80 s | 1.21 s |
| LCP | 1.66 s | 2.55 s | 2.76 s |
| TBT | 0.0 ms | 0.0 ms | 1.5 ms |
| CLS | 0.02 | 0.00 | 0.02 |
| Speed Index | 1.66 s | 1.80 s | 1.21 s |
See the five individual samples and methodology. These are controlled local lab results, not field Core Web Vitals. CDN behavior, production TTFB and repeat-visit caching are outside this comparison.
The following diagnostics predate the Nova alignment and use Juno in b/ui. The FCP cause investigation shows that script priority strongly affects this simulated ranking. With unchanged CSS and JavaScript files, lowering all b/ui module priorities produced a 1.06 s median, while raising Next.js script priority produced 2.76 s. The b/ui variant ranged from 1.06–1.36 s because scripts involved in initial layout can remain in Lighthouse's paint dependency graph. Under applied DevTools throttling, the baseline medians were 1.45 s for b/ui and 1.50 s for Next.js. These are separate diagnostic runs; the table above retains each framework's default output.
These interaction timings predate the Nova alignment and are retained as historical results, not measurements of the current page.
Measured September 26, 2026: 30 visits per implementation, ten settled actions per visit on an Apple M5 Pro with Chrome 153, a 390 × 844 mobile viewport, and touch input. Each cell is median interaction latency in milliseconds; lower is faster. All 900 actions succeeded within the 2-second outcome deadline, with no missing Event Timing samples.
| Interaction | Astro + b/ui | Astro + React + shadcn Base UI | Next.js + shadcn Base UI |
|---|---|---|---|
| Open Products menu | 200 | 384 | 544 |
| Close Products menu | 72 | 112 | 112 |
| Switch pricing to Yearly | 72 | 144 | 152 |
| Switch pricing to Monthly | 56 | 104 | 112 |
| Expand FAQ item | 64 | 72 | 80 |
| Collapse FAQ item | 48 | 56 | 56 |
| Open company-size select | 76 | 40 | 40 |
| Choose company size | 76 | 216 | 224 |
| Check newsletter box | 40 | 72 | 76 |
| Uncheck newsletter box | 32 | 48 | 52 |
On this page, the largest measured changes from data-slot 1.0.0 to 1.0.1 were opening Products: 256 → 200 ms (22% lower) and opening the company-size select: 96 → 76 ms (21% lower). FAQ expansion changed from 72 to 64 ms; checkbox medians were unchanged. The old version already had the local navigation fix, and both versions passed all 300 b/ui actions. See the complete before/after table, including p90 and success counts.
These are scripted lab interaction timings, not field INP or measurements from a physical phone. The before/after runs used the same page, framework versions, and Chrome, but ran sequentially; small differences can reflect run variation and Event Timing's 8 ms granularity. The select-opening row needs care: Base UI has lower Event Timing latency, but the expected open DOM state was observed later. The median touch-down-to-open-state delay was 69.5 ms for b/ui, 207.1 ms for Astro React, and 206.35 ms for Next.js. This is a DOM observation, not an exact pixel-presentation timestamp. Base UI schedules opening through an animation-frame callback, so its reported next paint can precede the completed dropdown update. The 40 ms entry therefore does not establish that its dropdown appears sooner. See all p90 values and counts, raw samples, and the reproduction and physical-device guide.
npm run test:components additionally checks the new overlays at phone and desktop widths, including hover, persistence, focus trapping, keyboard actions, modal accessibility, and WCAG checks with the settings menu open.
Browser verification confirms identical visible text, theme CSS and local font; working navigation, keyboard tooltips, tabs, hover cards, accordion, both select labels and checkbox; bounded navigation popups; six focusable tooltip triggers; zero nested controls, axe violations or browser errors; and requests matching the asset report. It also enforces Nova in all three demos, identical Base UI component source, matching installed Astro versions, and matching React / React DOM versions.
Visual geometry verification compares 101 corresponding visible component boxes, plus header, section and footer layout, against both React demos at 412px and 1280px widths. All comparisons pass within 0.5px. This checks settled light-mode layout, not pixel equality or every animated state. Run npm run verify:visual for screenshots and the report; the full benchmark includes this check.
| Structural metric | Astro + b/ui | Astro + React + shadcn Base UI | Next.js + shadcn Base UI |
|---|---|---|---|
| DOM elements after initialization | 400 | 284 | 277 |
| Serialized DOM after initialization | 98.08 KiB | 70.51 KiB | 145.33 KiB |
These counts measure browser elements. They do not include React's virtual DOM or Fiber objects in JavaScript memory.
The September 26 Nova baseline preserves measurements before the expanded component set and data-slot 1.0.2.
The pre-Nova Juno archive preserves the earlier assets, quality, Lighthouse and interaction reports. npm run verify:visual captures the current production pages at mobile and desktop widths for visual review.
The data-slot 1.0.0 archive preserves the reports, dependency lockfile, and local navigation patch from before the 1.0.1 upgrade. Its separate 30-visit b/ui baseline uses the same page and framework versions as the new interaction study.
The Base UI baseline before framework upgrades preserves the Astro 5.18.2 / Next.js 16.1.6 measurements and installed versions. It already uses Base UI 1.8.0, so it is the relevant baseline for this framework upgrade. The upgrade also aligns both React demos on React 19.3.0; changes in results cannot be attributed to the framework alone.
The Radix archive preserves all earlier reports, including the CSS inlining experiment, component CSS attribution, expanded 20× interaction study, and earlier four-profile study. Their timings describe the pre-migration code. The expanded study also predates the b/ui navigation bridge fix.
CSS inlining remains available through native build settings for all three projects. The old inlining numbers have not been relabeled as Base UI results. Run npm run performance:css for a fresh experiment.
For this page and component set:
- b/ui uses the least JavaScript because static components remain HTML and interactive behavior comes from small vanilla-JavaScript primitives.
- Astro React loads React, the Astro island runtime, and the Base UI/shadcn code used by its interactive islands.
- The Next page is an idiomatic Server Component. Static page text is not forced through a top-level
"use client"boundary, but the client router/runtime and interactive shadcn components still contribute to the route bundle. - Lower JavaScript does not mean lower output in every category: b/ui ships more HTML than Astro React and retains slightly more live DOM elements after initialization.
- Interaction results distinguish settled responsiveness from whether an early input produces a UI response. Read success counts alongside latency; the page's payload alone does not establish an interaction ranking.
Claims such as “React + Base UI has a fixed bundle floor” or “Next always adds a specific number of kilobytes” are intentionally avoided. Those values change with component selection, package versions, bundler behavior, and application architecture.
Every variant renders the same content and interaction set:
- Header navigation with two dropdown menus
- Hero badge and calls to action
- Six feature cards with tooltips
- Monthly/yearly pricing tabs
- Six inline hover cards
- Six FAQ accordion items
- Contact form with two selects and a checkbox
- Cookie preferences dialog with necessary, analytics, and marketing switches
- Notifications popover with a mark-as-read action
- Quick-settings dropdown with checkbox items
- Footer link columns
The projects use the same color tokens and local Geist variable font. Framework-specific DOM wrappers and runtime markers are allowed and reported rather than treated as identical markup.
- Node.js 24.16.0 (
.node-version) - npm 11.13.0
- Bun 1.3.14
- Google Chrome; set
CHROME_PATHif it is installed in a nonstandard location
All dependency lockfiles are tracked.
# Install the benchmark tools
npm ci
# Install each project from its lockfile
(cd astro-bui && bun install --frozen-lockfile)
npm ci --prefix astro-react-shadcn
npm ci --prefix nextjs-shadcn
# Build, measure assets, verify parity and accessibility,
# run 30 interaction rounds, and run five Lighthouse samples per project
npm run benchmarkIndividual stages can also be run independently:
npm run build
npm run measure
npm run verify
npm run test:interactions
npm run test:navigation
npm run test:components
npm run interactions
npm run performance
npm run performance:css
npm run performance:size
npm run performance:causeRun npm run sync:bui to reapply the pinned Bejamas UI registry snapshot and its matching data-slot versions. Update the commit and version constants in scripts/sync-bui.mjs before intentionally moving the benchmark to a newer snapshot.
Use BENCHMARK_RUNS to change the five-run Lighthouse default. Use INTERACTION_RUNS to change the 30-round interaction default. The full interaction matrix runs sequentially and can take tens of minutes.
npm run performance:css builds baseline and inline variants, saves isolated snapshots, and restores the baseline build outputs before measuring. Set CSS_RUNS to change its three-run default. BENCHMARK_INLINE_CSS=1 npm run build enables full CSS inlining in all three projects; ordinary builds retain their default CSS delivery. Next.js uses its experimental inlineCss setting.
npm run performance:size tests page-specific CSS generation and shorter class names in isolated b/ui build copies against an unchanged Next.js control. It verifies mobile and desktop appearance and controls, then measures both simulated and applied mobile throttling. Set FCP_SIZE_RUNS to change the three-run default. Results go to the size experiment report; production builds and headline measurements stay unchanged.
npm run performance:cause tests script priority and preconnect hints in isolated build copies, reconstructs Lighthouse's FCP dependency graph, replays priority changes against the same trace, and checks page content and controls. It writes the FCP cause investigation. FCP_CAUSE_RUNS, FCP_CAUSE_CASES, and FCP_CAUSE_MODES select runs, variants, and throttling methods; defaults cover seven variants with three runs under each method. These diagnostics keep production builds and headline results unchanged.
scripts/measure.mjs begins at each generated index.html and includes:
- The route HTML response
- Referenced stylesheets
- Font subsets whose Unicode ranges match the rendered route text
- Modern JavaScript script tags
- JavaScript reached through static or dynamic ES module imports
It excludes:
nomodulefallback bundles in modern-browser totals- Manifests and chunks not referenced by
/ - HTTP headers and protocol overhead
Gzip level 9 and Brotli quality 11 are calculated independently per response. These are reproducible compression estimates, not claims about a production CDN's negotiated encoding.
scripts/performance.mjs serves all production outputs through the same local server, runs Lighthouse sequentially, stores every full report, and commits a compact median summary. Running audits concurrently is deliberately avoided because resource contention distorts results.
scripts/verify.mjs uses Chrome to exercise the equivalent controls, compare rendered text and requested assets, and run axe-core. The suite fails immediately when parity, functionality, accessibility, or console-error checks regress.
scripts/interactions.mjs separately calibrates CPU profiles and collects complete input sequences and UI outcomes. It rotates execution order, saves raw samples, and generates the result table. npm run test:interactions checks ignored inputs, slow handlers, delayed outcomes, missed deadlines, opening and closing controls, selected values, failed prerequisites, and remote Chrome transport. See the interaction benchmark guide.
The projects are configured as Cloudflare static-asset Workers in the Bejamas OSS account:
| Project | Production URL | Build output |
|---|---|---|
| Astro + b/ui | astro-bui.bejamas-oss.workers.dev | astro-bui/dist |
| Astro + React + shadcn | astro-react-shadcn.bejamas-oss.workers.dev | astro-react-shadcn/dist |
| Next.js + shadcn | nextjs-shadcn.bejamas-oss.workers.dev | nextjs-shadcn/out |
Deployments are intentionally separate from the benchmark command:
These URLs serve the last manual deployment. Pushing or merging to main does
not deploy them automatically; this repository has no deployment workflow.
(cd astro-bui && bun run deploy)
(cd astro-react-shadcn && npm run deploy)
(cd nextjs-shadcn && npm run deploy)
# Check live HTML, JavaScript, CSS, fonts, images, and RSC payloads against local builds
npm run verify:deploymentsRun npm run build, npm run verify, npm run test:navigation, and
npm run test:components before deploying. After deployment, run
DEMO_DEPLOYED=1 npm run test:components to exercise the live demos. The deployment check fails on missing or different files, so stale
Workers cannot pass just because they return HTTP 200. To check an existing
deployment, keep the exact build outputs that were uploaded and run
npm run verify:deployments. Rebuilding Next.js can change its generated build
ID even with unchanged source. Record the Git commit with Wrangler's --tag
and --message options when deploying to track the source version too.
- This is one content-heavy marketing page, not a representative sample of every application type.
- Results apply to the exact locked dependencies and implementation choices in this repository.
- Local Lighthouse runs isolate application cost but do not represent production geography, caching, CDN behavior, or real-user hardware.
- No CrUX or other field dataset is available, so the benchmark does not publish field LCP, CLS, or INP.
- The current interaction study uses 30 visits per implementation at 20× CPU slowdown, with ten settled actions per visit. It does not cover early input in this run or represent a full user session.
- CPU-calibrated mobile profiles are approximations. Physical-device results require a separate run; missing Event Timing entries remain unreported.
- Framework and library upgrades require regenerating and reviewing all committed result files.