DEV Community

Manu MA
Manu MA

Posted on • Originally published at manualmeida.dev on

Qwik: Resumability That Feels Like React

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 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 Lighthouse 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.

There’s a neat way to see the flip. 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.

The first version worked; writing it was the hard part

Proving resumability was one thing. The developer experience was another. In that early versionthe 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>;
});

Enter fullscreen mode Exit fullscreen mode

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’sreactivity. 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 ofAngular. 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 intoGin’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 atalk on it.

This article has interactive figures — read it on manualmeida.dev.

Top comments (0)