Skip to content

Commit c2ded82

Browse files
authored
Merge pull request #123 from bejamas/blog/benchmark-post
Blog/benchmark post
2 parents 76c8792 + 2d20bb8 commit c2ded82

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

228 KB
Loading
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
---
2+
title: "How bejamas/ui ships 5x less JavaScript than shadcn/ui"
3+
description: "A three-way benchmark comparing Astro + b/ui, Astro + shadcn/ui, and Next.js + shadcn/ui on the same marketing page."
4+
draft: true
5+
publishDate: 2026-05-04
6+
updatedDate: 2026-05-04
7+
authors:
8+
- name: "Thom Krupa"
9+
role: "Co-founder"
10+
avatar: "https://cdn.sanity.io/images/34ent8ly/production/1699401246513c2c318dffd600dcc31b098c8780-512x512.png"
11+
- name: "Mojtaba Seyedi"
12+
role: "Content Writer"
13+
avatar: "https://cdn.sanity.io/images/34ent8ly/production/f7e1550a6e7234c5c5c22faa584e4ab34313668c-512x512.png"
14+
- name: "Ashot Vardanyan"
15+
role: "Engineering Manager"
16+
avatar: "/blog/authors/ashot-vardanyan.png"
17+
excerpt: "A three-way benchmark comparing Astro + b/ui, Astro + shadcn/ui, and Next.js + shadcn/ui on the same marketing page."
18+
---
19+
20+
If `bejamas/ui` looks close to `shadcn/ui`, the obvious question is: why not just use `shadcn/ui` with Astro?
21+
22+
That is a fair question. `shadcn/ui` is a strong model: copy components into your app, style them with Tailwind CSS, and keep ownership of the code. Astro can render React components too, so on paper the path is simple.
23+
24+
The difference is not the component philosophy. It is the runtime.
25+
26+
We built a benchmark to compare the same page in three setups:
27+
28+
- Astro + `bejamas/ui`
29+
- Astro + `shadcn/ui`
30+
- Next.js + `shadcn/ui`
31+
32+
In this benchmark, `shadcn/ui` means the standard React and Radix implementation. Astro can render it through React islands, but it does not make those components framework-free.
33+
34+
The short version: moving `shadcn/ui` into Astro removes much of the Next.js framework cost, but it does not remove the React and Radix runtime cost. For content-first Astro pages, that difference is the reason `bejamas/ui` exists.
35+
36+
## What we measured
37+
38+
The benchmark uses the same marketing one-pager in all three projects. Each version uses Tailwind CSS v4, static output, and the same page structure.
39+
40+
The page includes:
41+
42+
- A header with a navigation menu and two dropdowns
43+
- A hero with a badge and buttons
44+
- Six feature cards, each with a tooltip
45+
- Pricing tabs that switch between two pricing grids
46+
- Long-form text with six inline hover cards
47+
- An FAQ accordion with six items
48+
- A contact form with two selects, three inputs, one checkbox, and labels
49+
- A footer with a separator and link columns
50+
51+
Below is the exact marketing page used for the benchmark. You can also interact with the live demo here: [astro-bui.vercel.app](https://astro-bui.vercel.app/)
52+
53+
<video
54+
src="https://cdn.sanity.io/files/34ent8ly/production/f2563b61d6c1408a8c7ce9b7a3ba4d60d87cbc4a.mp4"
55+
class="w-full rounded-xl"
56+
controls
57+
/>
58+
59+
Across the page, the benchmark uses 13 component types: `NavigationMenu`, `Select`, `HoverCard`, `Tooltip`, `Tabs`, `Accordion`, `Button`, `Card`, `Badge`, `Input`, `Label`, `Checkbox`, and `Separator`.
60+
61+
The Astro + shadcn/ui version is designed to be a fair comparison, not a worst-case scenario. Static components such as buttons and cards are server-rendered where possible, while interactive sections hydrate as separate `client:load` islands. This way, what you see in the numbers is the cost of React and Radix alone, not Next.js on top of it.
62+
63+
## The result
64+
65+
In the tables below, `b/ui` means Astro + `bejamas/ui`, `Astro shadcn` means Astro + `shadcn/ui`, and `Next shadcn` means Next.js + `shadcn/ui`.
66+
67+
| Metric | b/ui | Astro shadcn | Next shadcn |
68+
| --- | --- | --- | --- |
69+
| JS bundle, gzipped | **22.80 KB** | 119.88 KB | 219.22 KB |
70+
| JS bundle, raw | **60.42 KB** | 358.35 KB | 729.83 KB |
71+
| JS files | **7** | 19 | 10 |
72+
| vs b/ui, gzip | **1x** | 5.3x more | 9.6x more |
73+
| Zero-JS types | 7 of 13 | 3 of 13 | 0 of 13 |
74+
75+
The difference is easier to understand visually:
76+
77+
<img
78+
src="https://cdn.sanity.io/images/34ent8ly/production/29fb820c7720b680112b8231b38c204398f8a99b-1536x1024.png"
79+
alt="Component bundle size breakdown"
80+
class="w-full rounded-xl"
81+
/>
82+
83+
The Next.js number is useful because many teams know `shadcn/ui` through Next.js. But the more important comparison is the middle column.
84+
85+
Astro + `shadcn/ui` ships 119.88 KB of gzipped JavaScript for the same page. That is much better than the Next.js version, but still 5.3x more than Astro + `bejamas/ui`.
86+
87+
That tells us the issue is not only the framework around the app. It is the client runtime needed by React components and Radix primitives.
88+
89+
## Why Astro alone does not remove the cost
90+
91+
Astro gives you islands, and islands help. They let you keep static parts of the page as HTML and hydrate only the pieces that need browser behavior.
92+
93+
But a React island still needs React. A `shadcn/ui` island still uses Radix primitives. When an interactive section hydrates, the browser still has to download, parse, and execute the code needed for that React component tree.
94+
95+
In the benchmark, the Astro + `shadcn/ui` version includes:
96+
97+
- React and ReactDOM runtime
98+
- Radix shared utilities
99+
- Individual hydrated islands for navigation menu, select, tabs, accordion, tooltip, and hover card behavior
100+
- Astro's island runtime
101+
102+
<img
103+
src="https://cdn.sanity.io/images/34ent8ly/production/4f9c6838eac160e799cb4127f56b970a486c0822-1402x1122.png"
104+
alt="Heavy vs light stack comparison"
105+
class="w-full rounded-xl"
106+
/>
107+
108+
That is why the Astro + `shadcn/ui` version lands around 120 KB gzip even though the page is statically rendered and the islands are split per section.
109+
110+
The following breakdown shows exactly where that 120 KB comes from:
111+
112+
<img
113+
src="https://cdn.sanity.io/images/34ent8ly/production/28f781c8ec9571403acc3cac24f001d985b04fd6-1536x1024.png"
114+
alt="JavaScript bundle comparison infographic"
115+
class="w-full rounded-xl"
116+
/>
117+
118+
## What b/ui changes
119+
120+
`bejamas/ui` keeps the same broad authoring model people like in `shadcn/ui`: own the component code, style with Tailwind, and compose primitives directly in your app.
121+
122+
The implementation model is different. Components are authored as Astro components and render HTML first. Interactive components attach behavior through [`@data-slot` packages](https://ui.bejamas.com/blog/introducing-data-slot) instead of hydrating a React tree.
123+
124+
That means static components stay static:
125+
126+
| Component | b/ui | Astro shadcn | Next shadcn |
127+
| --- | --- | --- | --- |
128+
| Button | 0 KB | Partial zero-JS | Bundled |
129+
| Card | 0 KB | Partial zero-JS | Bundled |
130+
| Badge | 0 KB | Partial zero-JS | Bundled |
131+
| Input | 0 KB | 0 KB | Bundled |
132+
| Label | 0 KB | Partial zero-JS | Bundled |
133+
| Checkbox | 0 KB | 0 KB | Bundled |
134+
| Separator | 0 KB | 0 KB | Bundled |
135+
136+
In Astro + `bejamas/ui`, 7 of 13 component types ship no JavaScript. Interactive components still ship JavaScript, but only the small behavior module for that pattern.
137+
138+
<img
139+
src="https://cdn.sanity.io/images/34ent8ly/production/7bbdd554706f1afa05484539ad8e976aca48a0f6-1378x1142.png"
140+
alt="JavaScript bundle comparison infographic"
141+
class="w-full rounded-xl"
142+
/>
143+
144+
The benchmark breakdown for Astro + `bejamas/ui` was:
145+
146+
| Interactive module | Gzipped JS |
147+
| --- | --- |
148+
| NavigationMenu | 7.20 KB |
149+
| Select | 3.67 KB |
150+
| HoverCard | 2.29 KB |
151+
| Tooltip | 1.98 KB |
152+
| Tabs | 1.79 KB |
153+
| Accordion | 1.17 KB |
154+
| Shared Astro runtime | 4.70 KB |
155+
| Total | **22.80 KB** |
156+
157+
<img
158+
src="https://cdn.sanity.io/images/34ent8ly/production/df918f01ddca4dff14f72764acf982cc698d4482-1464x1074.png"
159+
alt="JavaScript bundle comparison infographic"
160+
class="w-full rounded-xl"
161+
/>
162+
163+
The important detail is not just that the bundle is smaller. It is that the bundle matches the actual behavior on the page.
164+
165+
Tabs need tab behavior. Buttons inside a tab panel do not need to become client-side component functions. In the `bejamas/ui` version, tabs toggle pre-rendered HTML panels. The button inside the panel is already a DOM node.
166+
167+
In a React tabs island, React still needs the component code for the subtree it renders. That is the difference between enhancing HTML and hydrating a component tree.
168+
169+
## When shadcn/ui still makes sense
170+
171+
This benchmark is not an argument that `shadcn/ui` is wrong. It is an argument about fit.
172+
173+
`shadcn/ui` still makes sense when:
174+
175+
- Your app is already React-first
176+
- You are building a Next.js app surface where client state and React ecosystem integration matter
177+
- Your team wants Radix primitives specifically
178+
- The page already needs enough React that the incremental component cost is not the main constraint
179+
180+
If that describes the project, using `shadcn/ui` with Astro or Next.js can be the pragmatic choice.
181+
182+
But if the page is mostly content, marketing sections, documentation, forms, navigation, and a handful of interactive widgets, the runtime cost becomes easier to question. In that case, Astro's HTML-first model and a React component runtime are pulling in different directions.
183+
184+
## The answer
185+
186+
So why use `bejamas/ui` if `shadcn/ui` works with Astro?
187+
188+
Because the goal is not just to run shadcn-style components inside Astro. The goal is to keep the Astro-native performance model all the way through the component layer.
189+
190+
`bejamas/ui` is for teams that want component ownership, Tailwind styling, and familiar composition without carrying a React runtime for content-first Astro pages. The benchmark gives that tradeoff a concrete shape: the same page, the same component categories, and 22.80 KB of gzipped JavaScript instead of 119.88 KB or 219.22 KB.
191+
192+
## See the benchmark for yourself
193+
194+
All three implementations, build outputs, measurement scripts, and performance results are available in the benchmark repository.
195+
196+
That includes bundle analysis, Lighthouse scores, code coverage data, and Core Web Vitals measurements so you can inspect the results firsthand.
197+
198+
[Explore the benchmark on GitHub](https://github.com/bejamas/ui-benchmark).

0 commit comments

Comments
 (0)