Engineering Diaries · Frameworks
§
FrameworksDXPerformance

What "Zero Bundle Size" Frameworks Actually Cost in Developer Experience

The pitch is very attractive: ship no JavaScript, score 100 on Lighthouse. But the developer experience of working with Qwik, Astro Islands, and their cousins is a different story altogether.

01 · The Promise

Zero JavaScript: The Most Seductive Metric in Frontend

There is something genuinely satisfying about watching a PageSpeed Insights score hit 100. The green circles. That small moment of smugness. The screenshot you immediately want to share in the team Slack. For years, getting this on anything more complex than a portfolio site felt nearly impossible. The bundlers always had opinions, and those opinions were measured in kilobytes.

Then came a new generation of frameworks with an audacious claim: ship zero JavaScript by default. Astro popularised the Islands Architecture, where everything renders to static HTML and only the bits that genuinely need interactivity get hydrated as isolated "islands." Qwik took a more radical path called resumability, serialising the entire application state into HTML so the client runtime stays tiny until the exact moment a user actually does something. Marco, Fresh, and a handful of others orbit similar ideas.

And honestly? The performance numbers are real. Teams migrating content sites to Astro have reported 40 to 60 percent reductions in Time to Interactive. E-commerce shops saw Core Web Vitals improvements that translated into actual revenue lifts. The frameworks were not lying about any of that.

What consistently gets left out of those launch posts, conference keynotes, and glowing benchmark comparisons is the other side of the picture: what developers give up, what mental models need to be rebuilt from scratch, and what the ecosystem looks like when you're six months into a project and need to ship something by end of day.

"Zero bundle size is a measurement of output, not a description of complexity. The complexity doesn't disappear. It relocates. Usually into your head."

02 · Definitions

What "Zero Bundle Size" Actually Means (and What It Doesn't)

Before getting into the costs, we should be precise about what these frameworks actually mean when they say zero JavaScript. The term is used quite loosely in the community, and the differences matter a lot for how the DX pain shows up in practice.

The Islands Architecture (Astro, Fresh)

In Astro's model, your page is plain HTML by default. Components written in React, Vue, Svelte, or Astro's own syntax render entirely at build or request time on the server. The resulting page ships no JavaScript unless you explicitly mark a component with a client: directive: client:load, client:idle, client:visible, and a couple of others.

The "zero bundle" claim is accurate for all the static parts. But the interactive islands do ship JavaScript. They're just bounded and deferred. A page with three interactive islands ships three separate bundles, each loaded on its own schedule. The innovation is in the isolation and lazy loading, not in the elimination of JavaScript altogether. That distinction matters when you're setting expectations with a product manager.

Resumability (Qwik)

Qwik's approach is architecturally more interesting. Rather than deciding at build time which components are islands, Qwik serialises the entire application state, including event listeners and component references, as HTML attributes during server rendering. The client runtime is deliberately tiny: it listens for events, finds the serialised handler reference in the HTML, and only then fetches and runs the specific chunk of JavaScript needed for that one interaction.

In theory, a full Qwik application can arrive with roughly 1KB of runtime JavaScript regardless of how large the app actually is. In practice, interaction-triggered loads and prefetching push this number up. But the architectural achievement is legitimate and worth understanding on its own terms.

The Distinction That Matters Islands architecture changes when JavaScript runs (at interaction, not at load). Resumability changes how much JavaScript runs (only the handler, not the full component tree). Both reduce initial bundle size, but through fundamentally different mechanisms. And those mechanisms produce very different DX costs that you'll want to understand before picking one.
0kb
Default JS on
Static Astro Pages
~1kb
Qwik Runtime on
First Paint
~130kb
Typical React App
Baseline (gzipped)
40%
Avg TTI Improvement
Reported by Adopters
03 · Mental Models

The Mental Model Shift Nobody Warns You About

If I had to name the single biggest DX cost of these frameworks, it wouldn't be tooling gaps or ecosystem immaturity. It would be this: you have to fundamentally change how you think about component boundaries. And that reconceptualisation is harder than any blog post makes it sound.

React developers have a deeply internalised model of how a component tree works. State flows down. Events bubble up. Effects run after render. It's a complete mental framework, and it's simply wrong for this new paradigm. You can't carry it over and expect things to behave the way you're used to.

The Boundary Problem in Islands

In Astro, the moment you mark a component as client:load, you've created a hard boundary. Nothing outside that boundary can share reactive state with what's inside, unless you wire up an external store explicitly. This sounds reasonable at first. It stops sounding reasonable the moment you build something with even moderate interactivity.

Think about a navigation bar that needs to show the user's cart count. In React, you'd lift state to a context provider and consume it wherever needed. In Islands, your navigation bar island and your product page island are two completely separate JavaScript runtimes. React context doesn't cross that boundary. You need either a URL-based state mechanism, a shared store like nanostores, or a custom event bus over window. All three options are technically correct. All three are experientially annoying when you just wanted to show a number in a nav bar.

A Real Scenario That Will Catch You Off Guard

Say you're building an e-commerce product page. The image gallery needs interactivity (keyboard navigation, zoom). The variant selector needs interactivity (updates price and stock status). The "Add to Cart" button needs interactivity (loading state, optimistic UI). In React, this is one component tree. In Islands, this is three islands that need to talk to each other whenever the user picks a variant. You will spend real engineering time on state synchronisation that React would have made completely invisible.

This isn't a flaw in the architecture. It's a consequence of it. Understanding the difference between a flaw and a consequence is how you decide whether the tradeoff is worth it for your particular project.

The Serialisation Mental Model in Qwik

Qwik's resumability requires you to think constantly about what can be serialised to HTML, which is a constraint React developers have never had to internalise. In React you can freely close over module-level values, function references, class instances, anything available in scope when the component renders. In Qwik, closures in event handlers must be serialisable. No Date objects, no class instances, no circular references, no DOM node references unless you use Qwik's specific primitives.

The Qwik compiler automates a lot of this through the $ suffix convention. But when the compiler can't handle your code, the error messages are terse in ways that assume you already understand the serialisation model. Developers new to Qwik hit these walls hard, and they hit them often.

"Islands architecture doesn't eliminate complexity. It moves it from runtime bundle size into explicit inter-island communication. That trade is good for users, and genuinely irritating for developers who weren't expecting it."

04 · Debugging

Debugging Across Two Execution Environments

React developers debug in the browser. Something renders oddly, you open DevTools, inspect the component tree, find the misbehaving bit, check its props and state, fix it. The tooling is so mature it feels obvious. We've forgotten it took years to get there.

Zero-bundle frameworks introduce a new category of bug that has no direct equivalent in traditional SPA development: the hydration boundary failure. Your component renders perfectly on the server. The HTML arrives in the browser looking correct. Then JavaScript loads, and the interactive layer either fails silently, attaches to the wrong element, or creates a visible flicker as client-side rendering overwrites the correct server HTML with something different.

The SSR / Client Mismatch Problem

In React, a server/client mismatch gives you a console warning: Warning: Prop className did not match. Annoying, but at least it's surfaced. In Islands-based frameworks, mismatches can be much quieter. The island simply re-renders from scratch on the client, throwing away the server HTML. You lose the performance benefit with no warning at all. You only find out when you're profiling, and most developers don't profile until something is already wrong in production.

In Qwik, mismatches are more visible because the resumability contract is strict. But when resumability breaks, debugging it means reading encoded strings in HTML attributes, not clicking through a DevTools panel. It's a genuinely different skill, and it takes time to develop.

DevTools: The Gap Is Real

React DevTools took years of community investment to become genuinely useful. Qwik's DevTools are functional but early-stage. Astro's debugging story depends on which component framework you're using inside your islands. React islands get React DevTools. Svelte islands get the Svelte inspector. Plain Astro components get neither. You end up switching debugging contexts mid-session, which adds friction in exactly the moments you can least afford it.

Astro Islands · A Silent State Sync Failure
// The problem: two islands that need to share state
// Island A: ProductVariantSelector.tsx (client:load)
import { atom } from 'nanostores';
import { useStore } from '@nanostores/react';

// Must be defined in a SEPARATE module, not inline
export const selectedVariant = atom(null);

export function VariantSelector() {
  const variant = useStore(selectedVariant);
  return (
    <select onChange={e => selectedVariant.set(e.target.value)}>
      {/* options */}
    </select>
  );
}

// Island B: AddToCartButton.tsx (client:idle)
// If you import selectedVariant from a different path alias,
// Vite quietly creates a second module instance.
// The button reads undefined. No error. Just wrong behaviour.
import { selectedVariant } from '../stores/variant'; // Works fine
import { selectedVariant } from '@stores/variant';    // Different instance entirely

I've seen teams spend two full days tracking down exactly this kind of bug. The import path aliasing issue creating separate store instances is invisible in a single-SPA codebase and absolutely pernicious in Islands. It doesn't throw. It doesn't warn. The cart button just never learns which variant the user selected, and everyone assumes it's a state logic bug, not an import path issue.

05 · Ecosystem

The Ecosystem Gap Is Larger Than the Changelogs Suggest

React's ecosystem is so large that most developers have stopped thinking about it as a feature. Need a date picker? Half a dozen solid options. A data table with virtualisation? Three mature libraries, thoroughly documented. A rich text editor? Tiptap, Lexical, Quill, take your pick. We take all of this completely for granted.

Now try building the same product in Qwik. The Qwik ecosystem, as of early 2026, is meaningfully smaller. Not because the framework is poorly designed (the architecture is genuinely clever), but because ecosystem depth is a function of time and adoption. Qwik is still young. This creates a failure mode that is completely invisible during the "hello world" phase and absolutely brutal during the "we need to ship this on Tuesday" phase.

The Component Library Problem

The most common DX friction point teams report after adopting Islands-based frameworks is component libraries. Shadcn/UI, Radix UI, Chakra, Material UI, the whole ecosystem of composable, accessible, production-ready React components will technically work inside Astro React islands. But with caveats. They hydrate as complete React trees, which partly undermines the bundle savings you were trying to achieve. They can't share state cleanly with non-React islands. And their server rendering stories were designed for Next.js, not Astro's component boundary model.

Qwik's component library ecosystem is improving, but teams building production applications have generally found themselves writing more primitives from scratch than they budgeted for. Accessible modals, tooltip systems, comboboxes. Work that a React developer would install in thirty seconds and move on from.

The Hidden Cost: Third-Party Integration Time

Teams who migrated production applications to Astro or Qwik consistently identify third-party integration time as the largest unexpected cost, typically running two to three times the original estimate. The pattern is the same every time: a library works in isolation, breaks at a framework boundary, needs a custom wrapper, the wrapper breaks on a minor version update, and you now own a fork of something you expected to never touch. It compounds quietly and steadily over the project lifetime.

This isn't a reason to avoid these frameworks outright. It's a reason to budget honestly and to prefer projects where the interactive surface area is small and clearly defined upfront.

Tooling Maturity

ESLint plugins, TypeScript integrations, testing utilities, Storybook adapters, CI/CD presets. The scaffolding that makes a large React codebase navigable is the product of years of community work. Astro's tooling is genuinely good for a framework its age. Qwik's ESLint plugin is functional but quite sparse. The testing story for both frameworks, especially component testing with Vitest and Testing Library, requires workarounds that still feel provisional.

Tooling Category React (Next.js) Astro Qwik
DevTools Extension Mature, widely used Partial (varies by island type) Early stage, limited
TypeScript Support Excellent, deep inference Good, improving fast Good, some edge cases
Component Testing Testing Library, mature Needs adapter setup Limited, workarounds required
E2E Testing Playwright/Cypress, seamless Works well Works, fewer examples
Storybook First-class support Community addon, stable Experimental
ESLint Integration Comprehensive, rich rules Good for .astro files Basic, sparse rule set
Stack Overflow Coverage Exhaustive Growing, good for common cases Thin; GitHub issues are primary resource
06 · Qwik Specifically

The Qwik Dollar Sign Tax: Resumability's Cognitive Overhead

Qwik deserves its own section because its DX costs are structurally different from the Islands frameworks. Astro's friction is mostly about architecture decisions and ecosystem gaps. Qwik's friction is baked into the syntax itself. You encounter it in every file, on every interactive line, every single day you work in the codebase.

Resumability requires the compiler to know which code runs on the server, which runs on the client, and which crosses the boundary between them. The way you communicate this to the compiler is through the $ suffix convention: component$, useSignal$, onClick$, useTask$. Any function that crosses the server/client boundary must be explicitly wrapped with $().

The Serialisation Constraint in Practice

The constraint is enforced at compile time, which is actually a good thing. You find out fast when something isn't serialisable. But the error messages when the compiler rejects your code come from a bespoke optimiser with its own vocabulary. Terms like "symbol capture," "lazy-loaded closure," and "QRL extraction failure" are precise and meaningful to someone who has studied the Qwik internals carefully. To a developer on their second day with the framework, they read like a different language entirely.

Qwik · What the $ Convention Looks Like Day-to-Day
import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export const ProductCard = component$(async ({ productId }) => {
  const quantity = useSignal(1);
  const isAdding = useSignal(false);

  // useTask$ runs on server AND client (before render)
  useTask$(({ track }) => {
    track(() => quantity.value);
    // Runs whenever quantity changes
  });

  return (
    <button
      // onClick$ is a QRL: a serialisable handler reference
      onClick$={async () => {
        isAdding.value = true;
        // Cannot close over non-serialisable values here
        // Cannot reference DOM nodes directly
        await addToCart(productId, quantity.value);
        isAdding.value = false;
      }}
    >
      {isAdding.value ? 'Adding...' : 'Add to Cart'}
    </button>
  );
});

The code above is clean, idiomatic Qwik. But notice what you're actively managing at all times: which values are signals rather than plain variables, which functions need the $ wrapper, which values can safely be closed over inside a $ boundary. This is a permanent cognitive overhead on every interactive component you write. It doesn't disappear with experience. It becomes routine. Routine and invisible are very different things, and this one never quite becomes invisible.

The Honest Assessment Qwik's DX is significantly better than it was at version 1.0, and the team has genuinely invested in improving error messages and documentation. But as of 2026, writing Qwik components requires a level of compiler awareness that React, Vue, and Svelte have never asked of their developers. For teams who internalise the model, it becomes workable. For teams that are regularly onboarding new people, the ramp-up cost is real, recurring, and not something you can train your way out of quickly.
07 · Astro Specifically

Astro's Specific DX Costs: Where the Islands Model Starts Straining

Astro's developer experience is, relatively speaking, the most polished of the zero-bundle options currently available. The documentation is excellent. The CLI is fast. Error messages are clear and actually helpful. For content-heavy sites with a small interactive footprint, things like marketing pages, documentation portals, blogs, and landing pages, Astro is a genuine pleasure to work in.

The friction surfaces specifically when the interactive surface area grows. Not all at once, but gradually. Each new island introduces a question about which client directive to use. Each piece of shared state raises another question about how islands should communicate. Each third-party library triggers a negotiation about whether it hydrates cleanly as an island or needs a wrapper. These decisions are individually small and collectively exhausting over the life of a long project.

The client: Directive Decision Fatigue

Astro gives you five client directives: client:load, client:idle, client:visible, client:media, and client:only. Each optimises for a different loading scenario. In principle this is exactly the kind of granular control you want. In practice, picking the wrong one produces bugs that are difficult to diagnose because they're entirely timing-dependent.

A modal using client:idle works fine on a fast laptop. The idle callback fires quickly, the modal hydrates before anyone taps it. On a mid-range Android phone under CPU throttle (which describes a huge chunk of mobile traffic in India and other high-growth markets), idle fires late. The user taps the trigger, nothing happens for 400 milliseconds, and then the modal appears. The bug is real in production and completely invisible in local development. Working out whether you chose the wrong directive or whether your modal component itself has a bug requires the kind of profiling discipline that most teams don't apply routinely.

The .astro File Format Learning Curve

Astro files use a format unlike anything else in the frontend world: a frontmatter-style code fence at the top for server-side JavaScript, followed by a JSX-like template below. The separation is clean once you've understood it properly. But the mental shift required (the top half has no access to browser APIs; the bottom half renders as plain HTML by default) catches developers repeatedly in the first few weeks on a project. You'll reach for window in frontmatter at least twice before it sticks, I promise.

Astro Component · Execution Environment Map
Server / Build Time
Frontmatter (---)
No window, no document, no localStorage
HTML Template
Static by default
+ client: directive
Island (JS ships)
Island internals
Own JS runtime
Isolated from other islands

This is what every Astro developer needs to hold in working memory at all times. The execution environments are distinct, the boundaries are hard, and the failure mode when you cross them incorrectly ranges from a clear build error (accessing window in frontmatter) to a subtle runtime problem like assuming two islands share a module instance when they simply don't.

08 · The Verdict

When the Tradeoff Is Actually Worth It, and When It Isn't

Calling zero-bundle frameworks universally better than traditional SPAs is a marketing position, not an engineering one. The real question, the only question that actually matters, is whether the performance outcome is worth the DX cost for your specific project, with your specific team. That answer varies enormously depending on what you're building and who's building it.

Developers who've shipped real production applications on these frameworks are converging on fairly consistent opinions about where the line sits. Here is the honest version of that guidance.

Use Cases Where the Tradeoff Is Clearly Worth It

Marketing and content sites. A company's main website, product landing pages, documentation portals. These have large static surface areas and small, well-defined interactive zones: a contact form, a pricing calculator, a cookie consent banner. Traffic volume makes Core Web Vitals improvements directly measurable in business outcomes. Astro is exceptionally well-suited here. The DX cost is low because the interactive complexity is bounded and the team can reason about each island in isolation without much ceremony.

E-commerce product and category pages. Product listing pages are overwhelmingly static. The interactive elements (filtering, sorting, add-to-cart) are discrete and separable. The sensitivity of conversion rates to LCP and INP makes the performance investment genuinely worthwhile. Teams report that the inter-island complexity, while real, is manageable when island boundaries are designed clearly from the start of the project rather than retrofitted later.

Blogs and editorial platforms where content is the primary value, the interactive layer is thin, and page performance meaningfully affects reader retention and search ranking.

Use Cases Where You Should Think Twice

Complex web applications. If your product looks anything like Figma, Notion, or Linear (tools where users spend long sessions with deeply stateful interfaces), a zero-bundle architecture will fight you continuously. The state synchronisation story, the component library gaps, and the debugging complexity all multiply as application complexity grows. React with Suspense and Server Components remains a better fit for application-like products, and the honest answer is that it probably will for a while yet.

Teams with high turnover or a lot of junior developers. The DX costs are partly learning curve costs. A senior team that has fully internalised the mental models will eventually reach a point where the constraints feel natural. A team that's regularly onboarding new people will pay the ramp-up cost again and again. It adds up faster than most engineering managers account for.

Projects with heavy third-party integration requirements. If your roadmap includes Stripe's embedded UI, a complex CMS preview mode, or a vendor design system built in React, your integration friction budget is already high. A framework that magnifies integration complexity at boundaries will cost you more than you expect.

~3x
Reported Integration
Time Overrun
4-6wk
Typical Onboarding Time
for Experienced React Devs
60%
Teams Who Reported
Unexpected DX Issues
~85%
Said Performance
Gains Were Real
09 · Practical Guidance

Before You Adopt: A Practical Due-Diligence Checklist

If you're evaluating one of these frameworks for a real project, here's the process I'd recommend. It's the kind of due diligence that avoids the failure modes teams consistently fall into when they adopt too enthusiastically and too quickly.

  1. Map your interactive surface area first. List every part of the application that genuinely requires JavaScript: every form, every modal, every piece of dynamic content. If you end up with more than 15 to 20 distinct interactive zones that need to share state with each other, you're likely in application territory rather than content-site territory. Revisit your framework choice before writing any production code.
  2. Prototype your three most difficult third-party integrations before committing. Not the trivial ones. The ones with custom rendering lifecycles, imperative APIs, or internal use of React context. Spend a few days on this before the team aligns on a decision. The prototype will surface integration pain cheaply, well before you're six months in and can't reasonably turn back.
  3. Run a structured spike with two developers for one week. Have them build a representative 20 percent of the application's interactive features in the target framework. Measure not just whether it works, but how long it actually took, what surprised them, and what they'd need to explain to the next person joining the team. The DX cost compounds with team size, so get honest data on this early.
  4. Audit your testing requirements against what the framework can actually deliver. If your organisation has mandatory testing standards (coverage thresholds, component testing requirements, visual regression pipelines), validate that each one has a credible path in the target framework before adopting. Retrofitting testing infrastructure into a framework with limited tooling support is genuinely painful work, and it always happens at the worst possible moment in a project.
  5. Define your success metric before you start, not after. "Better performance" is not a success metric. "INP below 200ms at the 75th percentile on our three highest-traffic pages" is a success metric. Without a number to aim at, you have no way of knowing after the fact whether the DX tradeoff was justified. And if you can't demonstrate that, the conversation with your tech lead at the six-month mark will be uncomfortable.
The Most Common Way Teams Get This Wrong Someone watches a conference talk, gets genuinely excited (understandably so, these frameworks are impressive), and the team adopts without running any of the steps above. Enthusiasm is not due diligence. An impressive solution applied to the wrong problem still produces a bad outcome. The frameworks are good enough that it's worth being rigorous about whether they're right for your specific situation, rather than just the situation the demo was designed for.
10 · Conclusions

The Honest Accounting

Zero-bundle frameworks are a genuine architectural step forward. The performance outcomes are real. The underlying ideas are clever. For the right class of application, they produce results that would be very difficult to achieve any other way. I'm not writing this as a sceptic who thinks the old way is always better. I've seen Astro work beautifully on content sites. I've seen Qwik deliver performance numbers that made product managers genuinely happy. The point is to be honest about the full picture, which the enthusiast community has been slow to present.

The DX costs are real and systematic. The mental model shift is significant and doesn't fully dissolve with experience; it just becomes more familiar. The debugging complexity around server/client boundaries introduces a category of failure that experienced React developers have no prior intuition for diagnosing. The ecosystem gap is slowly closing but remains material for teams with comprehensive integration needs. And the tooling maturity, though improving with each release, still falls noticeably short of what a decade of community investment has given the React ecosystem.

The right framing for zero-bundle frameworks is not as universal replacements for React or Vue. They are the correct default for a specific, well-defined class of problem: content-heavy sites with bounded interactivity, where Core Web Vitals improvements translate directly into measurable business outcomes, and where the team has the patience to internalise a genuinely different way of thinking about components and boundaries.

Used there, they're excellent. Used as general-purpose application frameworks in the way React gets used today, they become a source of sustained friction that no Lighthouse score will fully compensate for. Your team will feel it. Your estimates will miss it. And your users won't even notice the difference, because your application's initial JavaScript bundle was not actually the bottleneck to begin with.

The bundle size is zero. The tradeoffs are not.

· · ·
Frameworks Discussed

Astro 4.x Qwik 2.x Fresh 2.x Marco 6.x React 19 (baseline) Nanostores Vite

Observations are drawn from production adoptions, public post-mortems, GitHub issue trackers, and patterns seen across teams who migrated content sites and applications to Islands-based or resumability-based frameworks between 2024 and early 2026. No single source is definitive; the goal is to triangulate from multiple real experiences rather than rely on any one team's story.

Framework APIs and ecosystem maturity change quickly. Version numbers reflect the state of each project in Q1 2026. Specific DX observations may not hold for future releases, particularly Qwik, which has improved its error messages and tooling with each minor version. The architectural tradeoffs, however, are structural in nature and unlikely to resolve on a minor release cycle.