SiamCafe.net Blog
Technology

Qwik Resumability Site Reliability SRE

qwik resumability site reliability sre
Qwik Resumability Site Reliability SRE | SiamCafe Blog
2025-12-27· อ. บอม — SiamCafe.net· 9,371 คำ

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

Qwik คืออะไร

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

Resumability ต่างจาก Hydration อย่างไร

Hydration ต้อง Download JS ทั้งหมดแล้ว Re-execute บน Client Resumability Serialize State ใน HTML แล้ว Resume จากจุดนั้นเลย ไม่ต้อง Re-execute ทำให้ TTI เร็วกว่ามาก โดยเฉพาะ App ขนาดใหญ่ที่มี Components เยอะ

SRE เกี่ยวข้องกับ Frontend อย่างไร

SRE ดูแล Reliability ทั้งระบบรวมถึง Frontend Core Web Vitals เป็น SLI สำคัญที่ส่งผลต่อ User Experience และ SEO โดยตรง Qwik ช่วยให้ได้ Score ดีโดย Architecture ตั้ง SLO สำหรับ LCP, INP, CLS และ Monitor ด้วย RUM

Qwik เหมาะกับงานแบบไหน

เหมาะกับเว็บที่ต้องการ Performance สูงสุด E-commerce, Landing Page, Content-heavy Sites, Dashboard ที่มี Components เยอะ ไม่เหมาะกับ App ที่ต้อง Interact ทุก Component ตั้งแต่แรก เช่น Game หรือ Real-time Editor

สรุป

Qwik กับ Resumability เป็นแนวคิดที่เปลี่ยนวิธีคิดเรื่อง Frontend Performance ไม่ต้อง Hydrate JavaScript ทั้งหมด โหลดเฉพาะที่ต้องใช้ สำหรับ SRE ช่วยให้ Core Web Vitals ดีโดยไม่ต้อง Optimize มาก ตั้ง SLO สำหรับ LCP, INP, CLS Monitor ด้วย RUM ใช้ Canary Deployment และ Automated Rollback เมื่อ Performance Degrade

📖 บทความที่เกี่ยวข้อง

Payload CMS Site Reliability SREอ่านบทความ → Qwik Resumability Container Orchestrationอ่านบทความ → Qwik Resumability Best Practices ที่ต้องรู้อ่านบทความ → Qwik Resumability Automation Scriptอ่านบทความ → Qwik Resumability Service Mesh Setupอ่านบทความ →

📚 ดูบทความทั้งหมด →