Skip to main content
โšก Calmops

Qwik Complete Guide: The Resumable Framework with Near-Zero JavaScript

Introduction

Qwik is a revolutionary web framework that eliminates the need for hydration entirely. Instead, it uses “resumability” - the ability to pause execution on the server and resume on the client without re-running code. This guide covers Qwik’s unique approach and how to build blazing-fast applications.

Understanding Qwik

What is Resumability?

Traditional frameworks use “hydration” - re-running all JavaScript on the client to make HTML interactive. Qwik skips this entirely.

graph TB
    subgraph "React/Next.js - Hydration"
        Server1[Server: Render HTML]
        Download[Download JS Bundle]
        Execute[Execute All JS]
        Interactive[Become Interactive]
        
        Server1 --> Download
        Download --> Execute
        Execute --> Interactive
    end
    
    subgraph "Qwik - Resumability"
        Server2[Server: Serialize State]
        Download2[Download Minimal JS]
        Resume[Resume from State]
        Interactive2[Already Interactive]
        
        Server2 --> Download2
        Download2 --> Resume
        Resume --> Interactive2
    end

Key Concepts

Concept Description
Resumability Pause on server, resume on client
Fine-grained Lazy Loading Load JS only when needed
Zero Hydration No JS execution on load
O(1) Startup Constant time regardless of app size
Serialization State serialized in HTML

Getting Started

Installation

# Create new Qwik app
npm create qwik@latest my-app

# Add to existing project
npm run qwik add

Basic Component

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

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

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

How It Works

// Qwik serializes this:
// <button q:target="counter_0">Count: 0</button>

// When clicked, loads only:
// http://localhost:5173/build/counter.js?hash=abc
// And executes just that tiny chunk!

Core API

Signals

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

// Primitive signal
const count = useSignal(0);
count.value;

// Object signal
const state = useStore({ name: 'John', age: 30 });
state.name = 'Jane';

useTask$

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

export const DataFetcher = component$(() => {
  const data = useSignal(null);

  useTask$(async ({ track }) => {
    track(() => data.value);
    
    const res = await fetch('/api/data');
    data.value = await res.json();
  });

  return <div>{JSON.stringify(data.value)}</div>;
});

Computed Values

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

export const Calculator = component$(() => {
  const a = useSignal(10);
  const b = useSignal(5);
  
  const sum = useComputed$(() => a.value + b.value);

  return (
    <div>
      <input type="number" bind:value={a} />
      <input type="number" bind:value={b} />
      <p>Sum: {sum.value}</p>
    </div>
  );
});

Routing

File-Based Routing

src/routes/
โ”œโ”€โ”€ index.tsx          # /
โ”œโ”€โ”€ about/
โ”‚   โ””โ”€โ”€ index.tsx      # /about
โ”œโ”€โ”€ blog/
โ”‚   โ”œโ”€โ”€ index.tsx      # /blog
โ”‚   โ””โ”€โ”€ [slug]/
โ”‚       โ””โ”€โ”€ index.tsx  # /blog/:slug
โ””โ”€โ”€ layout.tsx         # Shared layout

Route Loader

// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const useBlogPost = routeLoader$(async ({ params }) => {
  const post = await db.posts.find(params.slug);
  return post;
});

export default component$(() => {
  const post = useBlogPost();
  
  return (
    <article>
      <h1>{post.value.title}</h1>
      <div>{post.value.content}</div>
    </article>
  );
});

Qwik City

Actions

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

export const useSubmitForm = routeAction$(async (data) => {
  await db.createUser(data);
  return { success: true };
});

export default component$(() => {
  const action = useSubmitForm();

  return (
    <Form action={action}>
      <input name="email" type="email" />
      <button type="submit">Submit</button>
    </Form>
  );
});

Performance

Lazy Loading Example

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

// This function becomes a separate chunk
export const heavyComputation$: QRL<() => number> = $(() => {
  // Only loads when called
  return Math.random() * 100;
});

export const MyComponent = component$(() => {
  return (
    <button onClick$={async () => {
      const result = await heavyComputation$();
      console.log(result);
    }}>
      Calculate
    </button>
  );
});

Optimizer

// Qwik automatically splits:
// - Each component$: into separate chunk
// - Each $(...) lazy-loaded function
// - Only loads what's needed

// Result: 0KB JS on initial load!
// User clicks -> loads 1KB for that interaction

Qwik vs React

Aspect React Qwik
Initial JS 100KB+ 0-1KB
Hydration Required None
Startup O(n) O(1)
Lazy Loading Component-level Function-level
Learning Curve Moderate Easy
Ecosystem Large Growing

Conclusion

Qwik represents the future of web frameworks:

  • Instant interactivity - no hydration needed
  • O(1) startup - doesn’t scale with app size
  • Fine-grained code splitting - load only what’s needed
  • Familiar patterns - similar to React

Perfect for: performance-critical sites, e-commerce, content sites.


External Resources

Comments