Skip to main content
โšก Calmops

Qwik: The Resumable Framework for Instant Applications

Qwik is a revolutionary JavaScript framework that takes a different approach to web application performance. Instead of hydrating the entire application on load, Qwik uses a “resumable” model that delivers near-zero JavaScript to the client. This comprehensive guide covers everything you need to know about Qwik.

What is Qwik?

Qwik is a web framework that achieves instant interactivity through resumability - the ability to pause execution on the server and resume on the client without replaying all the application logic.

// Traditional React: Hydration loads all JavaScript
// User clicks button -> entire app hydrates -> event handler works

// Qwik: Event handler loads on-demand
// User clicks button -> only that handler's code loads -> works instantly

Core Philosophy

Qwik’s key innovations include:

  • Resumability - No hydration, no replay, instant interactivity
  • Lazy Loading - Code loads only when needed
  • Fine-grained Reactivity - Update only what changes
  • Zero Loading - Near-zero JavaScript on initial page load
  • Server-First - Compute on server, ship HTML
flowchart LR
    subgraph Traditional["Traditional Framework"]
        T1[Server Render HTML] --> T2[Download JS Bundle]
        T2 --> T3[Parse & Execute]
        T3 --> T4[Hydrate App]
        T4 --> T5[Interactive]
    end
    
    subgraph Qwik["Qwik Framework"]
        Q1[Server Render HTML] --> Q2[Download Minimal JS]
        Q2 --> Q3[No Hydration]
        Q3 --> Q4[Instant Interactive]
    end
    
    T1 -.->|Same| Q1
    T2 -.->|Much smaller| Q2

Installation and Setup

Creating a New Qwik Project

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

# Navigate to project
cd my-qwik-app

# Start development server
npm start

# Or use Qwik City for routing
npm create qwik@latest -- --template city

Project Structure

my-qwik-app/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ counter/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ counter.tsx
โ”‚   โ”‚   โ””โ”€โ”€ header/
โ”‚   โ”‚       โ””โ”€โ”€ header.tsx
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ”œโ”€โ”€ index.tsx          # Home page
โ”‚   โ”‚   โ”œโ”€โ”€ about/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ index.tsx      # /about route
โ”‚   โ”‚   โ””โ”€โ”€ layout.tsx         # Shared layout
โ”‚   โ”œโ”€โ”€ entry.ssr.tsx          # Server entry
โ”‚   โ”œโ”€โ”€ entry.dev.tsx          # Dev entry
โ”‚   โ”œโ”€โ”€ root.tsx               # Root component
โ”‚   โ””โ”€โ”€ global.css             # Global styles
โ”œโ”€โ”€ public/                    # Static assets
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ vite.config.ts

Core Concepts

The $ Symbol

The $ symbol is fundamental to Qwik. It marks boundaries for lazy loading.

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

// Component is lazy loaded
export const Counter = component$(() => {
  const count = useSignal(0);
  
  // This function is lazy loaded separately
  const increment = $(() => {
    count.value++;
  });
  
  return (
    <button onClick$={increment}>
      Count: {count.value}
    </button>
  );
});

// Without $: function is inlined (not lazy loaded)
export const InlineExample = component$(() => {
  const handleClick = () => {
    console.log('This is inlined');
  };
  
  return <button onClick$={handleClick}>Click</button>;
});

useSignal: Fine-Grained Reactivity

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

export const SignalExample = component$(() => {
  // Primitive signal - updates only this text
  const name = useSignal('World');
  
  return (
    <div>
      <input 
        value={name.value}
        onInput$={(e) => name.value = (e.target as HTMLInputElement).value}
      />
      <p>Hello {name.value}!</p>
    </div>
  );
});

// Object store for nested reactivity
export const StoreExample = component$(() => {
  const state = useStore({
    user: { name: 'John', age: 30 },
    items: ['item1', 'item2']
  });
  
  return (
    <div>
      <p>{state.user.name}</p>
      <button onClick$={() => state.user.name = 'Jane'}>
        Update Name
      </button>
    </div>
  );
});

useStore: Deep Reactivity

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

export const DeepStore = component$(() => {
  const store = useStore({
    nested: {
      deep: {
        value: 'initial'
      },
      array: [{ id: 1, name: 'Item 1' }]
    }
  }, { deep: true }); // Enable deep reactivity
  
  return (
    <div>
      <p>{store.nested.deep.value}</p>
      <button onClick$={() => {
        store.nested.deep.value = 'updated';
      }}>
        Update
      </button>
    </div>
  );
});

Component Patterns

Basic Components

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

interface Props {
  title: string;
  count?: number;
}

export const MyComponent = component$<Props>((props) => {
  return (
    <div class="my-component">
      <h1>{props.title}</h1>
      <p>Count: {props.count ?? 0}</p>
    </div>
  );
});

Components with Children

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

export const Card = component$(() => {
  return (
    <div class="card">
      <div class="card-header">
        <Slot name="header" />
      </div>
      <div class="card-body">
        <Slot />
      </div>
    </div>
  );
});

// Usage
export const Usage = component$(() => {
  return (
    <Card>
      <span q:slot="header">Card Title</span>
      <p>Card content goes here</p>
    </Card>
  );
});

Conditional Rendering

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

export const ConditionalExample = component$(() => {
  const show = useSignal(true);
  
  return (
    <div>
      <button onClick$={() => show.value = !show.value}>
        Toggle
      </button>
      
      {show.value && (
        <p>This is conditionally rendered</p>
      )}
      
      {/* Or use ternary */}
      {show.value ? (
        <p>Showing</p>
      ) : (
        <p>Hidden</p>
      )}
    </div>
  );
});

List Rendering

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

interface Item {
  id: number;
  name: string;
}

export const ListExample = component$(() => {
  const state = useStore<{ items: Item[] }>({
    items: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' }
    ]
  });
  
  return (
    <ul>
      {state.items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

Event Handling

Click Events

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

export const ClickHandler = component$(() => {
  const clicked = useSignal(false);
  
  return (
    <button 
      onClick$={() => {
        clicked.value = true;
        console.log('Button clicked!');
      }}
    >
      {clicked.value ? 'Clicked!' : 'Click Me'}
    </button>
  );
});

Input Events

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

export const InputHandler = component$(() => {
  const text = useSignal('');
  
  const handleInput = $((e: Event) => {
    const target = e.target as HTMLInputElement;
    text.value = target.value;
  });
  
  const handleSubmit = $(() => {
    alert(`Submitted: ${text.value}`);
  });
  
  return (
    <form preventdefault:submit onSubmit$={handleSubmit}>
      <input 
        value={text.value}
        onInput$={handleInput}
      />
      <p>You typed: {text.value}</p>
      <button type="submit">Submit</button>
    </form>
  );
});

Window/Document Events

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

export const WindowEvents = component$(() => {
  const scrollY = useSignal(0);
  
  useVisibleTask$(() => {
    const handleScroll = () => {
      scrollY.value = window.scrollY;
    };
    
    window.addEventListener('scroll', handleScroll);
    
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  });
  
  return (
    <div>
      <p>Scroll Y: {scrollY.value}</p>
    </div>
  );
});

Data Fetching

routeLoader$: Server Data Loading

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

// Server-side data loading
export const useUserLoader = routeLoader$(async (requestEvent) => {
  // This runs on the server
  const userId = requestEvent.params.id;
  
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  
  return user;
});

export default component$(() => {
  // Use the loader
  const user = useUserLoader();
  
  return (
    <div>
      <h1>{user.value.name}</h1>
      <p>{user.value.email}</p>
    </div>
  );
});

server$: Server Actions

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

// Define server function
const serverAddTodo = server$(async (text: string) => {
  // This runs on the server
  const todo = await db.todo.create({ text });
  return todo;
});

export const AddTodo = component$(() => {
  return (
    <button onClick$={async () => {
      const newTodo = await serverAddTodo('New task');
      console.log('Created:', newTodo);
    }}>
      Add Todo
    </button>
  );
});

Form Actions

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

export const useAddComment = routeAction$(async (data) => {
  // Handle form submission on server
  await db.comment.create({
    text: data.text,
    postId: data.postId
  });
  
  return { success: true };
});

export const CommentForm = component$(() => {
  const addComment = useAddComment();
  
  return (
    <Form action={addComment}>
      <input name="text" placeholder="Add a comment" />
      <input type="hidden" name="postId" value="123" />
      <button type="submit">Submit</button>
    </Form>
  );
});

State Management

useContext: Sharing State

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

// Create context
export const ThemeContext = createContextId<string>('theme-context');

export const App = component$(() => {
  const theme = useSignal('dark');
  
  // Provide context
  useContextProvider(ThemeContext, theme);
  
  return <Child />;
});

export const Child = component$(() => {
  // Consume context
  const theme = useContext(ThemeContext);
  
  return (
    <button onClick$={() => theme.value = theme.value === 'dark' ? 'light' : 'dark'}>
      Current: {theme.value}
    </button>
  );
});

useTask$: Side Effects

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

export const TaskExample = component$(() => {
  const count = useSignal(0);
  const doubled = useSignal(0);
  
  // Runs when count changes
  useTask$(({ track }) => {
    track(() => count.value);
    doubled.value = count.value * 2;
  });
  
  return (
    <div>
      <p>Count: {count.value}</p>
      <p>Doubled: {doubled.value}</p>
      <button onClick$={() => count.value++}>Increment</button>
    </div>
  );
});

Qwik City: Routing

Basic Routing

// src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';

export default component$(() => {
  return (
    <div>
      <h1>Welcome</h1>
      <Link href="/about">About</Link>
      <Link href="/blog/post-1">Blog Post</Link>
    </div>
  );
});

Dynamic Routes

// src/routes/blog/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { useLocation, 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 loc = useLocation();
  const post = useBlogPost();
  
  return (
    <article>
      <h1>{post.value.title}</h1>
      <p>Slug: {loc.params.slug}</p>
    </article>
  );
});

Nested Layouts

// src/routes/layout.tsx - Main layout
import { component$, Slot } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';

export default component$(() => {
  return (
    <div>
      <nav>
        <Link href="/">Home</Link>
        <Link href="/about">About</Link>
      </nav>
      <main>
        <Slot />
      </main>
    </div>
  );
});

// src/routes/dashboard/layout.tsx - Dashboard layout
import { component$, Slot } from '@builder.io/qwik';

export default component$(() => {
  return (
    <div class="dashboard">
      <aside>
        <Link href="/dashboard">Overview</Link>
        <Link href="/dashboard/settings">Settings</Link>
      </aside>
      <main>
        <Slot />
      </main>
    </div>
  );
});

Middleware and Hooks

// src/routes/[email protected]
import type { RequestHandler } from '@builder.io/qwik-city';

export const onGet: RequestHandler = async ({ cookie, redirect }) => {
  const session = cookie.get('session');
  
  if (!session) {
    throw redirect(302, '/login');
  }
};

// Use in any route
// src/routes/admin/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';

export const onGet: RequestHandler = async ({ redirect }) => {
  // This route is protected
  throw redirect(302, '/');
};

Performance Optimization

Code Splitting by Default

// Every component is automatically code-split
import { component$ } from '@builder.io/qwik';

// This component loads in a separate chunk
export const HeavyComponent = component$(() => {
  return <div>I'm lazily loaded!</div>;
});

// Usage
export const Parent = component$(() => {
  const show = useSignal(false);
  
  return (
    <div>
      <button onClick$={() => show.value = !show.value}>
        Toggle
      </button>
      {show.value && <HeavyComponent />}
    </div>
  );
});

Optimizing Images

import { component$ } from '@builder.io/qwik';
import { Image } from '@unpic/qwik';

export const OptimizedImage = component$(() => {
  return (
    <Image
      src="/large-image.jpg"
      layout="constrained"
      width={800}
      height={600}
      alt="Description"
    />
  );
});

Prefetching

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

// Qwik automatically prefetches on hover
export const FastLink = component$(() => {
  return (
    <Link href="/heavy-page">
      Load Heavy Page
    </Link>
  );
});

Comparison with Other Frameworks

Feature Qwik React Vue Svelte
Hydration None Full Full Full
Initial JS ~1KB 40KB+ 30KB+ ~10KB
Learning Curve Low Medium Low Low
Reactivity Fine-grained Hooks-based Reactive Compiler-based
Server Components Native Next.js Nuxt SvelteKit
Ecosystem Growing Large Large Growing
flowchart LR
    subgraph Loading["JavaScript Loading"]
        R[React] --> RBundle[40KB+]
        RBundle --> RHydrate[Hydration]
        RHydrate --> RReady[Interactive]
        
        V[Vue] --> VBundle[30KB+]
        VBundle --> VHydrate[Hydration]
        VHydrate --> VReady[Interactive]
        
        Q[Qwik] --> QBundle[~1KB]
        QBundle --> QReady[Instant Interactive]
    end

Best Practices

Do: Use $ Boundaries Correctly

// Good: $ marks lazy loading boundary
export const Good = component$(() => {
  const handleClick = $(() => {
    console.log('clicked');
  });
  
  return <button onClick$={handleClick}>Click</button>;
});

// Bad: Missing $ means inlining
export const Bad = component$(() => {
  const handleClick = () => { // No $
    console.log('clicked');
  };
  
  return <button onClick$={handleClick}>Click</button>;
});

Do: Use Proper Types

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

interface ButtonProps {
  onClick$: QRL<() => void>;
  label: string;
}

export const Button = component$<ButtonProps>(({ onClick$, label }) => {
  return <button onClick$={onClick$}>{label}</button>;
});

Don’t: Mutate Props Directly

// Bad: Mutating props
export const BadComponent = component$<{ count: number }>(({ count }) => {
  count++; // Don't do this!
  return <div>{count}</div>;
});

// Good: Use signals
export const GoodComponent = component$(() => {
  const count = useSignal(0);
  return <div>{count.value}</div>;
});

Deployment

Vercel

npm run qwik add vercel
// vercel.json
{
  "functions": {
    "api/*.js": {
      "runtime": "@vercel/node"
    }
  }
}

Netlify

npm run qwik add netlify

Cloudflare Pages

npm run qwik add cloudflare-pages

External Resources

Conclusion

Qwik represents a paradigm shift in how we think about web application performance. By eliminating hydration entirely and using a resumable model, Qwik achieves near-instant interactivity regardless of application complexity.

The framework is ideal for:

  • Performance-critical applications
  • E-commerce sites
  • Content-heavy websites
  • Progressive web applications
  • Any project where initial load time matters

While the ecosystem is younger than React or Vue, Qwik’s innovative approach makes it a compelling choice for teams prioritizing user experience and performance.

Comments