Skip to main content
ASoc
Comparison

Qwik vs. Next.js: Resumability vs. Not Shipping the Component

Qwik ships near-zero JS through resumability; this site's RSC split already does that for 9 of 10 home-page sections. Where the real cost still lands: one accordion.

The ASoc Team9 min read

Qwik's pitch is resumability: skip hydration entirely by serializing app state on the server and attaching event listeners lazily, per interaction. Next.js's App Router answer is different — React Server Components mean most components never ship to the browser at all, so there is nothing to resume. One framework makes hydration cheap; the other tries to make it rare. This storefront runs the second model, and the numbers below are what that actually costs and saves.

The short answer

Qwik defers and fine-grains hydration — components exist on the client but don't execute until a user interacts with them. Next.js's RSC model defers shipping — a Server Component's code never reaches the client, hydration or not. On this site, 9 of the 10 organisms composing the home page are Server Components with no hydration cost to eliminate, because they were never sent as executable JavaScript in the first place. Qwik's resumability matters most for the components that remain interactive; this repo's real example of that cost is one accordion.

What this storefront is, in numbers

We run Next.js 16.2.9 and React 19.2.4, and we have never shipped a Qwik build, so nothing here is a head-to-head benchmark. What we can measure is our own client/server split, which is the axis this comparison is actually about.

src/components/templates/HomeTemplate.tsx composes the home page from 10 organisms — Header, Hero, FeaturedTemplates, TechStack, Features, FeatureTabs, UseCases, Testimonials, Blog, Footer. One of them, Header, carries "use client", for the mobile drawer toggle and a lazy Supabase session check. The other nine are Server Components: no client bundle, no hydration, no resumability question to ask because there is no client-side execution to resume.

Repo-wide, 24 of 92 component files under src/components carry "use client". That is a different number from the home page's 1-in-10 because the interactive surfaces — the pricing accordion, the template gallery, the download menu, the forms — live on other routes.

Where the hydration cost actually lands

The clean place to see it is /pricing, which renders 8 instances of FaqItem:

// src/components/molecules/FaqItem.tsx
"use client";
import { useId, useState } from "react";

export default function FaqItem({ question, answer }: FaqEntry) {
  const [open, setOpen] = useState(false);
  const id = useId();
  // ...aria-expanded button, aria-controls panel, grid-row transition
}

Eight rows, each its own component instance, each with its own useState and useId. Under React's hydration model, all eight component functions re-execute on the client to attach event listeners and reconcile against the server-rendered markup — even though, per row, the interactive surface is a single click handler toggling a CSS grid-template-rows transition. Nothing about the row's output changes between server and client; the cost is running the function again to wire it up.

This is exactly the case Qwik's resumability targets. In Qwik's model, the server serializes each row's state and a reference to its handler; the client attaches nothing until a visitor actually clicks a row, and only that row's handler loads and runs. Seven of the eight FaqItem instances on a typical page view never execute anything client-side at all — not because they were skipped, but because resumability never re-runs component code that hasn't been triggered.

The comparison that matters

QwikNext.js App Router (React)
Default cost of an interactive componentSerialized on the server; resumed (not re-run) per interactionHydrated on load: the component function re-executes once, client-side
Default cost of a static componentSame resumability model, trivially cheapServer Component — never ships as executable JS
Unit of "lazy"Per event listener (onClick$)Per module boundary ("use client", dynamic())
Where the boundary is declaredCompiler-inferred from $ suffixExplicit per file, propagates through imports
Mental modelOne framework, one loading strategy everywhereTwo component kinds; you choose per file
Ecosystem / hiringSmall, growingVery large — most teams' actual tie-breaker
Best fitInteraction-dense pages where every widget should be near-free to loadContent-heavy sites where most of the page is not interactive at all

Qwik wins the row that says "every interactive widget, including the ones a user does click, should cost close to nothing to attach." Next's RSC model wins a different row: on a page where most sections are not interactive at all, there is no hydration cost to defer, because there's no client component present to defer it.

Why this repo doesn't need resumability to solve its actual problem

27 routes, a product catalog, a blog with 203 posts, and a handful of interactive islands — that's what this storefront is. The expensive failure mode here was never "our accordion hydrates a few milliseconds late." It was a client dependency reaching pages that had no business loading it at all: the Supabase auth SDK, imported eagerly at module scope in Header, was shipping on every page including /, /blog and /docs, none of which render any account UI.

// src/lib/supabase/lazyClient.ts — the fix
export function hasAuthCookie(): boolean {
  if (typeof document === "undefined") return false;
  return /(?:^|;\s*)sb-.+?-auth-token/.test(document.cookie);
}

export async function loadSupabaseClient(): Promise<BrowserClient> {
  const { createClient } = await import("@/lib/supabase/client");
  return createClient();
}

Header now checks for the auth cookie synchronously and only dynamically imports the Supabase client when one exists. That took the SDK off the critical path of every page with no account UI — a dependency-weight bug, not a hydration-timing one. Qwik's resumability model doesn't touch this class of problem: a $-lazy component that still imports a 68 KiB SDK at its own module scope pays for it the moment that component resumes. The fix is the same in both frameworks — defer the import to the code path that uses it.

Routing: file conventions on both sides

QwikCity (Qwik's meta-framework, the fairer counterpart to compare against Next.js rather than bare Qwik) and Next's App Router both route from the filesystem, and the conventions are close enough that neither is a differentiator on its own: src/routes/[slug]/index.tsx in QwikCity versus src/app/[slug]/page.tsx here. This repo has 27 page.tsx files and, per route where needed, a generateStaticParams export — the mechanism that turns /templates/[slug] into 111 individually prerendered product pages at build time.

// src/app/templates/[slug]/page.tsx
export function generateStaticParams(): Params[] {
  return catalog.map((p) => ({ slug: p.slug }));
}

QwikCity has an equivalent static-generation path, but the framework's headline feature is resumability, not routing — which is exactly why this comparison keeps landing on hydration cost rather than on how either framework resolves a URL to a component. The routing layer is close to a wash; the loading-strategy layer is where the two frameworks actually disagree.

Choosing

Choose Qwik when the page genuinely is dense with independent interactive widgets — dashboards, editors, comment threads with hundreds of expandable rows — and you want the loading cost of all of them to stay flat regardless of how many exist on the page.

Choose Next.js when most of a page is not interactive, so RSC can avoid shipping the component in the first place rather than optimizing how cheaply it resumes, and when you need the SDK, MDX and payment-integration ecosystem to already exist. For a storefront where nine of ten home-page sections are Server Components with nothing to hydrate, resumability had no cost left to remove.

Neither answer is about which framework parses faster in isolation — it's about whether your page has one interactive widget or fifty.

Troubleshooting

SymptomCauseFix
Expecting Qwik-style savings from marking a component "use client""use client" still hydrates eagerly on load, unlike Qwik's per-event resumabilityReach for dynamic(() => import(...)) if the component isn't needed on initial paint
Bundle stays large despite mostly Server ComponentsA client island imports a heavy dependency at module scopeDynamic-import it inside the code path that actually needs it, as lazyClient.ts does
Assuming fewer "use client" files means faster interactionFile count measures what ships, not how expensive it is to hydrate what didProfile actual hydration cost per island, not just its presence
Porting a Qwik $ mental model into ReactReact has no native per-listener lazy loadingCompose React.lazy + Suspense at the component boundary instead
Comparing Qwik to "React" instead of to a meta-frameworkQwik's meta-framework is QwikCity, the fairer counterpart to Next.jsCompare QwikCity's routing/data story to Next's, not just runtime cost

Frequently asked questions

Is Qwik faster than Next.js? For pages dense with independent interactive widgets, Qwik's resumability can make first interaction faster because it skips re-running component code until it's actually triggered. For a mostly-static page, the comparison doesn't apply the same way — Next's Server Components never ship the code to begin with, so there's no hydration cost on either side to compare.

Does using Server Components make resumability unnecessary? For the components that are Server Components, yes — there's nothing to hydrate or resume. For the ones that remain Client Components, like this site's FaqItem accordion, React still re-executes the whole function on hydration; Qwik's model would only attach the one handler a visitor actually triggers.

Can I get Qwik-style lazy event handlers in Next.js? Not natively at the per-listener level. dynamic() imports and React.lazy defer whole components, which is coarser than Qwik's $-suffix compiler transform, but it's the tool available for isolating an expensive island from the initial bundle.

How does this compare to the other Next.js alternatives already covered here? The reactivity half of this question overlaps with SolidJS vs. Next.js, which measures the same client/server split from the fine-grained-signals angle, and Next.js vs. Nuxt runs the same server/client-boundary question against Vue's meta-framework. Server Components vs. Client Components covers the boundary rules both posts lean on.

New to Next.js and want the fundamentals before comparing frameworks? Learn Next.js walks the App Router basics against this same shipped codebase rather than a toy example, which is the more useful starting point than either framework's own quick-start.

Templates in this post

ASoc Folio, ASoc Forge and ASoc Frame are Next.js + Tailwind landing page templates built on the same server-first split described above — static sections as Server Components, interactivity isolated to the few molecules that need it.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Comparison8 min read

React vs. Gatsby: The Real Question Is the GraphQL Layer

Gatsby adds a GraphQL data layer on top of React. This catalog imports typed data directly instead, with zero GraphQL and zero content plugins.

Read more
Comparison8 min read

React vs. Remix: A Mismatched Pairing, and the Real Question

Remix runs on React — they aren't competitors. The real comparison is Next.js vs. React Router v7, and it comes down to which way the static default points.

Read more