Your Next.js app ships 200KB of JavaScript. Users on 3G wait 4 seconds staring at a blank screen while hydration runs. You code-split, lazy-load, optimize — and shave off maybe 500ms.
What if your framework shipped near-zero JavaScript by default, and only loaded code when the user actually interacted with something?
That's Qwik's resumability — and Qwik City is the full-stack framework built on it.
The Hydration Problem (and Qwik's Solution)
Traditional SSR frameworks:
- Server renders HTML → sends to browser
- Browser downloads ALL JavaScript
- Framework re-executes everything to attach event handlers (hydration)
- App becomes interactive
Qwik:
- Server renders HTML with serialized state → sends to browser
- App is immediately interactive — no hydration step
- JavaScript loads on-demand when user clicks/interacts
The result: sub-second TTI (Time to Interactive) regardless of app complexity.
Quick Start
npm create qwik@latest
cd qwik-app && npm start
Resumable Components
import { component$, useSignal } from "@builder.io/qwik";
export const Counter = component$(() => {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
{/* This onClick handler's code loads ONLY when user clicks */}
<button onClick$={() => count.value++}>
Increment
</button>
</div>
);
});
The $ suffix is Qwik's marker for lazy-loading boundaries. The click handler code doesn't download until the user clicks the button. For a page with 50 buttons, that's 50 chunks that never load unless needed.
Server-Side Data Loading
// src/routes/products/index.tsx
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} — ${p.price}</li>
))}
</ul>
);
});
Server Actions — Form Handling Without Client JS
import { routeAction$, Form } from "@builder.io/qwik-city";
export const useAddToCart = routeAction$(async (data, { cookie }) => {
await db.cart.add(data.productId, cookie.get("userId"));
return { success: true };
});
export default component$(() => {
const addToCart = useAddToCart();
return (
<Form action={addToCart}>
<input type="hidden" name="productId" value="123" />
<button type="submit">Add to Cart</button>
{addToCart.value?.success && <p>Added!</p>}
</Form>
);
});
Works without JavaScript enabled. Progressive enhancement by default.
Real-World Performance Impact
| Metric | Next.js (avg) | Qwik City (avg) |
|---|---|---|
| JS shipped (initial) | 180-350KB | 1-5KB |
| TTI | 2.1-4.5s | 0.3-0.8s |
| Lighthouse Performance | 65-85 | 95-100 |
When to Choose Qwik City
Choose Qwik when:
- Page load speed directly impacts revenue (e-commerce, landing pages)
- Your app is complex but most users only interact with a small part
- Mobile/low-bandwidth users are a significant audience
- You want SSR benefits without hydration cost
Skip Qwik when:
- Your app is a highly interactive SPA (dashboards where all JS loads anyway)
- You need React's massive component ecosystem
- Team familiarity with React/Vue outweighs performance gains
The Bottom Line
Qwik City makes the "ship less JavaScript" philosophy automatic. You don't optimize — the framework does it by design. Every component, every handler, every piece of code is lazy-loaded at the interaction boundary.
Start here: qwik.dev
Need custom data extraction, scraping, or automation? I build tools that collect and process data at scale — 78 actors on Apify Store and 265+ open-source repos. Email me: Spinov001@gmail.com | My Apify Actors
Top comments (0)