Scripts2026年4月11日·1 分钟阅读

Qwik — Instant-Loading Web Apps Without Effort

Qwik is a web framework designed for instant-loading apps through resumability. Zero hydration, fine-grained code loading, and O(1) startup time — regardless of app size.

SC
Script Depot · Community
快速使用

先拿来用,再决定要不要深挖

这里应该同时让用户和 Agent 知道第一步该复制什么、安装什么、落到哪里。

# Create a new Qwik app
npm create qwik@latest
cd my-app
npm install
npm start
介绍

Qwik is a new kind of web framework designed for the best possible time-to-interactive metric by focusing on resumability of server-side rendering of HTML, and fine-grained lazy-loading of code. Unlike traditional frameworks that need to "hydrate" (re-execute all component code on the client), Qwik can "resume" execution from where the server left off — resulting in instant page loads regardless of app complexity.

With 22K+ GitHub stars and MIT license, Qwik was created by Misko Hevery (also creator of Angular). It represents a fundamentally new approach to building web apps that scales to massive applications without sacrificing startup performance.

What Qwik Does

  • Resumability: Skip hydration entirely — apps resume from server state
  • O(1) Time-to-Interactive: Startup time independent of app size
  • Fine-Grained Lazy Loading: Auto-split code at function boundaries
  • Zero JS on Initial Load: Interactive pages with 0 JavaScript
  • React-like Syntax: JSX and component model familiar to React devs
  • Qwik City: Meta-framework with routing, layouts, server actions
  • SSR/SSG/Static: Flexible rendering modes
  • Optimized Bundles: Automatic code splitting at event handler level
  • Signal-based: Fine-grained reactivity like SolidJS

Key Concept: Resumability

Traditional Hydration (React, Vue, etc.):
  Server: Render HTML
  Client: Download JS  Execute all components  Attach listeners  Interactive
  Time: Proportional to app size

Qwik Resumability:
  Server: Render HTML + serialize state
  Client: No JS execution needed
  User clicks button  Load ONLY that handler  Execute
  Time: O(1) - constant regardless of app size

Getting Started

Create Project

npm create qwik@latest

# Choose:
# - my-app (name)
# - Basic App (template)
# - Select Qwik City (meta framework)

Project Structure

my-app/
├── src/
│   ├── routes/           ← File-based routing
│   │   ├── index.tsx
│   │   ├── about/
│   │   │   └── index.tsx
│   │   └── blog/
│   │       └── [slug]/
│   │           └── index.tsx
│   ├── components/
│   │   └── header/
│   │       └── header.tsx
│   ├── root.tsx         ← Root component
│   └── entry.ssr.tsx    ← SSR entry
├── vite.config.ts
└── package.json

Basic Components

Counter Component

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

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

  return (
    <div>
      <h1>Counter: {count.value}</h1>
      <button onClick$={() => count.value++}>
        Increment
      </button>
    </div>
  );
});

Note: component$ and onClick$ — the $ marker tells Qwik to split this code for lazy loading.

Important: Only Runs on Interaction

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

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

  // This console.log runs on server during SSR
  console.log('Component setup');

  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick$={() => {
        // THIS CODE is lazy-loaded ONLY when button clicked
        // Not included in initial JS bundle
        count.value++;
      }}>
        Click
      </button>
    </div>
  );
});

Async Data with useResource$

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

export default component$(() => {
  const postsResource = useResource$(async () => {
    const res = await fetch('https://api.example.com/posts');
    return res.json();
  });

  return (
    <Resource
      value={postsResource}
      onPending={() => <div>Loading...</div>}
      onRejected={(error) => <div>Error: {error.message}</div>}
      onResolved={(posts) => (
        <ul>
          {posts.map((post) => <li key={post.id}>{post.title}</li>)}
        </ul>
      )}
    />
  );
});

Qwik City (Meta Framework)

File-Based Routing

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

// Runs on server before rendering
export const useBlogPost = routeLoader$(async ({ params }) => {
  const post = await db.post.findUnique({ where: { slug: params.slug } });
  return post;
});

export default component$(() => {
  const post = useBlogPost();

  return (
    <article>
      <h1>{post.value?.title}</h1>
      <div dangerouslySetInnerHTML={post.value?.content} />
    </article>
  );
});

Server Actions

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

// Server-side action
export const useCreatePost = routeAction$(
  async (data, { redirect }) => {
    const post = await db.post.create({ data });
    throw redirect(302, `/blog/${post.slug}`);
  },
  zod$({
    title: z.string().min(1),
    content: z.string().min(10),
  })
);

export default component$(() => {
  const createPost = useCreatePost();

  return (
    <Form action={createPost}>
      <input name="title" placeholder="Title" />
      <textarea name="content" placeholder="Content" />
      <button type="submit">Create Post</button>
      {createPost.value?.failed && (
        <p>Errors: {JSON.stringify(createPost.value.fieldErrors)}</p>
      )}
    </Form>
  );
});

Layouts

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

export default component$(() => {
  return (
    <>
      <header>My Site</header>
      <main>
        <Slot /> {/* Child routes render here */}
      </main>
      <footer>© 2024</footer>
    </>
  );
});

Performance Characteristics

Initial Page Load (Traditional React):
  HTML: 10KB
  JS: 300KB+ (React + app code)
  Time-to-Interactive: 2-5 seconds

Initial Page Load (Qwik):
  HTML: 15KB (includes serialized state)
  JS: 0KB (nothing to execute!)
  Time-to-Interactive: ~0 seconds

When user interacts:
  Download 3KB chunk for specific handler
  Execute handler
  Update DOM

Why Resumability?

Problem with hydration:

1. Server renders HTML (fast)
2. User sees content
3. Client downloads framework + components (slow)
4. Client RE-EXECUTES all components (builds VDOM)
5. Reconciles with existing DOM
6. Attaches event listeners
7. NOW interactive (1-5 seconds later)

Hydration = doing everything twice

Qwik solves this by:

1. Server renders HTML + serializes component state
2. Client sees content
3. Client needs ZERO JavaScript to be interactive
4. On first user interaction, download ONLY the specific handler
5. Execute handler, update DOM
6. Done

Qwik vs Alternatives

Feature Qwik React Solid Astro
Hydration None (Resume) Required Required Islands
Time-to-Interactive O(1) O(n) O(n) O(islands)
Initial JS 0KB Full bundle Full bundle Minimal
Syntax JSX + $ markers JSX JSX JSX/MD
Learning curve Medium Easy Easy Easy
Best for Large apps General Performance Content sites

常见问题

Q: Qwik 的 $ 符号是什么? A: $ 是 Qwik 的"lazy loading boundary"标记。每个 $ 标记的函数都会被编译成独立的 chunk,按需加载。这是 Qwik 实现细粒度代码分割的关键机制。

Q: Qwik 和 Astro 怎么选? A: Astro 是"岛屿架构"——静态 HTML + 少量交互岛屿,适合内容网站。Qwik 是整个应用都是 resumable 的,适合有大量交互的 Web 应用。内容博客选 Astro,SaaS 应用选 Qwik。

Q: 生态系统成熟吗? A: Qwik 相对较新(2022 发布),生态仍在成长中。但核心团队(包括 Angular 创造者)非常活跃,大公司如 Builder.io 在生产环境使用。对于追求极致性能的新项目很值得考虑。

来源与致谢

讨论

登录后参与讨论。
还没有评论,来写第一条吧。

相关资产