Archive for the ‘AI’ Category

The AI cost of JavaScript frameworks

One of my pet peeves with the web development community is that so many of the decisions we make are vibes-based. Frameworks are “blazing fast,” tools will “supercharge” your development, the right abstraction will benefit both you and your users (DX and UX all in one!). All of this breathless boasting is usually offered with zero evidence. Alex Russell has called this “frontend’s lost decade”, Baldur Bjarnason has called programming a pop culture, and I’ve tried to counter this trend in my blog with facts and benchmarks (at my best, anyway).

For a long time it seemed like our community’s allergy to evidence was driven by the desire for greater DX. In other words, devs just liked the way it felt to type JSX in React, and the arguments promoting React or related tools (“the virtual DOM is fast!”) were largely post-hoc rationalizations to justify those initial gut feelings. (Jonathan Haidt would likely agree with this assessment.)

But in the agentic era, when developers are no longer typing code by hand and are thus less constrained by DX, I still see vibes ruling the day (and not just in the sense of “vibe-coding”). Frontends are rewritten because some framework or another is “better for agents.” (Did anyone think to ask the agents?) The resulting code is “faster,” the agents make “fewer mistakes,” etc. Once again, these claims are often made without evidence. Whenever I read one of these accounts, I think to myself, “Where are the benchmarks?”

Well I got curious, so I wrote a benchmark. The structure of this benchmark is pretty straightforward:

  • Design a small but non-trivial Pokémon app using the PokéAPI (those who have followed my work will be unsurprised)
  • Instruct agents to build the app with a particular JavaScript framework/meta-framework (e.g. Next.js, Nuxt, SvelteKit, etc.)
  • Test the output against a spec conformance suite (to avoid agent cheating)
  • Evaluate the results in terms of development time, tokens spent, lines of code, and performance and accessibility scores in Lighthouse

The app is fairly simple: you’ve got a list of Pokémon with their types, moves, and data from various games. You can search, filter, etc.:

Screenshot of a Pokémon web app with various monsters listed, their types, etc.

I should start off with some caveats: this is not a perfect experiment. This app represents only one holotype – it’s a relatively content-based site with little interactivity, closer to a Wikipedia than a Figma, GMail, etc. That said, I had to choose some app to build, and this one is at least easy to understand at a high level, and relatively straightforward to spec out and test.

To try to even the playing field, I had each framework/meta-framework use the official MCP or skills for that tool. I wanted to avoid accusations of “this framework is not well-represented in the training data, that’s why we built our MCP, we expect people to use it,” etc. I also didn’t over-specify the exact architecture to use (e.g. SSR vs CSR), nor did I specify that the app should be as performant as possible. I simply gave the spec to Opus 5.5 with high effort and told it to use the latest version of the given framework. Each agent ran in a sandbox and without any knowledge that it would be tested in Lighthouse on performance and accessibility, although it was given the conformance test suite.

Note I also didn’t mandate any CSS methodology, except to avoid using Tailwind, Bootstrap, or any other component framework, and to design based on a set of spec tokens to enforce some visual consistency. I didn’t want to make this test all about CSS, but instead about JS frameworks, CSR vs SSR, SPAs vs MPAs, etc. The resulting app is tested against a conformance suite based on the accessibility tree rather than the raw DOM to avoid mandating any particular HTML structure – one framework uses web components which rely on shadow DOM, so this was required.

One thing I learned from this experiment is that everything I’m measuring is very stochastic. LLMs are random-word generators, so sometimes you get a low dice-roll and an agent goes off on some weird tangent wasting a bunch of tokens, through no fault of the framework or its tools. Lighthouse itself can give inconsistent results as well: Largest Contentful Paint (LCP) may decide that one image is the “largest” element, then pick another one on the next run. It’s hard to have a perfectly fair experiment, but running multiple iterations did help, and I’ve tried to be honest about the variance.

With all that throat-clearing out of the way, let’s get to the results.

Results

I’m going to break this down by multiple dimensions: cost to build, quality, and maintainability. For each framework/meta-framework, I took the median of 5 runs: in other words, I built the same app 5 times in the same framework. (Yes, this is the kind of zany thing you can do with coding agents these days.)

The exception was Angular, which failed to create a measurable app twice, so I took 3 iterations instead for some metrics. (This is explained below.)

Cost

Cost per run, in US dollars
Framework Cost ($)
Hono+htmx $2.67 (2.23–3.19)
Astro $2.76 (2.16–3.63)
Nuxt $2.82 (2.39–4.25)
Vanilla $2.98 (2.24–3.37)
Next.js $3.14 (2.58–4.13)
Qwik $3.27 (2.35–3.80)
TanStack Start $3.54 (3.11–3.96)
SolidStart $3.79 (2.84–4.81)
React Router $3.81 (2.82–3.97)
SvelteKit $4.28 (3.59–4.65)
Gracile+Lit $4.65 (3.92–5.07)
Angular $4.97 (3.19–6.82)

Median of 5 runs.

Note that this quotes the API prices directly from Claude Code and doesn’t actually represent how much money I spent. I’m on a Max plan, so this was just part of my overall $100/month.

Note also the variance: React Router’s median is roughly $1 worse than that of Hono+htmx, but the cheapest version of React Router ($2.82) is still cheaper than the most expensive version of Hono+htmx ($3.19). However some reliable patterns do emerge.

Most clearly, SvelteKit, Gracile+Lit, and Angular are more expensive to build than the other frameworks. For SvelteKit, this may be due to the fact that the agents read the SvelteKit v3 migration guide in every run, likely because it’s too new to be in Opus 5.5’s training data. Similarly for Gracile, the agents didn’t know the framework very well and either cloned the Gracile repo or read its compiled source from node_modules. For Angular it seems to be a function of the total code size (see below).

I should also mention that not every agent made use of an MCP/skill/Claude Code plugin, nor did every framework have one:

Click to see AI tool usage
Framework Installed Used by the agents?
Angular Angular MCP and Angular agent skills MCP in 3/5 runs, skill tool in 4/5 runs, skill files in 2/5
Astro Astro MCP server MCP in 2/5 runs
Gracile+Lit N/A N/A
Hono+htmx Hono skills Skill tool in 5/5 runs
Next.js next-devtools-mcp and next-dev-loop skill Skill files in 1/5 runs, AGENTS.md in 2/5 runs
Nuxt Nuxt MCP N/A
Qwik N/A N/A
React Router React Router skill Skill tool in 5/5 runs, skill files in 5/5 runs
SolidStart N/A N/A
SvelteKit Svelte Claude Code plugin MCP in 5/5 runs
TanStack Start N/A Skill files in 2/5 runs
Vanilla N/A N/A

For example, TanStack Start’s agent only discovered its skill files in 2/5 runs (they’re shipped in the node_modules of the project). For Nuxt, the MCP was never used by the agents. For Next.js, agents only read the skill file in 1/5 runs and the AGENTS.md in 2/5 runs. This is a wide range of results, which points back to the stochastic nature of LLMs. I still consider this a valid experiment, though, because I doubt many vibe-coders are shouting at their agents in all-caps to use the MCP (and plenty are probably skipping it entirely).

Also, if you’re wondering how I came up with Hono+htmx: essentially, I wanted htmx represented somehow, and Hono seemed like a fine pairing (i.e. as a JavaScript alternative to Rails/Django/etc.). As for Gracile+Lit, I wanted web components represented via their standard-bearer framework (Lit), and was only aware of Gracile as a meta-framework. If there are better candidates, then those would be great for a follow-up experiment.

For “Vanilla,” note that the agent was told to use “vanilla JavaScript or TypeScript with Vite, without a UI framework.” Surprisingly, this underperforms several frameworks, which seems to demonstrate that having some framework can be better than nothing (or better than raw Vite, anyway). Vite was chosen because it undergirds many frameworks and represents fairly standard and agnostic JavaScript infrastructure.

The total time and tokens track pretty closely with the cost:

Time per run
Framework Time (minutes)
Astro 8.2 min (6.2–9.7)
Hono+htmx 8.6 min (5.9–9.6)
Nuxt 9.0 min (8.0–17.6)
Vanilla 10.1 min (6.9–18.6)
TanStack Start 10.3 min (8.3–13.9)
Qwik 10.7 min (7.1–12.9)
SvelteKit 10.8 min (8.6–13.8)
Next.js 10.9 min (8.3–11.9)
SolidStart 11.2 min (8.2–15.9)
Gracile+Lit 12.1 min (10.8–12.9)
React Router 12.5 min (8.4–13.6)
Angular 17.2 min (9.8–23.7)

Wall-clock minutes from the agent’s perspective. Median of 5 runs.

Tokens per run, in millions
Framework Tokens (M)
Nuxt 3.85 M (3.16–8.18)
Astro 4.13 M (3.03–7.57)
Hono+htmx 4.26 M (3.40–5.61)
Next.js 4.99 M (3.54–8.82)
Vanilla 5.00 M (3.44–6.32)
Qwik 5.64 M (3.41–7.03)
TanStack Start 7.07 M (5.85–7.99)
SolidStart 7.13 M (5.02–10.07)
React Router 7.58 M (4.55–8.18)
SvelteKit 8.53 M (6.79–9.29)
Gracile+Lit 9.04 M (7.36–11.10)
Angular 9.76 M (4.92–14.94)

Cache reads, uncached input and output. Median of 5 runs.

The tokens, by the way, were reported by Claude Code. The duration is just the total wall-clock time.

Quality

Of course, because I think that outcomes matter, I also tested the agents with Lighthouse. As software developers, I think it’s important that we not only build things cheaply but also performantly and accessibly.

I should first note two caveats about my benchmark:

  • The Pokémon app is small and doesn’t represent a large, complex real-world application.
  • Lighthouse and the Core Web Vitals in general are biased against SPAs, so the MPA frameworks are almost guaranteed to win on metrics like Total Blocking Time (TBT), JavaScript bytes, etc. I considered adding some in-app performance metrics but backed off because it added complexity and variability to the benchmark, and these would be metrics I invented as opposed to industry standards.

For simplicity’s sake, I’m only reporting the scores for the home page. There are other pages, but they all score fairly similarly or they have wild swings in metrics like LCP, so it just complicates things to include them.

In short: the Lighthouse scores for Performance, Accessibility, and CLS (Cumulative Layout Shift) are nearly saturated and don’t vary much by framework:

Framework CLS Performance Accessibility
Angular 0.000 98 100
Astro 0.000 100 100
Gracile+Lit 0.000 100 100
Hono+htmx 0.000 100 100
Next.js 0.000 99 100
Nuxt 0.000 100 100
Qwik 0.000 100 100
React Router 0.000 100 100
SolidStart 0.000 100 100
SvelteKit 0.000 100 100
TanStack Start 0.000 99 100
Vanilla 0.111 97 100

So to get some more interesting numbers, we need to look at other metrics. The most reliable one is the JavaScript bytes sent, which largely tracks the client-side framework size:

JavaScript bytes transferred
Framework JS (KiB)
Astro 0 KiB (0–1)
Vanilla 7 KiB (6–7)
Gracile+Lit 8 KiB (1–13)
Hono+htmx 18 KiB (18–18)
SvelteKit 36 KiB (35–36)
SolidStart 40 KiB (38–40)
Qwik 40 KiB (40–41)
Nuxt 84 KiB (83–85)
Angular 93 KiB (81–102)
React Router 115 KiB (115–119)
TanStack Start 118 KiB (112–119)
Next.js 142 KiB (136–143)

As reported by Lighthouse. Median of 5 apps (3 for Angular).

The Total Blocking Time (TBT) is interesting in that it’s not purely a function of the JavaScript bytes; some of the apps spend more time on the main thread than others. That said, the numbers are all quite small:

Total Blocking Time (TBT)
Framework TBT (ms)
Astro 0 ms (0–0)
Gracile+Lit 0 ms (0–0)
Hono+htmx 0 ms (0–0)
SvelteKit 0 ms (0–1)
Vanilla 0 ms (0–0)
TanStack Start 1 ms (0–36)
Qwik 3 ms (0–30)
SolidStart 11 ms (0–12)
React Router 17 ms (0–52)
Nuxt 40 ms (14–54)
Next.js 71 ms (66–121)
Angular 103 ms (93–123)

As reported by Lighthouse, simulated mobile throttling. Median of 5 Lighthouse runs per app, then median of 5 apps after that (3 for Angular).

Largest Contentful Paint (LCP) is highly variable due to the aforementioned issues with the metric itself. As expected, though, it does broadly punish SPAs and reward MPAs:

Largest Contentful Paint (LCP)
Framework LCP (ms)
Astro 810 ms (792–1662)
Gracile+Lit 956 ms (796–980)
Hono+htmx 978 ms (977–1662)
Qwik 979 ms (954–1278)
SvelteKit 1130 ms (1129–1131)
SolidStart 1204 ms (1203–1278)
Vanilla 1667 ms (764–1676)
Nuxt 1727 ms (1427–1844)
React Router 1729 ms (1729–1770)
Angular 1954 ms (1351–1955)
TanStack Start 2028 ms (1805–2104)
Next.js 2090 ms (1028–2187)

As reported by Lighthouse, simulated mobile throttling. Median of 5 Lighthouse runs per app, then median of 5 apps after that (3 for Angular).

Note that the wild variance in the Next.js numbers can be explained by the fact that one app avoided lazy-loading images and the other four didn’t. Astro and Vanilla have similar swings that appear to be caused by prioritization or LCP quirks.

In general though, it’s unsurprising that the metric tends to reward MPAs that paint quickly with HTML rather than SPAs that paint after client-side JavaScript runs (along with possibly multiple throttled round-trips to grab the Pokémon data). Ignoring variance, LCP here is mostly a function of JavaScript bundle size. (Qwik is a notable exception here: technically it’s an SPA that uses client-side routing, but it scores among the MPA frameworks.)

Note though that some of the frameworks varied in whether they focused more on the client or the server, or even whether they used a client-side framework. For example, the Gracile agent didn’t even use Lit in two runs. For “Vanilla,” the agent had full latitude (aside from being told to use Vite), but still chose to build an SPA all 5 times.

SSR vs CSR is also partly where the trouble with Angular came from: in 2 runs, the agent built an SSR app that rejected non-localhost (even though the instructions explicitly said that the app would be behind a reverse proxy). I considered this a valid failure to adhere to the spec and decided not to re-run the 2 failed iterations. (This only affects the code size and performance measurements; I have reported all 5 Angular iterations for the “cost” section.)

Maintainability

There are lots of ways to measure maintainability (including fancy things like cyclomatic complexity), but I decided to keep it simple and report something tried-and-true: total lines of code.

Lines of code
Framework Lines of code
Hono+htmx 1370 (1264–1489)
Nuxt 1415 (1382–1458)
Astro 1461 (1379–1503)
Vanilla 1492 (1437–1742)
Next.js 1539 (1452–1627)
React Router 1564 (1421–1621)
SvelteKit 1611 (1503–1816)
SolidStart 1630 (1528–1894)
TanStack Start 1697 (1506–1771)
Gracile+Lit 1717 (1412–1748)
Qwik 1744 (1646–1827)
Angular 2009 (2003–2051)

Number of non-blank source file lines. Median of 5 apps (3 for Angular).

To me this is one of the more interesting charts. In a sense, the whole point of a framework is to reduce the amount of code you need to maintain. In Rich Harris’s memorable words:

“Frameworks are not tools for organizing your code; they are tools for organizing your mind.”

– Rich Harris, “Rethinking reactivity”

In the agentic era, this hits even harder because the code size ultimately impacts the number of tokens, including in future edits, meaning it affects the literal cost to maintain a codebase over time. This doesn’t mean that succinctness is the only value that matters, but it’s certainly interesting to see that Hono+htmx can do roughly the same thing as Angular with ~70% of the code (and faster, to boot).

Conclusion

There are many ways you could improve this benchmark. For example, you could mandate that the agent use Service Workers for offline caching, or optimize the Pokémon image files, or use lazy-loading, fetchpriority, the Speculation Rules API, etc. Even though I know a lot of these performance hacks, I deliberately didn’t steer the agents toward any of them, because it’s not what a typical vibe-coder would do.

On the other hand, this entire benchmark could be cheated by just pre-generating all the HTML files as static assets (there are only so many Pokémon). I specifically forebade that in the instructions and made it an explicit check in the test harness, because I felt that it wasn’t in the spirit of the benchmark. (My aim is to simulate a large volume of possibly-dynamic data, which is more challenging than a purely static site.)

You could also improve the way I ran the benchmark by running more iterations, using different coding agents, trying different effort levels, using different frameworks or different versions of the frameworks, trying with and without MCPs/skills, testing in more browsers than just Chromium, building different types of apps, etc. I didn’t do these things mostly in the interest of time. However, if anybody wants to run the benchmark themselves or fork it, it’s on GitHub, so I encourage you to go nuts. I’ll happily report your results here (along with any mistakes or flaws you may find).

My goal here is not to write the end-all-be-all benchmark for “I’m a vibe-coder, which framework should I choose?” The point of this post is to demonstrate that it’s possible in the era of AI coding to still make decisions based on evidence rather than vibes. The benchmark itself is largely vibe-coded, although I did review the code, so if there are mistakes then please consider them mine. I’d also be perfectly happy to see competitor frameworks try to improve on what I’ve done here.

I’m also not trying to vilify any particular framework. I didn’t know which one would “win” before running the benchmark, although I did suspect that MPAs would outperform SPAs (which I’ve cheerfully admitted throughout this post). You could certainly write a more SPA-favored benchmark by making the app a complex dashboard, a video game, a productivity app, etc. You could also pick different metrics: Lighthouse and the Core Web Vitals are pretty tough on SPAs, so you could measure in-app interactivity instead.

I’m also sure that the framework authors themselves could improve their frameworks or AI tools and do better on this benchmark. I have a strong suspicion, for example, that the current crop of MCPs and skills may be doing more harm than good. So if this post is the last word on these frameworks’ performance, I’ll be sorely disappointed. I’m sure there are also emerging frameworks that are champing at the bit to do well on benchmarks such as this one.

If nothing else, I hope this post inspires you to apply additional scrutiny to your choice of JavaScript framework in this weird new era of vibe coding. Are you really picking your tech stack based on evidence? Or are you just going with what you already know? That’s certainly understandable, especially if you’re still reading, reviewing, and debugging the code yourself. But if you’ve gone largely hands-free, then what attachment do you really have to the choices you made back when you were typing code line-by-line like an animal? If you’re re-evaluating the entire way you build software, maybe it’s worth re-evaluating what you type after npm install? Just saying.

Personally, I don’t have any attachment to the frameworks I’ve evaluated above, except that I believe we should strive to build the best possible experiences for our users. I know and respect many of the authors of the above frameworks, and I know that they are working tirelessly for the same values I champion. My hope is just to push the entire community in a better direction toward better, faster, and more accessible frameworks to power the next generation of web apps.

Addendum

Framework versions used in the test (note Qwik had a patch release partway through the test):

Framework Version
Angular @angular/core 22.2.3
Astro astro 7.4.2
Gracile+Lit @gracile/gracile 0.11.0, lit 3.3.3
Hono+htmx hono 4.13.14, htmx.org 2.0.11
Next.js next 16.4.3, react 19.3.1
Nuxt nuxt 4.6.2, vue 3.5.44
Qwik @builder.io/qwik-city 1.20.2 (iteration 1), 1.20.3 (iterations 2, 3, 4, 5), @builder.io/qwik 1.20.2 (iteration 1), 1.20.3 (iterations 2, 3, 4, 5)
React Router react-router 8.5.0, react 19.3.1
SolidStart @solidjs/start 2.1.0, solid-js 1.9.17
SvelteKit @sveltejs/kit 3.1.0, svelte 5.57.2
TanStack Start @tanstack/react-start 1.169.4, react 19.3.1
Vanilla vite 8.3.5

You can comment on the fediverse or Hacker News.

Do web components have a future if nobody is writing them?

With my previous post on “using the platform”, I was surprised that the comments on Hacker News and Lobsters tended to veer toward web components. This surprised me because I didn’t mention web components at all in the post.

I was less surprised when I did some research and found that the entire “use the platform” mantra was first popularized by Polymer, which was Google’s early attempt at a web-components-based JavaScript framework. In 2018 they even called it “our motto”! So this explains why a lot of people seem to have it burned into their brains that “use the platform” means “use web components.”

Now, one of the reasons I didn’t mention web components in the post is that, in my opinion, they’re actually not a great example of “use the platform” (regardless of the motto’s origins). I instead leaned on examples of HTML and CSS features that tend to be more performant, more accessible, and require far fewer lines of code than the equivalent in JavaScript. This is largely thanks to the advances that the browser vendors and standards bodies (e.g. the CSS Working Group and Open UI project) have made in the past few years.

Modern Web Guidance

Web components popped into my head again, though, after a comment on Mastodon pointed me to Google’s Modern Web Guidance. I wasn’t aware of this tool before writing the post, but essentially it’s a set of skills that you can give to an agent to nudge them in the direction of using more modern browser features. Essentially, it’s “use the platform” for agents (or at least the Chrome team’s vision of it).

As I was watching the demo, though, I was struck by how hard it is to see where web components fit into the picture. In the presentation, Phil Walton shows how you can use Modern Web Guidance to create a web app that feels more like a native app in terms of following your finger smoothly as you touch the screen, thanks to features like CSS scroll snap points and scroll-driven animations.

But nowhere in the demo are we really seeing the code – this is pure vibe-coding. And as I watched, I struggled to think of any place where, if I were building Modern Web Guidance myself, I would put instructions to tell the agent, “Hey, you should use custom elements, shadow DOM, or other web component primitives here because it’s going to make the end user experience better.” And that’s because there’s nothing that web components can really do to make the end user experience better. (Arguably you could say that shadow DOM improves CSS performance, but I’ve benchmarked this to death, and you can effectively get the same boost with compile-time CSS scoping, which many frameworks and tools like Svelte, Vue, and Tailwind are already doing.)

So I took a look at the actual code for Modern Web Guidance, and my hunch was confirmed: web components feel like an afterthought. For the first six months of the project’s lifetime, there was almost no mention of web components. This only changed 3 weeks ago when a single commit landed, which mostly provides guidelines for those already using web components, around the topics of accessibility, pre-rendering, styling, etc. But nowhere does the Modern Web Guidance strongly recommend the agent to use web components – in fact, there’s a whole section titled “Decide whether you even need a custom element” which sounds downright discouraging at times. As written, if you throw the Modern Web Guidance at an agent building a brand-new product, it seems unlikely to me that it would choose to use web components.

To be fair, there is one exception: a line for Chrome extension authors recommends using shadow DOM for complex UI injected into a page. This seems perfectly sensible to me, although it’s a fairly narrow use case. Another line suggests using web components for “design systems, embeddable widgets, and self-contained UI,” but this is also fairly narrow, and reads to me as a faint nod within the larger context of “decide whether you even need a custom element.”

Web components at a crossroads

So where do web components fit into this new AI coding world? I’m not going to rehash all my points about web components’ strengths and weaknesses; I’ve already done that at considerable length. Instead I want to talk about what’s new if people are no longer writing the code (or increasingly, even reading it).

This section was painful for me to write, because I spent years working on a web-components-based framework (LWC), participating in the Web Components Community Group and Accessibility Object Model group, and defending web components on my blog, in podcasts, etc. And yet I have to acknowledge that their core value proposition have been severely weakened by agentic coding.

In short, web components were designed for a world where people typed HTML by hand, code was scarce, and thus reuse was an economic necessity. None of those things are true anymore, or at least they’re much less true and becoming less so by the day. So the elevator pitch of “build for a common standard and your components will be easier to pick up and reuse” becomes less compelling as more and more people are vibing UIs from scratch on top of a largely invisible platform substrate.

This doesn’t just devastate the value proposition of web components: it also impacts the JavaScript frameworks that inspired them. Increasingly I read about teams rewriting their entire frontend from one framework to another, based largely on the vibes of “it’s probably better for agents.” Regardless of the merits of these rewrites (which I think ought to be more evidence-based along the lines of “it uses fewer tokens” or “it’s a faster end-user experience”), we’re likely to see more of them in the future. And if people start seeing the framework layer as disposable, it seems unlikely to me that they would assign much value to the layer one step below that.

As I argued in a 2023 post, component libraries were already moving from a “batteries-included” model to a more “DIY” mindset. The apotheosis of this is shadcdn, which might seem like a machine for spitting out reams of boilerplate, but in a sense, that’s the point. Rather than make your touchpoint with a component system an inflexible API that resists your attempts to style or tweak it, you import the individual bits and pieces and then rearrange them at your will. This shift was already underway before AI coding, and now with LLMs “reams of boilerplate” are an advantage rather than a detriment. And none of this plays to the strengths of shadow DOM, reflected attributes, or other web component mainstays.

Salvaging web components

There are two (slightly weak) cases I can try to make for web components in this new world of AI coding.

The first is so-called micro-frontends, or the idea of separating a large UI into multiple sections that can be built by different teams without clobbering each other. Web components were explicitly designed for this scenario: avoiding “friendly fire” (in Brian Kardell’s memorable phrasing). In a world of agents, this kind of sandboxing is actually a well-known technique, because typically you want to isolate agents into strict boundaries to avoid them rampaging on top of each other. In other words, web components could theoretically allow you to have multiple swarms of agents working on separate parts of the UI without their CSS, events, or globals clashing with one another.

The obvious counterpoint is that the web already has a great sandboxing primitive, which is iframes. Iframes offer even firmer boundaries than web components: not only are components isolated from each other, but they also live in separate JavaScript realms with postMessage as the only communication channel. There are several tradeoffs that I don’t want to get into here (synchronous vs asynchronous communication, memory usage, the porousness of closed vs open shadow DOM), but I do think web components have a chance in this fight. (Arguably, the Modern Web Guidance’s “design systems and self-contained UI” are also examples of micro-frontend-like scenarios where web components can shine.)

The second argument is that reuse still matters even when rewrites are just a prompt away. For example, I still believe that my <emoji-picker> element is a good poster child for an effective web component: there is no reason to rebuild this thing in React, Vue, Svelte, and a dozen other frameworks, even though you could trivially do so with an LLM these days. It’s a standalone, client-only component, and most of its complexity lives in managing the emoji data, IndexedDB, etc., with little need for touchpoints with a framework to manage its rendering or lifecycle. So packaging it as a web component and allowing folks to wrap it in their framework layer of choice (or none at all) is still a sensible distribution model.

Arguing against web components as a lingua franca for reuse sounds (to me) like an argument that all code sharing or even open source in general is a waste of time in the agentic era. My colleague André Staltz has shown that this is clearly false: if anything, AI coding is increasing the amount of shared code, not decreasing it. Even if you can rebuild something with agents, that doesn’t mean that it’s worth spending the tokens to do so. (Although I do still wonder about smaller projects.)

The stronger counterargument is that agents already have a perfectly good component reuse layer, which is React. I don’t believe this, though, because I don’t think that React is the last JavaScript framework the world will ever need. Performance benchmarks like the js-framework-benchmark and isomorphic SSR benchmark still stubbornly argue against React adoption: as it turns out, the virtual DOM is still pure overhead. React could certainly reinvent itself for the agentic era, but I imagine this could lead to compatibility problems that would argue more strongly for web component adoption, if for no other reason than to get off the update treadmill.

Still, I think the strongest argument of all is that neither web components nor React nor any other framework really matter, except maybe as a footnote in someone’s AGENTS.md or an aside in an agent’s thinking process. Recently I vibe-coded a small app for my wife, and the agent mentioned that it was wavering between Preact and Svelte before finally settling on Preact. To be honest, I didn’t care at all because the app worked and solved the problem at hand.

Conclusion

I still have a soft spot for web components, and when push comes to shove I’ll happily defend them against any detractors.

There’s even a web component on this very site! The theme picker above is an “HTML web component” (or “light DOM component”), which I find quite nice as an alternative to the old-school “HTML with jQuery sprinkles” approach. Defining setup logic in connectedCallback just feels cleaner to me than awkward querySelector acrobatics. That said, I’ll admit that I had to explicitly tell my agent to use a web component (otherwise I’m certain it would have just shrugged and reached for querySelector).

Despite my sentimentality for web components, though, I think that the web component advocates working on standards, frameworks, or browsers will have to work twice as hard to argue for their relevance in an AI coding future. I no longer have a dog in this fight, but I looked at the status of some longstanding problems with web components and was disappointed to see that server-side rendering, stylesheet sharing, and accessibility still appear to be unsolved. These kinds of flaws – which mind you, directly impact the end-user experience and are thus unchanged with AI coding – will have to be fixed if web components want to have a fighting chance.

If web components don’t survive, or they wither away to forgotten infrastructure on a minority of websites, then what will they have accomplished? Well, I do think that one unambiguous victory for web components has been in “explaining the platform.” This was one of the original goals of web components: taking a native HTML element like <video controls> or <details>/<summary> and explaining how web developers could build it themselves from platform primitives. And in fact if you look at the browser DevTools, you’ll even see “user-agent shadow DOM” hiding inside these elements.

An example of Chromium's DevTools showing a video with controls having a user-agent shadow root. The video is shown on the left and the browser Elements panel in the DevTools on the right, with various elements inside shadow root.
The user agent shadow root for <video controls> in Chromium DevTools.

So from a standards and internal-consistency perspective, web components have been a clear win. They provide a degree of coherence between userland and the platform that, without them, might have felt ad-hoc or inconsistent from browser to browser.

This has also created a symbiotic relationship between standards and userland, where web component concepts have flowed back into standards and influenced the design of new platform features. You can see this most clearly in the Open UI project, where terms like “parts” and “slots” are frequently invoked (both of which come from web components), and which has landed several new features in browsers such as customizable <select>, popovers, and invoker commands, with more likely on the way.

So whatever happens to web components in the future, and regardless of whether web developers ignore them, forget them, celebrate their demise, or write them off entirely, nothing can take this victory away from them. They are woven into the texture of the web platform in a way that can never be undone. For me, this makes me happy even if I never write another web component again.

You can comment on the fediverse.