Development8 min read

Why I'm Building a Web Framework Where JavaScript Is Optional

By the QUFF Team

Most web pages don't need to be applications. A blog post, a product page, a documentation site - most of what's on them is just content. But the modern default ships them all the same way: as a JavaScript application that happens to render some text. Stoneware starts from the opposite assumption. HTML is the default, and JavaScript is something a component has to explicitly opt into.

A glossy teal-glass letter S standing on a deep blue background, beside the Stoneware wordmark and the line 'HTML by default. JavaScript by choice.'

Most pages are documents, not applications

The framing that got lost somewhere in the last decade is that a web page and a web application are different things. A documentation site is a document. A product page is a document with a button on it. A blog post is a document, full stop. None of them stop being documents just because the tooling that produced them was built for applications.

What the application-shaped default costs you is not abstract. The browser downloads a runtime, parses it, executes it, and then re-creates in memory a DOM the server already sent as text. On a fast laptop on a fast connection this is invisible. On a mid-range phone on a congested network - which is the median condition for most of the world - it is the difference between a page that is readable immediately and one that is readable in three seconds.

Stoneware inverts the default rather than adding an escape hatch to it. You do not opt out of shipping JavaScript; you opt in, per component, and you can see exactly which components did.

What Stoneware actually is

It's a Bun-native framework - not Bun-compatible, actually built on Bun's own APIs. Routing, bundling, escaping and CSRF all come from the runtime rather than from a stack of dependencies layered on top of it.

Every route renders to complete HTML on the server. If a page has no interactive components, it ships zero bytes of JavaScript. If a component does need interactivity - a cart button, a theme switcher, a search box - it becomes an island, and only that island's code reaches the browser.

  • Bun.serve for the HTTP layer, so there is no separate server framework to configure.
  • Bun.build for bundling island code - one bundler, already in the runtime.
  • Bun.escapeHTML for auto-escaping every interpolated value by default.
  • Bun.CSRF for token generation and verification on state-changing requests.
  • Bun.FileSystemRouter for file-based routes without a routing dependency.

The numbers, not just the pitch

A framework author claiming their framework is fast is worth nothing on its own, so I built the same 16-page site three times - matching content, matching five interactive components - once in Stoneware, once in Astro, once in Next.js, and measured all three the same way.

Same 16 pages, same content, same five interactive components. Lighthouse, mobile profile, throttled, 10 runs per page, median reported. My own measurements on the versions named - re-run them on your own hardware before treating them as universal.
MetricStonewareAstro 5.18Next.js 15.5
JS transferred14.2 KB193.1 KB346.0 KB
LCP1217 ms2253 ms2965 ms
Lighthouse performance1009995
Cold build, 16 pages0.71 s35.6 s61.6 s

What the Lighthouse score hides

That is 13.6x less JavaScript than Astro and 24.4x less than Next.js, for the exact same interactive functionality. The build-time gap is the one that changes how the work feels day to day - a cold build in under a second instead of a minute means you stop context-switching while you wait.

But look at the Lighthouse row again: 100, 99, 95. Those scores look close enough to ignore, and they hide the 24x spread completely. A perfect score does not mean nothing was shipped. It means what shipped did not block paint. Lighthouse is measuring whether the JavaScript got out of the way in time, not whether it needed to be there - so if payload size is what you care about, measure payload size.

The same caveat runs in the other direction, and I would rather say it than have someone else say it for me: a synthetic benchmark on one site shape is evidence about that site shape. Build your own version of the page you actually ship and measure that.

Secure by default, not by remembering to configure it

Auto-escaping, CSRF verification and a restrictive Content-Security-Policy are on before you write a line of config. The unsafe path - raw(), dangerouslySetInnerHTML - is there when you need it, and it is named to be easy to grep for and easy to review.

The idea is simple: you shouldn't have to remember every security primitive before deploying a basic site. Defaults are the only security control that protects people who never read the docs, and most people never read the docs.

Actively hardened, not just shipped and abandoned

Two recent patch releases focused entirely on production reliability.

v0.1.8 fixed a deployment bug where Vercel apps would render HTML correctly but silently 404 on CSS and island JavaScript in production - while local testing passed and the page still returned 200. That combination is the worst kind of bug: nothing in your development loop tells you it is happening, and the status code says the page is fine. The fix embeds generated assets directly into the server bundle for Vercel deployments, so they cannot get lost between build and static hosting.

The same release also closed a prototype-pollution-shaped hole in asset lookup, where a request to a path like /_stoneware/toString could accidentally resolve to an inherited JavaScript object method instead of a clean 404. Nothing exploitable was demonstrated, but a lookup that can return an inherited property instead of your own data is a class of bug worth removing rather than reasoning about.

Both were caught by an edge-case test sweep, not by the initial build. Stoneware now runs 500+ automated tests, and every bug fix ships with a regression test that fails against the pre-fix code - which is the part that matters. A test written after a fix, that was never seen to fail, proves nothing.

Where it fits - and where it doesn't

Stoneware's sweet spot is anything where most of the page is content and only part of it is interactive. If you're building something closer to a browser-based app, a client-heavy framework is still the right tool, and I would rather tell you that here than have you find out three weeks in.

Stoneware isn't trying to replace those frameworks. It's making a deliberate bet for the much larger category of sites that don't need one.

The dividing line is whether the page is mostly content with pockets of interactivity, or mostly interactivity.
Good fitReach for something else
Documentation sitesCollaborative editors
Blogs and publicationsReal-time dashboards
Marketing and landing pagesBrowser games
Product catalogues and business sitesAnything with a heavy client-side state model

Try it

No config required to get HTML-first, secure-by-default, and a client bundle that's 0 bytes until you actually ask for one.

bun create stoneware my-app
cd my-app
bun dev
  • Source: github.com/stoneware-dev/stoneware-core
  • If you try it, I'd genuinely like to know what breaks - that's how the last two releases got better.

The bottom line

Now go test yourself

The bet behind Stoneware is that the web's default should match what most of the web actually is: documents with some interactive parts, not applications that happen to contain text. Making HTML the default and JavaScript the opt-in is not a performance trick - it is a statement about which case deserves the easy path.

If you write JavaScript for a living, the fastest way to sharpen the instincts this argument rests on is to test them. Run a JavaScript or Node.js quiz, see which of the language's actual behaviours you can explain without checking, and go from there.

FAQs

Frequently asked questions

Does Stoneware really ship zero JavaScript?

On a page with no interactive components, yes - zero bytes, not a minimal runtime and not a hydration shim. As soon as a page contains an island, that island's code ships, and nothing else does.

What is an island?

A component you explicitly mark as interactive. The server still renders its HTML, but its JavaScript is also bundled and sent to the browser so it can respond to input. Everything not marked as an island stays server-rendered HTML.

How is this different from Astro?

The architectural idea - server-rendered HTML with interactive islands - is the same, and Astro deserves the credit for popularising it. The differences are that Stoneware is built directly on Bun's own APIs rather than being runtime-agnostic, and that the measured baseline payload came out much smaller on a matched build: 14.2 KB against 193.1 KB for the same 16 pages and the same five components.

Do I need Bun to use it?

Yes. Stoneware is Bun-native rather than Bun-compatible - it uses Bun.serve, Bun.build, Bun.escapeHTML, Bun.CSRF and Bun.FileSystemRouter directly. That is what removes the dependency stack, and it is also the constraint you are accepting.

Is it production-ready?

It is at 0.1.x, which is the honest answer to that question. There are 500+ automated tests and every bug fix ships with a regression test, but two recent patch releases fixed real production issues - a Vercel asset-serving bug and a prototype-pollution-shaped lookup hole. Treat it as early software that is being actively hardened, and pin your version.

Why do the Lighthouse scores look so close when the JavaScript payloads are 24x apart?

Because Lighthouse's performance score measures whether JavaScript blocked paint, not how much of it was sent. A well-deferred 346 KB bundle can still score 95. If payload size is what you care about - and on slow networks and cheap devices it is - measure payload size directly rather than reading it off the score.

Should I rewrite my existing site in it?

Not on the strength of a benchmark. Build the one page you care most about, measure it under the conditions your actual users have, and decide from that. The comparison in this post is a matched synthetic site, which is evidence about that site shape and not a promise about yours.

Related quizzes

Put it into practice

Keep reading

Related articles

Browse all articles →

Test yourself in two minutes

Six adaptive questions, every answer explained by an AI tutor. Free.

▶ Start an AI quiz