manu·martínez-almeida

Open Source

Qwik: Resumability That Feels Like React

· Manu Martínez-Almeida

When I joined Builder.io to work with Miško Hevery, Qwik already proved the hard part possible. You could load a web page and make it interactive without ever re-running your application in the browser. No hydration. The catch was that writing a Qwik app, in that first version, was a chore.

I knew Miško and Adam Bradley from my years on Ionic, which is how I ended up on the open-source team. The pitch that pulled me in was resumability, and the work that needed doing was making it something a normal developer would want to use.

Performance is a human problem, not a technology one

The thing I came to believe working on Qwik is that the web’s performance problem isn’t really a technology problem. It’s a design and developer-experience problem. Developers, like water, follow the path of least resistance, and in most frameworks the easy way to build something is also the way that ships a slow site. We then tell people to go optimize it afterward, as a chore, trading off capabilities or DX or their weekend to claw the performance back.

I find it more honest to stop blaming developers and fix the path instead. A framework should be designed so the path of least resistance lands you on a fast site by default. You shouldn’t have to be a performance expert to ship a fast page; you should have to work to ship a slow one.

And the thing worth optimizing is almost always JavaScript. Most sites already handle their images and CSS reasonably well, so there’s little left on the table there. JavaScript is where the big wins hide — the difference between a sluggish page and a snappy one is often tens of points of score, and for anyone running an e-commerce or consumer site, that gap is revenue. Resumability is the lever Qwik pulls on exactly that.

The idea: resume, don’t rebuild

Every mainstream framework hydrates. The server renders HTML, then the browser downloads the whole component tree as JavaScript, re-executes it, and reattaches event listeners to reconstruct state the server already had. You pay, in bytes and CPU, to rebuild something you were handed seconds ago.

Islands and partial hydration — the approach Astro popularized — make this better by only hydrating the interactive bits. But each island still has to download and execute its JavaScript before it’s ready, and someone has to draw and maintain those island boundaries by hand.

Resumability skips the rebuild entirely. The server serializes the application’s state and its event wiring into the HTML itself. The browser ships almost no JavaScript up front. Conceptually the app becomes a hashmap: an event on an element points at the one chunk of code that handles it. When you click a button, Qwik reads an attribute on that element, fetches that small chunk, restores the state it captured, and runs it. There’s no global “boot the app” step, because there’s no app to boot.

The neat way to see it: in a hydrated app, event handlers are the last thing to become ready, after the whole tree has downloaded and executed. In a resumable app they’re the first — the page is interactive the moment it arrives.

Booting a page: hydrate vs. resume

JS up front 0 KB on click interactive
Figure. The same component tree, booted two ways. Hydration walks it bottom-up and the page goes live only when the wave reaches the leaves — last. Resumability ships static HTML that's interactive on arrival, then fetches one handler's chunk per click. KB and time-to-interactive are computed from the tree.

The server sends HTML

Both approaches start in the same place: the server renders the page and ships the markup. It paints instantly, but nothing responds yet — there are no event handlers attached to any of these nodes. Zero JavaScript has run.

Hydration boots the whole tree

To make it interactive, the browser downloads the component tree as JavaScript and re-executes it from the root up, re-attaching listeners node by node to rebuild state the server already had. Watch the wave climb: the 👆 interactive bits at the leaves are the very last thing to wake up.

Resumability: ready on arrival

Now the same tree, resumed. The server serialized the state and event wiring into the HTML, so the browser boots almost nothing. Every handler is live the moment the page lands — handlers are the first thing ready, not the last. No re-execution, no rebuild.

One click, one chunk

Interactivity costs nothing until you use it. Click a button and Qwik reads an attribute, fetches that one handler's chunk, restores its captured state, and runs it. The cost is flat whether the app has three components or five hundred — never the whole tree.

The first version worked; writing it was the hard part

Proving resumability was one thing. The developer experience was another. In that early version the philosophy leaked all the way into the API. You split your code into many small files by hand, you referenced lazy-loadable symbols by name, and you wrote a lot of ceremony to tell the runtime how the pieces fit together. It worked, and it asked too much of the person typing.

My bet was that all of that ceremony could become the compiler’s job. A developer should write something that looks like ordinary React, with components, event handlers, and local state, and the build step should do the hard work of making it resumable. The point of the whole project, after all, was to make the fast path the easy path — and an API that demanded this much manual wiring was the opposite of that.

Making it feel like React

In the design I led, you write a component with handlers and hooks, the way you already think. The magic lives in the compiler.

export const Counter = component$(() => {
  const count = useSignal(0);
  // looks like an ordinary closure; the compiler will extract it
  return <button onClick$={() => count.value++}>{count.value}</button>;
});

The compiler reads the AST, finds every $-marked boundary (a component$, an onClick$), and pulls that closure out into its own module with no dependencies except the variables it closed over. Those captured variables get serialized straight into the HTML. When the user clicks, the runtime loads that one module, restores the state it captured, and runs the handler. Nothing else downloads.

The captured state can be a live signal, so the handler reads and updates a reactive count and the DOM follows along. useSignal lets the runtime make a surgical update to exactly the text node that changed, with no component re-render in the browser. The signal model drew on Solid’s reactivity. The result reads like React and behaves like a resumable app, with none of the manual wiring the first version demanded.

One thing I like about this model over React Server Components: a Qwik component is universal. The same component runs on the server or the client, and the developer doesn’t annotate the boundary. With RSC it’s easy to reach for use client everywhere out of convenience and quietly opt back into shipping everything. In Qwik there’s no boundary to get wrong, because the compiler is the one deciding what crosses it.

Rust and SWC

The optimizer that does all this is written in Rust, on top of SWC. It runs on every build and across large codebases, so it had to stay fast enough to disappear into the dev loop. Rust kept the AST passes quick as projects grew, which matters when the entire premise is that the compiler, and not the developer, carries the complexity.

QwikCity

Later, with Adam Bradley (co-creator of Ionic and Stencil), I worked on the design of QwikCity, the meta-framework that sits on top: routing, data loading, and the conventions that make Qwik a way to build whole sites. That part was a close back-and-forth between the two of us.

Credit where it’s due

I want to be precise about this. The resumability concept, and much of how it works under the hood, including the runtime and the Solid-inspired signals, came from Miško Hevery, the creator of Angular. What I led was the developer-facing redesign and the compiler beneath it: turning closures into independent, serializable modules and working out the reachability analysis that decides what to ship.

The throughline to the rest of my work is that compiler. Make the source look ordinary, push the hard analysis to build time, and let the runtime stay lazy — so the path of least resistance is also the fast one. I’d wired the same instinct into Gin’s router years earlier, and I’d reach for it again tomorrow. If you’re designing a framework, that’s where I’d spend the effort: on the compiler that lets people write the easy thing while you quietly ship the simple one.

If you want the longer version of this argument, I gave a talk on it.

← All writing