Qwik และ Resumability คืออะไร

Qwik เป็น JavaScript Framework ที่ใช้แนวคิด Resumability แทน Hydration แบบเดิม ปัญหาของ Hydration คือต้อง Download JavaScript Bundle ทั้งหมดแล้ว Execute บน Client เพื่อทำให้หน้า Interactive แม้จะใช้ SSR ก็ตาม ทำให้ Time to Interactive (TTI) ช้า โดยเฉพาะบน Mobile หรือเครือข่ายช้า

Resumability แก้ปัญหานี้โดย Serialize Application State และ Event Listeners ไว้ใน HTML เมื่อ User Interact กับ Component ไหน JavaScript ของ Component นั้นจะถูกโหลดและ Execute เฉพาะส่วนที่ต้องการ (Lazy Loading at Interaction Level) ทำให้ Initial Load เร็วมากเพราะ JavaScript ที่โหลดตอนแรกแทบจะเป็น 0

ติดตั้งและสร้างโปรเจค Qwik

# สร้างโปรเจค Qwik
npm create qwik@latest

# เลือก:
# - App (QwikCity) สำหรับ Full-stack App
# - Library สำหรับ Component Library

cd my-qwik-app

# ติดตั้ง Dependencies
npm install

# รัน Development Server
npm run dev
# เปิด http://localhost:5173

# Build สำหรับ Production
npm run build

# Preview Production Build
npm run preview

# === โครงสร้างโปรเจค ===
my-qwik-app/
├── src/
│ ├── components/ # Shared Components
│ │ ├── header/
│ │ └── footer/
│ ├── routes/ # File-based Routing
│ │ ├── index.tsx # Home Page
│ │ ├── about/
│ │ │ └── index.tsx
│ │ ├── api/ # API Routes
│ │ │ └── health/
│ │ │ └── index.ts
│ │ └── layout.tsx # Root Layout
│ ├── entry.ssr.tsx # SSR Entry
│ └── root.tsx # Root Component
├── public/ # Static Assets
├── adapters/ # Deploy Adapters
│ └── cloudflare-pages/
├── package.json
├── tsconfig.json
└── vite.config.ts

# === Deploy Adapters ===
# Cloudflare Pages
npm run qwik add cloudflare-pages

# Vercel
npm run qwik add vercel-edge

# Node.js Server
npm run qwik add express

# Netlify
npm run qwik add netlify-edge

Qwik Components กับ SRE Monitoring

// src/components/performance-monitor.tsx
// Component สำหรับ Monitor Core Web Vitals
import { component$, useVisibleTask$, useStore } from '@builder.io/qwik';

interface WebVitals {
 lcp: number | null;
 inp: number | null;
 cls: number | null;
 fcp: number | null;
 ttfb: number | null;
}

export const PerformanceMonitor = component$(() => {
 const vitals = useStore<WebVitals>({
 lcp: null, inp: null, cls: null, fcp: null, ttfb: null,
 });

 // useVisibleTask$ — รันเฉพาะเมื่อ Component Visible
 // นี่คือตัวอย่าง Resumability: Code นี้ไม่โหลดจนกว่า Component จะ Visible
 useVisibleTask$(async () => {
 const { onLCP, onINP, onCLS, onFCP, onTTFB } = await import('web-vitals');

 onLCP((metric) => {
 vitals.lcp = Math.round(metric.value);
 reportToSRE('LCP', metric);
 });

 onINP((metric) => {
 vitals.inp = Math.round(metric.value);
 reportToSRE('INP', metric);
 });

 onCLS((metric) => {
 vitals.cls = Math.round(metric.value * 1000) / 1000;
 reportToSRE('CLS', metric);
 });

 onFCP((metric) => {
 vitals.fcp = Math.round(metric.value);
 reportToSRE('FCP', metric);
 });

 onTTFB((metric) => {
 vitals.ttfb = Math.round(metric.value);
 reportToSRE('TTFB', metric);
 });
 });

 return (
 <div class="vitals-panel">
 <h3>Core Web Vitals</h3>
 <div class="vitals-grid">
 <VitalCard name="LCP" value={vitals.lcp} unit="ms"
 good={2500} poor={4000} />
 <VitalCard name="INP" value={vitals.inp} unit="ms"
 good={200} poor={500} />
 <VitalCard name="CLS" value={vitals.cls} unit=""
 good={0.1} poor={0.25} />
 <VitalCard name="FCP" value={vitals.fcp} unit="ms"
 good={1800} poor={3000} />
 <VitalCard name="TTFB" value={vitals.ttfb} unit="ms"
 good={800} poor={1800} />
 </div>
 </div>
 );
});

// Vital Card Component
const VitalCard = component$<{
 name: string; value: number | null; unit: string;
 good: number; poor: number;
}>((props) => {
 const status = props.value === null ? 'loading'
 : props.value <= props.good ? 'good'
 : props.value <= props.poor ? 'needs-improvement'
 : 'poor';

 return (
 <div class={`vital-card `}>
 <span class="vital-name">{props.name}</span>
 <span class="vital-value">
 {props.value !== null ? `` : '...'}
 </span>
 </div>
 );
});

// ส่งข้อมูลไป SRE Monitoring (Prometheus/Datadog)
async function reportToSRE(name: string, metric: any) {
 try {
 await fetch('/api/vitals', {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify({
 name, value: metric.value, rating: metric.rating,
 navigationType: metric.navigationType,
 timestamp: Date.now(),
 url: window.location.href,
 userAgent: navigator.userAgent,
 }),
 keepalive: true,
 });
 } catch (e) {
 // Silent fail — ไม่กระทบ User Experience
 }
}

// --- src/routes/api/vitals/index.ts ---
// API Route สำหรับรับ Web Vitals Data
import type { RequestHandler } from '@builder.io/qwik-city';

export const onPost: RequestHandler = async ({ json, request }) => {
 const data = await request.json();

 // ส่งไป Prometheus Pushgateway
 const metrics = `
web_vitals_{url="", rating=""} 
 `.trim();

 await fetch('http://prometheus-pushgateway:9091/metrics/job/web_vitals', {
 method: 'POST',
 body: metrics,
 });

 json(200, { status: 'ok' });
};

Error Handling และ Reliability

// src/components/error-boundary.tsx
// Error Boundary สำหรับ Qwik
import { component$, Slot, useErrorBoundary } from '@builder.io/qwik';

export const ErrorBoundary = component$(() => {
 const error = useErrorBoundary();

 if (error.error) {
 // Report error ไป SRE
 reportError(error.error);

 return (
 <div class="error-fallback">
 <h2>Something went wrong</h2>
 <p>We have been notified and are working on it.</p>
 <button onClick$={() => window.location.reload()}>
 Retry
 </button>
 </div>
 );
 }

 return <Slot />;
});

function reportError(error: any) {
 fetch('/api/errors', {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify({
 message: error.message,
 stack: error.stack,
 url: window.location.href,
 timestamp: Date.now(),
 }),
 keepalive: true,
 }).catch(() => {});
}

// --- src/routes/api/health/index.ts ---
// Health Check Endpoint สำหรับ Load Balancer
import type { RequestHandler } from '@builder.io/qwik-city';

export const onGet: RequestHandler = async ({ json }) => {
 const checks = {
 status: 'healthy',
 timestamp: new Date().toISOString(),
 uptime: process.uptime(),
 memory: process.memoryUsage(),
 version: process.env.APP_VERSION || 'unknown',
 };

 // ตรวจสอบ Dependencies
 try {
 // ตรวจสอบ API Backend
 const apiCheck = await fetch('http://api-backend:8080/health',
 { signal: AbortSignal.timeout(3000) });
 checks.api = apiCheck.ok ? 'healthy' : 'unhealthy';
 } catch {
 checks.api = 'unhealthy';
 checks.status = 'degraded';
 }

 const statusCode = checks.status === 'healthy' ? 200 : 503;
 json(statusCode, checks);
};

Deployment Strategy สำหรับ SRE

# === Dockerfile สำหรับ Qwik App ===
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/server /app/server
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/package.json /app/

USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server/entry.express.js"]

---
# === Kubernetes Deployment ===
apiVersion: apps/v1
kind: Deployment
metadata:
 name: qwik-app
 labels:
 app: qwik-app
 version: v1
spec:
 replicas: 3
 strategy:
 type: RollingUpdate
 rollingUpdate:
 maxSurge: 1
 maxUnavailable: 0
 selector:
 matchLabels:
 app: qwik-app
 template:
 metadata:
 labels:
 app: qwik-app
 spec:
 containers:
 - name: qwik-app
 image: registry/qwik-app:latest
 ports:
 - containerPort: 3000
 resources:
 requests:
 cpu: 100m
 memory: 128Mi
 limits:
 cpu: 500m
 memory: 512Mi
 livenessProbe:
 httpGet:
 path: /api/health
 port: 3000
 initialDelaySeconds: 10
 periodSeconds: 15
 readinessProbe:
 httpGet:
 path: /api/health
 port: 3000
 initialDelaySeconds: 5
 periodSeconds: 5
 env:
 - name: NODE_ENV
 value: production
 - name: APP_VERSION
 valueFrom:
 fieldRef:
 fieldPath: metadata.labels['version']
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
 name: qwik-app-pdb
spec:
 minAvailable: 2
 selector:
 matchLabels:
 app: qwik-app

SRE Best Practices สำหรับ Qwik

  • SLI/SLO: ตั้ง SLO สำหรับ Core Web Vitals เช่น LCP p95 < 2.5s, INP p95 < 200ms, CLS p95 < 0.1
  • Monitoring: ใช้ Real User Monitoring (RUM) เก็บ Web Vitals จาก User จริง ไม่ใช่แค่ Synthetic Tests
  • CDN: ใช้ CDN สำหรับ Static Assets Qwik Chunks จะถูก Cache ได้ดีเพราะมี Content Hash
  • Error Budget: กำหนด Error Budget สำหรับ Frontend Errors ถ้าเกินให้หยุด Feature Development แก้ Reliability
  • Canary Deployment: Deploy Version ใหม่ให้ 5% ของ Traffic ก่อน ตรวจสอบ Web Vitals แล้วค่อยขยาย
  • Rollback: มี Automated Rollback เมื่อ Web Vitals หรือ Error Rate เกิน Threshold

Qwik คืออะไร

Qwik เป็น JavaScript Framework ใช้ Resumability แทน Hydration ทำให้หน้าเว็บ Interactive เร็วมากเพราะไม่ต้อง Download JavaScript ทั้งหมดตอนโหลด โหลดเฉพาะเมื่อ User Interact กับ Component Initial JavaScript แทบเป็น 0KB