EmpireUI
Get Pro
← Blog9 min read#qwik#resumability#performance

Qwik Framework: Resumability, Lazy Loading and Zero Hydration

Qwik skips hydration entirely with resumability. Here's how it actually works, why it matters for TTI, and when you should switch from Next.js.

abstract light beams representing lazy JavaScript loading and resumability

The Hydration Problem Nobody Wants to Talk About

Every SSR framework you've used — Next.js, Remix, Nuxt, SvelteKit — does the same thing after sending HTML to the browser: it downloads your entire JavaScript bundle, boots the framework runtime, walks the whole DOM tree, and re-registers every event listener. That process is called hydration. It's also the reason your Lighthouse score shows a great FCP but a terrible TTI.

Look, the HTML is there immediately. The user *sees* a page. But they can't *interact* with it until hydration finishes — which, depending on your bundle size, could be 2–8 seconds on a mid-range Android device. You've served them a beautiful picture of a button. Not an actual button.

Qwik's answer to this isn't 'do hydration faster.' It's 'don't hydrate at all.' That sounds like marketing nonsense until you understand *how* they pull it off. Worth noting: Misko Hevery, the creator of AngularJS, spent years thinking about this exact problem before building Qwik 1.0 in 2022 — so there's serious theory behind the approach.

The core insight is that the server already did the work. The DOM is already in the right state. Why would you replay all that computation in the browser just to attach a click handler? Qwik serializes the application state *into* the HTML itself, so the browser can pick up exactly where the server left off — no replay required.

What Resumability Actually Means

Resumability is the idea that execution can be *paused* on the server and *resumed* on the client without starting over. In practice, Qwik serializes three things into your HTML: component state, the event handler URLs (as q:id attributes), and the application's execution context. The browser loads nothing until an event fires.

Here's a minimal Qwik component so you can see what you're working with:

import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);

  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick$={() => count.value++}>
        Increment
      </button>
    </div>
  );
});

Notice the $ suffix on component$, onClick$, and useSignal. That dollar sign is Qwik's optimizer hint — it marks a function boundary that can be lazy-loaded as a separate chunk. The Qwik compiler (running at build time via Vite) splits your code at every $ boundary and generates individual chunk files. The onClick$ handler above becomes its own 300-byte chunk that only downloads when the user actually clicks.

That said, the serialization magic happens in the HTML output. When Qwik server-renders this component, it emits something like <button q:id='a1' on:click='chunk-a1.js#Counter_onClick'>. The tiny Qwik loader script (~1 KB) parses those attributes and fetches the right chunk on demand. Your initial JS payload for a Qwik app is roughly 1 KB regardless of app size. One kilobyte. Compare that to the 45 KB React runtime you're shipping today.

Fine-Grained Lazy Loading vs Islands Architecture

You might be thinking 'this sounds like Astro's islands.' It's not quite the same thing. Astro's islands (and Marko's partial hydration) still hydrate *islands* — discrete interactive components — using their full framework runtime. You're still shipping React or Vue for those islands. Qwik's lazy loading goes much deeper: it's per-event-handler, not per-component.

Quick aside: Astro with a React island still downloads the React runtime (~45 KB) the moment that island scrolls into view. With Qwik, an equivalent component downloads only the code for the specific event that fired. If you have a dropdown that the user never opens, that dropdown's open/close logic never downloads. Ever.

The difference shows up hard on interaction-heavy pages. Consider a typical SaaS dashboard with 40 interactive components — filters, charts, modals, dropdowns. With React SSR, you're hydrating all 40 upfront. With Astro islands, you're hydrating each component as it enters the viewport. With Qwik, you're downloading nothing until the user actually touches something. Honestly, for apps where most users leave after reading the hero section, Qwik's model is just objectively better.

// This entire subtree is lazy — the modal code downloads
// only when the trigger button is clicked
import { component$, useSignal } from '@builder.io/qwik';

export const LazyModal = component$(() => {
  const open = useSignal(false);

  return (
    <>
      <button onClick$={() => (open.value = true)}>
        Open Modal
      </button>
      {open.value && (
        <div class="modal">
          <p>This rendered lazily.</p>
          <button onClick$={() => (open.value = false)}>Close</button>
        </div>
      )}
    </>
  );
});

The downside — and there is one — is that the *first* interaction after page load has a tiny network round-trip to fetch the handler chunk. On a fast connection it's imperceptible (< 50ms). On a slow 3G connection it's noticeable. Qwik mitigates this with prefetching: it speculatively fetches chunks the user is likely to need based on cursor position and scroll depth. In Qwik 2.0+ this prefetch strategy became significantly smarter.

Setting Up a Qwik Project in 2026

Qwik City is the meta-framework layer on top of bare Qwik — think of it like the difference between React and Next.js. For anything beyond a toy demo you want Qwik City. It handles routing, loaders, middleware, and SSR out of the box.

npm create qwik@latest
# Choose: Empty App or Qwik City App
# Qwik City is what you want for real projects

cd my-qwik-app
npm install
npm run dev

Qwik City's routing is file-based, similar to Next.js App Router. Your src/routes/index.tsx maps to /. A layout.tsx wraps all children. The key difference is how you fetch data — Qwik uses routeLoader$ instead of getServerSideProps or server components:

import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const useProducts = routeLoader$(async () => {
  const res = await fetch('https://api.example.com/products');
  return res.json();
});

export default component$(() => {
  const products = useProducts();

  return (
    <ul>
      {products.value.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
});

The routeLoader$ runs server-side and its result is serialized into the HTML payload — the same resumability trick applied to data fetching. One more thing — Qwik City also has routeAction$ for mutations, which maps roughly to Remix's action or Next.js Server Actions. The mental model transfers if you've used either.

Signals, Stores and Reactivity

Qwik's reactivity system centers on signals — the same primitive that SolidJS popularized, now everywhere after React 19 introduced its own signal-adjacent model. In Qwik, useSignal wraps a single value; useStore wraps an object. Both are deeply reactive at the property level.

import { component$, useStore } from '@builder.io/qwik';

export const Form = component$(() => {
  const form = useStore({
    name: '',
    email: '',
    submitted: false,
  });

  return (
    <form
      onSubmit$={(e) => {
        e.preventDefault();
        form.submitted = true;
      }}
      preventdefault:submit
    >
      <input
        value={form.name}
        onInput$={(e) => (form.name = (e.target as HTMLInputElement).value)}
      />
      <input
        value={form.email}
        onInput$={(e) => (form.email = (e.target as HTMLInputElement).value)}
      />
      <button type="submit">Submit</button>
      {form.submitted && <p>Thanks, {form.name}!</p>}
    </form>
  );
});

In practice, useStore behaves like a Proxy-wrapped object. Mutating form.name directly triggers a re-render of only the parts of the JSX that read form.name — not the entire component. This is fine-grained reactivity at 0 to 16px of boilerplate. Compare that to useState where any state change re-renders the whole component subtree unless you memoize aggressively.

Worth noting: Qwik's reactivity doesn't use a virtual DOM diffing algorithm. It tracks which DOM nodes depend on which signal values at the template level and updates them surgically. This is why Qwik tends to outperform React on complex, frequently-updating UIs even after React has fully hydrated.

When to Actually Use Qwik (and When Not To)

Qwik shines hardest on content-heavy sites with interactive islands — e-commerce product pages, marketing sites, documentation, media publications. Anywhere your users are likely to bounce without interacting, or where the perceived performance gap between your app and competitors costs you conversions. If you care about Core Web Vitals and your current React app has a TTI above 3 seconds, Qwik is worth a serious look.

It's less compelling for highly interactive apps where the user is constantly touching the UI — think Figma, Notion, or a real-time dashboard. In those cases the user will have triggered enough chunk downloads that the lazy-loading advantage shrinks, and you're dealing with Qwik's learning curve for no real TTI win. For those apps, React with good performance practices and aggressive bundle analysis probably makes more sense.

The ecosystem is also still maturing. As of 2026, Qwik's component library story is thinner than React's. You won't find a Qwik port of every headless component you're used to. That said, Qwik supports qwikify$() — a wrapper that lets you drop in any React component as a Qwik island. It's not free (you pay the React runtime cost for those components), but it's a practical migration path. You can start using Qwik City for routing and SSR, drop in React components where you need ecosystem coverage, and gradually rewrite them as native Qwik components.

In terms of design system compatibility — Qwik's JSX is close enough to React's that adapting components is straightforward. You can pair it with Tailwind just like you would in any other framework. If you're building a custom design system from scratch and want UI inspiration, browse the Empire UI component library for patterns that translate cleanly regardless of framework. The glassmorphism components and effects from our glassmorphism generator are purely CSS-driven, so they work in Qwik with zero modification.

Measuring the Real Performance Difference

Numbers matter here. A Qwik-rendered page typically ships 1–2 KB of JavaScript for the initial interaction layer. A comparable Next.js 14 page with SSR ships somewhere between 80–200 KB depending on your dependencies. That gap translates directly to Time to Interactive on constrained devices — Qwik pages routinely hit TTI under 1 second on a Moto G4 at simulated 3G (25 Mbps throttled), where Next.js equivalents land at 3–6 seconds.

You can verify this yourself with Chrome DevTools' Performance panel. Record a page load, look at the 'Long Tasks' section. On a hydrating React app you'll see a solid block of scripting work 200–800ms wide right after the HTML parses. On a Qwik app that block is absent — there's nothing to execute. The main thread stays idle until the user does something.

# Analyze your Qwik bundle with the built-in stats
npm run build
npm run preview
# Open http://localhost:4173 and check the Network tab
# Filter to JS — you should see ~1KB for qwikloader.js
# Everything else is fetched on-demand

One more thing — Qwik's build output includes a q-manifest.json that maps every $-boundary chunk to its file. You can inspect this to understand exactly what gets prefetched and when. It's one of the more transparent build artifacts I've seen from any meta-framework. Pair it with the gradient generator and box shadow generator for your UI polish, and you're shipping a fast *and* good-looking product.

FAQ

Is Qwik production-ready in 2026?

Yes. Qwik 2.0 shipped in late 2024 and the ecosystem has matured significantly. Builder.io runs Qwik in production at scale. It's ready — just expect a thinner third-party component ecosystem than React.

Can I use React components inside a Qwik app?

You can with qwikify$(), which wraps a React component as a Qwik island. You'll pay the React runtime cost (~45 KB) for those components, but it's a solid migration strategy.

Does Qwik work with Tailwind CSS?

Perfectly. Tailwind is framework-agnostic — you install it via PostCSS just like in any Vite project. The Qwik CLI even has a Tailwind starter template.

What's the difference between Qwik resumability and Astro islands?

Astro islands hydrate discrete components using their full framework runtime. Qwik serializes execution context into HTML and only downloads individual event handler chunks on demand — no component-level runtime needed upfront.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

SolidJS Introduction: Fine-Grained Reactivity Without a VDOMWeb Animations API: element.animate(), Keyframes, Timing and GroupsWeb Animations API in 2026: Native Animations Without a LibraryFrontend Framework Wars 2026: The Definitive Comparison Guide