算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
with track option to re-run on signal changes:\n\n```tsx\nuseVisibleTask$(({ track }) => {\n track(() => someSignal.value)\n // re-runs when someSignal changes\n})\n```\n\n## Build & Bundle Considerations\n\n### Production Build\n```bash\nnpm run qwik build\n# Output in dist/ — contains HTML, JS chunks per $ boundary, CSS\n```\n\n### Chunk Analysis\nQwik generates many small chunks by design. To analyze:\n```bash\nnpx qwik build --analyze\n# Opens bundle analysis in browser\n```\n\n### Optimizer Configuration\nQwik's optimizer is configured through Vite. Key settings in `vite.config.ts`:\n\n```ts\nimport { defineConfig } from 'vite'\nimport { qwikVite } from '@builder.io/qwik/optimizer'\nimport { qwikCity } from '@builder.io/qwik-city/vite'\n\nexport default defineConfig({\n plugins: [\n qwikCity(),\n qwikVite({\n ssr: { input: 'src/entry.ssr.tsx' },\n client: { input: 'src/entry.dev.tsx' },\n // Optimizer options\n entryStrategy: { type: 'smart' }, // 'smart' | 'hoist' | 'single'\n symbolMapper: true,\n genDts: false,\n }),\n ],\n})\n```\n\nEntry strategy options:\n- `smart` (default): Splits into optimal chunks per $ boundary\n- `hoist`: Fewer, larger chunks — better for non-lazy scenarios\n- `single`: Single bundle — disables all lazy loading\n\n### Adapter Configuration\n\n```ts\n// qwik.config.ts (Cloudflare Pages)\nimport { extendConfig } from '@builder.io/qwik-city/vite'\nimport cloudflarePages from '@builder.io/qwik-city/adapters/cloudflare-pages/vite'\n\nexport default extendConfig(baseConfig, () => ({\n plugins: [cloudflarePages()],\n}))\n```\n\nAvailable adapters: `cloudflare-pages`, `vercel-edge`, `node-server`, `deno-server`, `static`.\n\n### Static Generation (SSG)\n\n```ts\nimport { staticGenerate } from '@builder.io/qwik-city/static'\n// Configure in vite.config.ts for SSG routes\n```\n\nSSG generates HTML at build time for static routes. Dynamic routes still use SSR.\n\n### Environment Variables\n```tsx\n// Accessible anywhere\nconst apiUrl = import.meta.env.PUBLIC_API_URL\nconst mode = import.meta.env.MODE // 'development' | 'production'\n```\n\nPrefix with `PUBLIC_` for client-accessible vars. Private vars available only on server.\n\n### CSS Strategy\n- Qwik supports any CSS approach: Tailwind, CSS Modules, styled-components, plain CSS\n- CSS is automatically split per component chunk\n- Scoped styles via CSS Modules or Qwik's built-in scoping\n\n## Testing Strategies\n\n### Unit Testing Components\n\n```tsx\n// __tests__/counter.test.tsx\nimport { createDOM } from '@builder.io/qwik/testing'\nimport { test, expect } from 'vitest'\nimport Counter from './counter'\n\ntest('increments count', async () => {\n const { screen, render, userEvent } = await createDOM()\n await render(\u003cCounter />)\n expect(screen.innerText).toContain('0')\n await userEvent('button', 'click')\n expect(screen.innerText).toContain('1')\n})\n```\n\n### Testing Route Loaders\n\n```tsx\n// __tests__/routes.test.ts\nimport { test, expect } from 'vitest'\nimport { useProductData } from '../src/routes/product/[id]/index'\n\ntest('loader returns product', async () => {\n const loader = useProductData()\n const result = await loader({ params: { id: '1' }, ...mockContext })\n expect(result).toHaveProperty('name')\n})\n```\n\n### Testing Server Functions\n\n```tsx\nimport { test, expect } from 'vitest'\nimport { createUser } from '../src/components/createUser'\n\ntest('creates user on server', async () => {\n const user = await createUser({ email: 'test@test.com', name: 'Test' })\n expect(user.email).toBe('test@test.com')\n})\n```\n\n### E2E Testing with Playwright\n\n```tsx\n// e2e/app.spec.ts\nimport { test, expect } from '@playwright/test'\n\ntest('page loads without JS', async ({ page }) => {\n await page.goto('/')\n // Content is visible even without JS\n expect(await page.locator('h1').textContent()).toBe('Welcome')\n})\n\ntest('interaction lazy-loads chunk', async ({ page }) => {\n await page.goto('/')\n await page.locator('button').click()\n // Button handler downloaded lazily, interaction still works\n expect(await page.locator('output').textContent()).toBe('1')\n})\n\ntest('form action works', async ({ page }) => {\n await page.goto('/dashboard')\n await page.fill('input[name=\"name\"]', 'John')\n await page.click('button[type=\"submit\"]')\n await expect(page.locator('.success')).toBeVisible()\n})\n```\n\n## Migration Patterns\n\n### Migrating from React to Qwik\n\n**Component conversion:**\n```tsx\n// React\nfunction Counter() {\n const [count, setCount] = useState(0)\n return \u003cbutton onClick={() => setCount(c => c + 1)}>{count}\u003c/button>\n}\n\n// Qwik\nexport default component$(() => {\n const count = useSignal(0)\n return \u003cbutton onClick$={() => count.value++}>{count.value}\u003c/button>\n})\n```\n\n**Hook conversion:**\n```\nuseState -> useSignal / useStore\nuseEffect -> useVisibleTask$ (rare) or routeLoader$\nuseContext -> useContext + useContextProvider\nuseReducer -> useStore with action functions\nuseMemo -> useComputed$\nuseCallback -> $()\nuseRef -> useSignal (for elements)\n```\n\n**State management:**\n```\nRedux/Zustand -> useContext + useStore OR routeLoader$\nReact Query -> routeLoader$ + routeAction$\nReact Router -> Qwik City file-based routing\nFormik/React Hook -> routeAction$ + \u003cForm>\nForm\n```\n\n### Migrating from Next.js to Qwik City\n\n| Next.js | Qwik City |\n|---------|-----------|\n| pages/ directory | src/routes/ directory |\n| getServerSideProps | routeLoader$ |\n| API routes | src/routes/api/ or server$ |\n| Layout (app dir) | layout.tsx per route |\n| Server Actions | routeAction$ |\n| Middleware | plugin@name.ts |\n| next/image | qwik-image |\n\n### Incremental Adoption\n\nAdd Qwik to an existing project via micro-frontends or iframe embedding. Use Qwik on new interactive features while keeping the legacy app running. Qwik can be embedded in any page via a script tag — it doesn't require full app control.\n\n## Anti-Patterns\n\n### Eager Code Outside $ Boundaries\n\n```tsx\n// Anti-pattern: heavy computation at module level\nconst data = expensiveCalculation() // runs on every import\n\n// Correct: lazy $ boundary\nconst getData = $(() => expensiveCalculation())\n```\n\n### Dynamic $() Construction\n\n```tsx\n// Anti-pattern: dynamic $ breaks optimizer\nconst handler = condition ? $(fn1) : $(fn2)\n\n// Correct: static $ boundaries\nif (condition) {\n return \u003cbutton onClick$={fn1}>A\u003c/button>\n}\nreturn \u003cbutton onClick$={fn2}>B\u003c/button>\n```\n\n### Overusing useVisibleTask$\n\n```tsx\n// Anti-pattern: data fetch in useVisibleTask$\nuseVisibleTask$(async () => {\n const data = await fetch('/api/data').then(r => r.json())\n state.value = data\n})\n\n// Correct: routeLoader$ for data\nexport const useData = routeLoader$(async () => {\n return await fetch('/api/data').then(r => r.json())\n})\n```\n\n### Missing Prefetch Configuration\n\nWithout `PrefetchServiceWorker`, every lazy interaction triggers a network waterfall. Always include it in the root layout.\n\n### Mutating Signals Incorrectly\n\n```tsx\n// Anti-pattern: reassignment\ncount = 5\n\n// Correct: .value assignment\ncount.value = 5\n```\n\n### Passing Non-Serializable Props to component$\n\n```tsx\n// Anti-pattern: function as prop\n\u003cChild onEvent={fn} />\n\n// Correct: $ suffix on event\n\u003cChild onEvent$={fn} />\n```\n\n### Using React Patterns (useEffect, useState)\n\nQwik has its own reactivity model. Importing React patterns causes confusion and breaks resumability. Always use Qwik primitives.\n\n### Forgetting Cleanup in useVisibleTask$\n\n```tsx\n// Anti-pattern: no cleanup — memory leak\nuseVisibleTask$(() => {\n window.addEventListener('scroll', handler)\n})\n\n// Correct: cleanup\nuseVisibleTask$(() => {\n window.addEventListener('scroll', handler)\n return () => window.removeEventListener('scroll', handler)\n})\n```\n\n## Common Pitfalls\n\n### Pitfall 1: Forgetting $ on Event Handlers\n```tsx\n// Wrong — click handler not lazy, entire component eager\n\u003cbutton onClick={() => count.value++}>\n\n// Correct — click handler is lazy boundary\n\u003cbutton onClick$={() => count.value++}>\n```\nAlways suffix interactive events with `
frontend-qwik-architecture【设计与创意】Use this skill when the user says 'Qwik', 'Qwik architecture', 'Qwik resumable', 'Qwik City', 'Qwik component', '$()', 'resumable', 'lazy loading Qwik', 'Qwik vs React'. This skill enforces: resumability (no hydration, serialized state), dollar-sign API for lazy boundaries, Qwik City file-based routing with lazy loaders/actions, and automatic bundle …
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build instantly interactive web applications with Qwik's resumability model — no hydration, lazy everything, serialized state in HTML, and automatic code splitting per component and per event.
Exact user phrases: "Qwik", "Qwik architecture", "Qwik resumable", "Qwik City", "Qwik component", "$()", "resumable", "lazy loading Qwik", "Qwik vs React".
Before activating, verify:
No file output. Produces component code, routing patterns, and resumability explanations as text.
Code: show component$(), $(), routeLoader$(), routeAction$(). Show serialization and lazy boundaries.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Code: 15 lines per example. Unlimited patterns.
| Approach | Trade-off | When to Use |
|---|---|---|
| Pure component$() + useSignal | Simplest, minimal chunks | Simple interactive widgets |
| Component + useStore() for deep state | More serialization, finer updates | Forms with multiple fields |
| useContext() + useContextProvider() | Shared state without prop drilling | Theme, auth, global filters |
| Inline components (no $) | Eager, no code-splitting | Non-interactive wrapper layouts |
| Server$() + routeLoader$() | Full server-side logic | Data-fetching heavy pages |
Do you need shared state across components?
├── No → useSignal() or useStore() locally
└── Yes → Do you need it across routes?
├── No → useContext() + useContextProvider()
└── Yes → useSignal() in root + pass via props
Does the component need to be lazy?
├── No (non-interactive wrapper) → plain function component
└── Yes (has state or handlers) → component$()
Where does data come from?
├── Route-level data → routeLoader$() in route file
├── User action/mutation → routeAction$() + <Form>
├── Component-level data → $() closure called from event
└── Real-time / WebSocket → useVisibleTask$() with WebSocket
What creates a lazy boundary?
├── A component → component$()
├── An event handler → onClick$(), onInput$(), onSubmit$()
├── A closure → $()
├── A data loader → routeLoader$()
└── An action → routeAction$()
What type of side effect?
├── Data fetch on load → routeLoader$() in parent route
├── DOM measurement → useVisibleTask$() with cleanup
├── Third-party widget init → useVisibleTask$() with NoSerialize
├── Analytics/tracking → useVisibleTask$() with IntersectionObserver
└── Polling/subscription → useVisibleTask$() with cleanup interval
import { component$, useSignal } from '@builder.io/qwik'
export default component$(() => {
const count = useSignal(0)
const step = useSignal(1)
return (
<div>
<p>Count: {count.value}</p>
<button onClick$={() => count.value += step.value}>Increment</button>
<button onClick$={() => count.value -= step.value}>Decrement</button>
<input type="number" bind:value={step} />
</div>
)
})
import { component$, useStore } from '@builder.io/qwik'
interface FormState {
email: string
password: string
errors: { email?: string; password?: string }
}
export default component$(() => {
const form = useStore<FormState>({
email: '',
password: '',
errors: {},
})
const validate = $((field: string) => {
if (field === 'email' && !form.email.includes('@')) {
form.errors.email = 'Invalid email'
} else {
delete form.errors.email
}
})
return (
<form onSubmit$={() => console.log(form.email, form.password)}>
<input name="email" onInput$={(_, el) => { form.email = el.value; validate('email') }} />
{form.errors.email && <span>{form.errors.email}</span>}
<input name="password" type="password" onInput$={(_, el) => form.password = el.value} />
<button type="submit">Submit</button>
</form>
)
})
import { createContextId, useContextProvider, useContext, component$, Slot } from '@builder.io/qwik'
export const ThemeContext = createContextId<'light' | 'dark'>('theme')
export const ThemeProvider = component$(() => {
useContextProvider(ThemeContext, 'dark')
return <Slot />
})
export const ThemedButton = component$(() => {
const theme = useContext(ThemeContext)
return <button class={`btn-${theme}`}>Click me</button>
})
import { server$ } from '@builder.io/qwik-city'
import { component$, useSignal } from '@builder.io/qwik'
const searchProducts = server$(async (query: string) => {
const results = await db.product.findMany({
where: { name: { contains: query } },
take: 10,
})
return results
})
export default component$(() => {
const query = useSignal('')
const results = useSignal<Product[]>([])
return (
<div>
<input bind:value={query} onInput$={async (_, el) => {
results.value = await searchProducts(el.value)
}} />
<ul>{results.value.map(p => <li>{p.name}</li>)}</ul>
</div>
)
})
// src/routes/dashboard/layout.tsx
import { component$, Slot } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
export const useAuthCheck = routeLoader$(async ({ redirect, cookie }) => {
const token = cookie.get('token')?.value
if (!token) throw redirect(302, '/login')
return token
})
export default component$(() => {
return (
<div class="dashboard-layout">
<nav>
<a href="/dashboard">Home</a>
<a href="/dashboard/settings">Settings</a>
</nav>
<main><Slot /></main>
</div>
)
})
export const useProductList = routeLoader$(async ({ query }) => {
const page = Number(query.get('page')) || 1
const [products, total] = await Promise.all([
db.product.findMany({ skip: (page - 1) * 20, take: 20 }),
db.product.count(),
])
return { products: products as Product[], total, page }
})
export default component$(() => {
const data = useProductList()
return (
<div>
{data.value.products.map(p => <ProductCard product={p} />)}
{data.value.page > 1 && <a href={`?page=${data.value.page - 1}`}>Previous</a>}
{data.value.page * 20 < data.value.total && <a href={`?page=${data.value.page + 1}`}>Next</a>}
</div>
)
})
Simple primitive values that trigger reactive updates when .value changes:
const count = useSignal(0)
count.value++ // triggers re-render
const readonly = count.value // reads current value
Deep objects with nested reactivity:
const user = useStore({
profile: { name: '', email: '' },
preferences: { theme: 'light', notifications: true },
metadata: { lastLogin: null as Date | null },
})
user.profile.name = 'John' // triggers re-render
const items = useSignal<Item[]>([])
const filter = useSignal('')
const filteredItems = useComputed$(() =>
items.value.filter(i => i.name.includes(filter.value))
)
// filteredItems.value updates automatically when items or filter change
// auth-context.ts
export const AuthContext = createContextId<{ user: User | null; token: string | null }>('auth')
// root.tsx
export default component$(() => {
useContextProvider(AuthContext, { user: null, token: null })
return <Slot />
})
// any child
const auth = useContext(AuthContext)
Data fetched on the server, serialized into HTML, available to the client without a separate API call:
export const usePosts = routeLoader$(async () => {
const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } })
return posts as Post[]
})
// Access: const posts = usePosts(); posts.value
export const useLogin = routeAction$(async (form, { fail, cookie }) => {
const user = await db.user.findUnique({ where: { email: form.get('email') } })
if (!user) return fail(401, { message: 'Invalid credentials' })
cookie.set('token', user.token, { httpOnly: true, maxAge: 86400 })
})
// Access: const action = useLogin(); action.value?.failed; action.value?.message
$() boundary produces a separate chunk. A page with 20 interactive components produces ~20-60 small chunks (2-5KB each).// Level 1: PrefetchServiceWorker — preloads links on hover
import { PrefetchServiceWorker } from '@builder.io/qwik/prefetch-service-worker'
// Level 2: Prefetch resources for above-the-fold interactions
// In root.tsx:
<link rel="prefetch" href="/build/q-abc123.js" />
Use PrefetchServiceWorker in all production builds. Monitor the Service Worker cache in DevTools to verify prefetch coverage.
useVisibleTask$ with track option to re-run on signal changes:useVisibleTask$(({ track }) => {
track(() => someSignal.value)
// re-runs when someSignal changes
})
npm run qwik build
# Output in dist/ — contains HTML, JS chunks per $ boundary, CSS
Qwik generates many small chunks by design. To analyze:
npx qwik build --analyze
# Opens bundle analysis in browser
Qwik's optimizer is configured through Vite. Key settings in vite.config.ts:
import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
export default defineConfig({
plugins: [
qwikCity(),
qwikVite({
ssr: { input: 'src/entry.ssr.tsx' },
client: { input: 'src/entry.dev.tsx' },
// Optimizer options
entryStrategy: { type: 'smart' }, // 'smart' | 'hoist' | 'single'
symbolMapper: true,
genDts: false,
}),
],
})
Entry strategy options:
smart (default): Splits into optimal chunks per $ boundaryhoist: Fewer, larger chunks — better for non-lazy scenariossingle: Single bundle — disables all lazy loading// qwik.config.ts (Cloudflare Pages)
import { extendConfig } from '@builder.io/qwik-city/vite'
import cloudflarePages from '@builder.io/qwik-city/adapters/cloudflare-pages/vite'
export default extendConfig(baseConfig, () => ({
plugins: [cloudflarePages()],
}))
Available adapters: cloudflare-pages, vercel-edge, node-server, deno-server, static.
import { staticGenerate } from '@builder.io/qwik-city/static'
// Configure in vite.config.ts for SSG routes
SSG generates HTML at build time for static routes. Dynamic routes still use SSR.
// Accessible anywhere
const apiUrl = import.meta.env.PUBLIC_API_URL
const mode = import.meta.env.MODE // 'development' | 'production'
Prefix with PUBLIC_ for client-accessible vars. Private vars available only on server.
// __tests__/counter.test.tsx
import { createDOM } from '@builder.io/qwik/testing'
import { test, expect } from 'vitest'
import Counter from './counter'
test('increments count', async () => {
const { screen, render, userEvent } = await createDOM()
await render(<Counter />)
expect(screen.innerText).toContain('0')
await userEvent('button', 'click')
expect(screen.innerText).toContain('1')
})
// __tests__/routes.test.ts
import { test, expect } from 'vitest'
import { useProductData } from '../src/routes/product/[id]/index'
test('loader returns product', async () => {
const loader = useProductData()
const result = await loader({ params: { id: '1' }, ...mockContext })
expect(result).toHaveProperty('name')
})
import { test, expect } from 'vitest'
import { createUser } from '../src/components/createUser'
test('creates user on server', async () => {
const user = await createUser({ email: 'test@test.com', name: 'Test' })
expect(user.email).toBe('test@test.com')
})
// e2e/app.spec.ts
import { test, expect } from '@playwright/test'
test('page loads without JS', async ({ page }) => {
await page.goto('/')
// Content is visible even without JS
expect(await page.locator('h1').textContent()).toBe('Welcome')
})
test('interaction lazy-loads chunk', async ({ page }) => {
await page.goto('/')
await page.locator('button').click()
// Button handler downloaded lazily, interaction still works
expect(await page.locator('output').textContent()).toBe('1')
})
test('form action works', async ({ page }) => {
await page.goto('/dashboard')
await page.fill('input[name="name"]', 'John')
await page.click('button[type="submit"]')
await expect(page.locator('.success')).toBeVisible()
})
Component conversion:
// React
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// Qwik
export default component$(() => {
const count = useSignal(0)
return <button onClick$={() => count.value++}>{count.value}</button>
})
Hook conversion:
useState -> useSignal / useStore
useEffect -> useVisibleTask$ (rare) or routeLoader$
useContext -> useContext + useContextProvider
useReducer -> useStore with action functions
useMemo -> useComputed$
useCallback -> $()
useRef -> useSignal (for elements)
State management:
Redux/Zustand -> useContext + useStore OR routeLoader$
React Query -> routeLoader$ + routeAction$
React Router -> Qwik City file-based routing
Formik/React Hook -> routeAction$ + <Form>
Form
| Next.js | Qwik City |
|---|---|
| pages/ directory | src/routes/ directory |
| getServerSideProps | routeLoader$ |
| API routes | src/routes/api/ or server$ |
| Layout (app dir) | layout.tsx per route |
| Server Actions | routeAction$ |
| Middleware | plugin@name.ts |
| next/image | qwik-image |
Add Qwik to an existing project via micro-frontends or iframe embedding. Use Qwik on new interactive features while keeping the legacy app running. Qwik can be embedded in any page via a script tag — it doesn't require full app control.
// Anti-pattern: heavy computation at module level
const data = expensiveCalculation() // runs on every import
// Correct: lazy $ boundary
const getData = $(() => expensiveCalculation())
// Anti-pattern: dynamic $ breaks optimizer
const handler = condition ? $(fn1) : $(fn2)
// Correct: static $ boundaries
if (condition) {
return <button onClick$={fn1}>A</button>
}
return <button onClick$={fn2}>B</button>
// Anti-pattern: data fetch in useVisibleTask$
useVisibleTask$(async () => {
const data = await fetch('/api/data').then(r => r.json())
state.value = data
})
// Correct: routeLoader$ for data
export const useData = routeLoader$(async () => {
return await fetch('/api/data').then(r => r.json())
})
Without PrefetchServiceWorker, every lazy interaction triggers a network waterfall. Always include it in the root layout.
// Anti-pattern: reassignment
count = 5
// Correct: .value assignment
count.value = 5
// Anti-pattern: function as prop
<Child onEvent={fn} />
// Correct: $ suffix on event
<Child onEvent$={fn} />
Qwik has its own reactivity model. Importing React patterns causes confusion and breaks resumability. Always use Qwik primitives.
// Anti-pattern: no cleanup — memory leak
useVisibleTask$(() => {
window.addEventListener('scroll', handler)
})
// Correct: cleanup
useVisibleTask$(() => {
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
})
// Wrong — click handler not lazy, entire component eager
<button onClick={() => count.value++}>
// Correct — click handler is lazy boundary
<button onClick$={() => count.value++}>
Always suffix interactive events with $ to mark lazy boundaries.
// Wrong — inline closure not extractable by optimizer
export default component$(() => {
const process = (data: string) => console.log(data)
return <button onClick$={() => process('hi')}>Click</button>
})
// Correct — $() makes closure lazy-loadable
export default component$(() => {
const process = $((data: string) => console.log(data))
return <button onClick$={() => process('hi')}>Click</button>
})
useVisibleTask$ puts code into the client bundle eagerly. Prefer $() closures called from event handlers. Only use useVisibleTask$ for intersection-observer logic or third-party widget initialization.
The Qwik optimizer relies on static analysis. Dynamic $() calls prevent code extraction:
// Wrong — optimizer cannot analyze this
const handler = condition ? $(fn1) : $(fn2)
// Correct — let optimizer see both paths
if (condition) {
return <button onClick$={fn1}>A</button>
}
return <button onClick$={fn2}>B</button>
Without prefetching, the first interaction triggers a network fetch that feels slow. Always enable PrefetchServiceWorker in root.tsx for instant navigation.
useSignal.value = x replaces the value. useStore.field = x mutates in place. Mixing these patterns causes subtle bugs.
| Aspect | Qwik | React |
|---|---|---|
| Hydration model | Resumable (no replay) | Hydration (replay all) |
| Bundle size | Proportional to interactions | Proportional to page weight |
| Time to interactive | Constant (~1ms) | Scales with component count |
| Code splitting | Automatic per $ boundary | Manual with React.lazy |
| State serialization | Auto in HTML | Manual (React Query, Zustand) |
| Learning curve | Higher (dollar-sign API) | Lower (standard JSX) |
| Ecosystem | Smaller, growing | Massive, mature |
Both avoid VDOM, but Qwik differs by lazy-loading per event rather than compiling fine-grained reactivity. SolidJS is eager but granular; Qwik is fully lazy but requires opt-in syntax.
Astro ships zero JS by default but hydrates entire framework islands eagerly. Qwik can lazy-load individual event handlers on a component, making it more granular. Astro is better for content sites; Qwik for interactive apps.
| Package | Purpose |
|---|---|
| @builder.io/qwik | Core framework with compiler and runtime |
| @builder.io/qwik-city | Meta-framework with routing, loaders, actions |
| @builder.io/qwik-labs | Experimental features (partytown, etc.) |
$() boundaries, auto-import for QRL utilities.export default component$(() => {
const count = useSignal(0)
return <button onClick$={() => count.value++}>{count.value}</button>
})
No hydration — HTML contains serialized state + QRL references. Browser resumes execution without replaying component. HTML is functional without JS.
export default component$(() => {
const name = useSignal('World')
const greet = $((greeting: string) => {
alert(`${greeting}, ${name.value}!`)
})
return <button onClick$={() => greet('Hello')}>Greet</button>
})
component$() — lazy component. $() — lazy closure. onClick$() — lazy event. useVisibleTask$() — lazy client effect. useComputed$() — lazy computed.
src/routes/
index.tsx -> /
about/index.tsx -> /about
product/[id]/index.tsx -> /product/:id
dashboard/layout.tsx -> layout
api/users/index.ts -> resource route
plugin@auth.ts -> middleware
All routes, layouts, and data are lazy-loaded. Use <Slot /> for layout children. Plugin files (plugin@name.ts) run as request middleware.
export const useProductData = routeLoader$(async ({ params, redirect }) => {
const product = await db.product.findUnique({ where: { id: params.id } })
if (!product) throw redirect(302, '/products')
return product as Product
})
export const useCreateProduct = routeAction$(async (form, { fail }) => {
const name = form.get('name')
if (!name || name.length < 2) return fail(400, { fieldErrors: { name: 'Min 2 chars' } })
await db.product.create({ data: { name } })
return { success: true }
})
Use <Form action={action}> for progressive enhancement. Access action value via action.value for status and field errors.
// src/routes/dashboard/layout.tsx
export default component$(() => {
return (
<div class="dashboard">
<nav><Link href="/dashboard/settings">Settings</Link></nav>
<main><Slot /></main>
</div>
)
})
Middleware via plugin@name.ts runs on every request for that route segment. Use for auth guards, logging, redirects.
// src/routes/api/users/index.ts
export const onGet: RequestHandler = async ({ json }) => {
const users = await db.user.findMany()
json(200, users)
}
Use onGet, onPost, onPut, onDelete, onPatch for REST handlers.
// Root — enable prefetching
import { PrefetchServiceWorker } from '@builder.io/qwik/prefetch-service-worker'
<head>
<PrefetchServiceWorker />
</head>
Prefetch strategy is critical for instant navigation. Bundle splitting is automatic — per component and per event handler. Monitor chunk sizes with qwik inspect.
$ suffix marks lazy boundaries (component$, onClick$, $()).No artifact produced. Next skill: frontend-universal-seo or frontend-universal-testing for Qwik projects. Carry forward: component$ patterns, route loader/action conventions, prefetch strategy.