วันที่เจอปัญหา Latency
วันหนึ่งได้รายงานจาก product team: “API ช้ามาก! User จาก Singapore เรียก API แล้วรอ 800ms” 😱
ตอนนั้นใช้ Express.js deploy อยู่ที่ AWS us-east-1:
// server.js - Express.js ที่ deploy ในอเมริกา
const express = require('express');
const app = express();
app.get('/api/user/:id', async (req, res) => {
// Query database ใช้เวลา ~50ms
const user = await db.users.findById(req.params.id);
// Response time ทั้งหมด:
// Network latency (SG -> US): ~400ms
// Database query: ~50ms
// Processing: ~10ms
// Network latency (US -> SG): ~400ms
// Total: ~860ms 🐌
res.json(user);
});
ปัญหาคือ: ไม่ว่าจะ optimize code ยังไง network latency จาก Singapore ไป US มันไม่มีทางเร็วขึ้นได้!
แล้วก็มาได้ยินเรื่อง “Edge Computing” และ Cloudflare Workers 🌟
การรู้จัก Cloudflare Workers ครั้งแรก
Edge Computing คือการรัน code ใกล้ๆ กับ user แทนที่จะรันใน data center กลาง
Cloudflare Workers คือ serverless platform ที่รันโค้ดใน 300+ locations ทั่วโลก!
Concept ที่เจ๋ง:
- Code รันใน edge location ที่ใกล้ user ที่สุด
- Cold start ต่ำกว่า 1ms
- Auto-scaling แบบ global
- จ่ายแค่ตาม requests
First Attempt กับ Cloudflare Workers
// worker.js - ครั้งแรกที่ลอง
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
if (url.pathname === '/api/hello') {
return new Response('Hello from the edge!', {
headers: { 'Content-Type': 'text/plain' },
});
}
return new Response('Not found', { status: 404 });
}
ปัญหาที่เจอ:
- Syntax แปลกๆ ไม่เหมือน Express
- Routing ต้องเขียนเอง
- Middleware concept ไม่มี
- TypeScript support ไม่ดี
ผลลัพธ์: ใช้เวลา 2 วันแต่งได้แค่ Hello World! 😅
การค้นพบ Hono Framework
พอเจอ Hono รู้เลยว่า “นี่แหละที่หาอยู่!”
Hono คืออะไร:
- Fast web framework สำหรับ edge runtimes
- Syntax คล้าย Express.js
- TypeScript first
- รองรับ Cloudflare Workers, Bun, Deno, Node.js
- Super lightweight (~13KB)
ลองใช้ Hono ครั้งแรก
// index.ts - Hono version
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => {
return c.text('Hello from Hono on the edge! 🔥');
});
app.get('/api/user/:id', async (c) => {
const userId = c.req.param('id');
// Simulate user data (ยังไม่มี database)
const user = {
id: userId,
name: `User ${userId}`,
location: c.req.header('cf-ipcountry') || 'Unknown'
};
return c.json(user);
});
export default app;
Deploy แค่:
npm install hono
npx wrangler dev # Local development
npx wrangler deploy # Deploy to edge
ผลลัพธ์: API ที่เคยใช้เวลา 800ms กลายเป็น 80ms! 🚀
เคสจริง: URL Shortener API
ต่อมาลองทำโปรเจคจริงๆ: URL Shortener ที่ต้องเร็วมาก
Architecture ที่ออกแบบ
// types.ts
export interface ShortUrl {
id: string;
originalUrl: string;
shortCode: string;
clicks: number;
createdAt: string;
expiresAt?: string;
}
export interface CreateUrlRequest {
url: string;
customCode?: string;
expiresIn?: number; // seconds
}
Main Application
// index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
// Middleware
app.use('*', logger());
app.use('/api/*', cors({
origin: ['https://myapp.com', 'http://localhost:3000'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// Validation schemas
const createUrlSchema = z.object({
url: z.string().url('Invalid URL format'),
customCode: z.string().min(3).max(10).optional(),
expiresIn: z.number().min(60).max(31536000).optional(), // 1 min to 1 year
});
// Routes
app.get('/', (c) => {
return c.html(`
<html>
<body>
<h1>URL Shortener API 🔗</h1>
<p>Running on Cloudflare Workers at the edge!</p>
<p>Location: ${c.req.header('cf-ipcountry')}</p>
<p>Datacenter: ${c.req.header('cf-colo')}</p>
</body>
</html>
`);
});
// Create short URL
app.post('/api/shorten',
zValidator('json', createUrlSchema),
async (c) => {
const { url, customCode, expiresIn } = c.req.valid('json');
try {
// Generate short code if not provided
const shortCode = customCode || generateShortCode();
// Check if custom code already exists
if (customCode) {
const existing = await getUrlByCode(shortCode);
if (existing) {
return c.json({
error: 'Custom code already exists',
code: 'CODE_EXISTS'
}, 409);
}
}
// Save to KV storage
const shortUrl: ShortUrl = {
id: crypto.randomUUID(),
originalUrl: url,
shortCode,
clicks: 0,
createdAt: new Date().toISOString(),
expiresAt: expiresIn
? new Date(Date.now() + expiresIn * 1000).toISOString()
: undefined
};
await saveUrl(shortUrl);
// Analytics event
c.executionCtx.waitUntil(
trackEvent('url_created', {
shortCode,
country: c.req.header('cf-ipcountry'),
userAgent: c.req.header('user-agent')
})
);
return c.json({
shortUrl: `https://short.ly/${shortCode}`,
shortCode,
originalUrl: url,
expiresAt: shortUrl.expiresAt
}, 201);
} catch (error) {
console.error('Error creating short URL:', error);
return c.json({
error: 'Failed to create short URL',
code: 'CREATION_FAILED'
}, 500);
}
}
);
// Redirect endpoint
app.get('/:code', async (c) => {
const shortCode = c.req.param('code');
try {
const urlData = await getUrlByCode(shortCode);
if (!urlData) {
return c.html(`
<html><body>
<h1>404 - Short URL not found 😞</h1>
<p>The link you're looking for doesn't exist or has expired.</p>
</body></html>
`, 404);
}
// Check expiration
if (urlData.expiresAt && new Date() > new Date(urlData.expiresAt)) {
return c.html(`
<html><body>
<h1>410 - Link Expired ⏰</h1>
<p>This short URL has expired.</p>
</body></html>
`, 410);
}
// Increment click count (async, don't wait)
c.executionCtx.waitUntil(incrementClicks(shortCode));
// Track click analytics
c.executionCtx.waitUntil(
trackEvent('url_clicked', {
shortCode,
originalUrl: urlData.originalUrl,
country: c.req.header('cf-ipcountry'),
referer: c.req.header('referer'),
userAgent: c.req.header('user-agent')
})
);
// Redirect
return c.redirect(urlData.originalUrl, 302);
} catch (error) {
console.error('Error redirecting:', error);
return c.json({ error: 'Redirect failed' }, 500);
}
});
// Get URL stats
app.get('/api/stats/:code', async (c) => {
const shortCode = c.req.param('code');
try {
const urlData = await getUrlByCode(shortCode);
if (!urlData) {
return c.json({ error: 'Short URL not found' }, 404);
}
const analytics = await getAnalytics(shortCode);
return c.json({
shortCode,
originalUrl: urlData.originalUrl,
clicks: urlData.clicks,
createdAt: urlData.createdAt,
expiresAt: urlData.expiresAt,
analytics
});
} catch (error) {
console.error('Error getting stats:', error);
return c.json({ error: 'Failed to get stats' }, 500);
}
});
export default app;
Data Layer กับ Cloudflare KV
// storage.ts
import type { ShortUrl } from './types';
declare const URL_STORE: KVNamespace;
declare const ANALYTICS_STORE: KVNamespace;
export async function saveUrl(shortUrl: ShortUrl): Promise<void> {
const key = `url:${shortUrl.shortCode}`;
await URL_STORE.put(key, JSON.stringify(shortUrl), {
expirationTtl: shortUrl.expiresAt
? Math.floor((new Date(shortUrl.expiresAt).getTime() - Date.now()) / 1000)
: undefined
});
}
export async function getUrlByCode(shortCode: string): Promise<ShortUrl | null> {
const key = `url:${shortCode}`;
const data = await URL_STORE.get(key);
if (!data) return null;
try {
return JSON.parse(data) as ShortUrl;
} catch {
return null;
}
}
export async function incrementClicks(shortCode: string): Promise<void> {
const urlData = await getUrlByCode(shortCode);
if (!urlData) return;
urlData.clicks += 1;
await saveUrl(urlData);
}
export async function trackEvent(
eventType: string,
data: Record<string, any>
): Promise<void> {
const timestamp = Date.now();
const key = `analytics:${eventType}:${timestamp}:${Math.random()}`;
await ANALYTICS_STORE.put(key, JSON.stringify({
eventType,
timestamp,
...data
}), {
expirationTtl: 30 * 24 * 60 * 60 // 30 days
});
}
export async function getAnalytics(shortCode: string): Promise<any> {
const prefix = 'analytics:url_clicked:';
const list = await ANALYTICS_STORE.list({ prefix });
const clicks = [];
for (const key of list.keys) {
const data = await ANALYTICS_STORE.get(key.name);
if (data) {
const event = JSON.parse(data);
if (event.shortCode === shortCode) {
clicks.push(event);
}
}
}
// Aggregate data
const countries = clicks.reduce((acc, click) => {
acc[click.country] = (acc[click.country] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const dailyClicks = clicks.reduce((acc, click) => {
const date = new Date(click.timestamp).toISOString().split('T')[0];
acc[date] = (acc[date] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return {
totalClicks: clicks.length,
countries,
dailyClicks,
topReferers: getTopReferers(clicks)
};
}
function getTopReferers(clicks: any[]): Record<string, number> {
return clicks
.filter(click => click.referer)
.reduce((acc, click) => {
const domain = new URL(click.referer).hostname;
acc[domain] = (acc[domain] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}
Utilities และ Helpers
// utils.ts
const CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
export function generateShortCode(length: number = 6): string {
let result = '';
for (let i = 0; i < length; i++) {
result += CHARACTERS.charAt(Math.floor(Math.random() * CHARACTERS.length));
}
return result;
}
export function isValidUrl(string: string): boolean {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
export function sanitizeUrl(url: string): string {
// Remove dangerous protocols
const dangerous = ['javascript:', 'data:', 'vbscript:', 'file:'];
const lower = url.toLowerCase().trim();
for (const protocol of dangerous) {
if (lower.startsWith(protocol)) {
throw new Error('Dangerous URL protocol detected');
}
}
// Add https if no protocol
if (!url.match(/^https?:\/\//)) {
return `https://${url}`;
}
return url;
}
Advanced Features ที่เพิ่มเข้าไป
1. Rate Limiting
// rate-limit.ts
import type { Context } from 'hono';
interface RateLimit {
requests: number;
resetTime: number;
}
declare const RATE_LIMIT_STORE: KVNamespace;
export async function rateLimitMiddleware(
requests: number = 100,
windowMs: number = 60 * 1000 // 1 minute
) {
return async (c: Context, next: Function) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const key = `ratelimit:${ip}`;
const now = Date.now();
const windowStart = now - windowMs;
// Get current rate limit data
const data = await RATE_LIMIT_STORE.get(key);
let rateLimit: RateLimit;
if (data) {
rateLimit = JSON.parse(data);
// Reset if window expired
if (rateLimit.resetTime < windowStart) {
rateLimit = { requests: 0, resetTime: now + windowMs };
}
} else {
rateLimit = { requests: 0, resetTime: now + windowMs };
}
// Check if limit exceeded
if (rateLimit.requests >= requests) {
const resetIn = Math.ceil((rateLimit.resetTime - now) / 1000);
return c.json({
error: 'Rate limit exceeded',
code: 'RATE_LIMIT_EXCEEDED',
resetIn
}, 429);
}
// Increment and save
rateLimit.requests += 1;
await RATE_LIMIT_STORE.put(key, JSON.stringify(rateLimit), {
expirationTtl: Math.ceil(windowMs / 1000)
});
// Add headers
c.res.headers.set('X-RateLimit-Limit', requests.toString());
c.res.headers.set('X-RateLimit-Remaining', (requests - rateLimit.requests).toString());
c.res.headers.set('X-RateLimit-Reset', rateLimit.resetTime.toString());
await next();
};
}
// Usage
app.use('/api/*', rateLimitMiddleware(100, 60 * 1000));
2. Caching Strategy
// cache.ts
import type { Context } from 'hono';
export function cache(ttl: number = 300) { // 5 minutes default
return async (c: Context, next: Function) => {
const cacheKey = `cache:${c.req.url}`;
// Try to get from cache first
const cached = await caches.default.match(c.req);
if (cached) {
console.log('Cache HIT:', cacheKey);
return cached;
}
console.log('Cache MISS:', cacheKey);
await next();
// Cache the response
if (c.res.status === 200) {
const response = c.res.clone();
response.headers.set('Cache-Control', `public, max-age=${ttl}`);
response.headers.set('X-Cache', 'MISS');
// Store in edge cache
c.executionCtx.waitUntil(
caches.default.put(c.req, response)
);
}
};
}
// Usage
app.get('/api/stats/:code', cache(60), async (c) => {
// ... stats logic
});
3. Error Handling และ Monitoring
// error-handler.ts
import type { Context } from 'hono';
export function errorHandler() {
return async (c: Context, next: Function) => {
try {
await next();
} catch (error) {
console.error('Unhandled error:', error);
// Log to external service (async)
c.executionCtx.waitUntil(
logError(error, {
url: c.req.url,
method: c.req.method,
headers: Object.fromEntries(c.req.headers.entries()),
userAgent: c.req.header('user-agent'),
ip: c.req.header('cf-connecting-ip'),
country: c.req.header('cf-ipcountry'),
timestamp: new Date().toISOString()
})
);
// Return user-friendly error
if (error instanceof z.ZodError) {
return c.json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: error.errors
}, 400);
}
return c.json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
}, 500);
}
};
}
async function logError(error: any, context: any): Promise<void> {
// Send to external logging service
try {
await fetch('https://api.logtail.com/logs', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
level: 'error',
message: error.message,
error: {
name: error.name,
stack: error.stack
},
context
})
});
} catch (logError) {
console.error('Failed to log error:', logError);
}
}
Performance Optimizations
1. Connection Reuse
// http-client.ts
class HTTPClient {
private static instance: HTTPClient;
private connections: Map<string, any> = new Map();
static getInstance(): HTTPClient {
if (!HTTPClient.instance) {
HTTPClient.instance = new HTTPClient();
}
return HTTPClient.instance;
}
async fetch(url: string, options?: RequestInit): Promise<Response> {
// Reuse connections for the same origin
const origin = new URL(url).origin;
return fetch(url, {
...options,
// Cloudflare Workers handles connection pooling
cf: {
cacheTtl: 300,
cacheEverything: false
}
});
}
}
export const httpClient = HTTPClient.getInstance();
2. Smart Caching
// smart-cache.ts
export function smartCache(c: Context, key: string, ttl: number) {
return {
async get<T>(): Promise<T | null> {
try {
const cached = await caches.default.match(
new Request(`https://cache.internal/${key}`)
);
if (cached) {
const data = await cached.json();
return data as T;
}
return null;
} catch {
return null;
}
},
async set<T>(data: T): Promise<void> {
try {
const response = new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': `public, max-age=${ttl}`,
'X-Cache-Key': key
}
});
c.executionCtx.waitUntil(
caches.default.put(
new Request(`https://cache.internal/${key}`),
response
)
);
} catch (error) {
console.warn('Failed to cache data:', error);
}
}
};
}
// Usage
app.get('/api/popular-urls', async (c) => {
const cache = smartCache(c, 'popular-urls', 300);
let popularUrls = await cache.get<ShortUrl[]>();
if (!popularUrls) {
popularUrls = await getPopularUrls();
await cache.set(popularUrls);
}
return c.json(popularUrls);
});
Deployment และ Configuration
1. wrangler.toml
name = "url-shortener"
main = "src/index.ts"
compatibility_date = "2023-12-01"
# KV Namespaces
kv_namespaces = [
{ binding = "URL_STORE", id = "your-kv-namespace-id" },
{ binding = "ANALYTICS_STORE", id = "your-analytics-namespace-id" },
{ binding = "RATE_LIMIT_STORE", id = "your-ratelimit-namespace-id" }
]
# Environment Variables
[env.production.vars]
ALLOWED_ORIGINS = "https://myapp.com,https://admin.myapp.com"
LOG_LEVEL = "info"
[env.staging.vars]
ALLOWED_ORIGINS = "https://staging.myapp.com"
LOG_LEVEL = "debug"
# Custom domains
[[env.production.routes]]
pattern = "short.ly/*"
zone_name = "short.ly"
[[env.staging.routes]]
pattern = "staging-short.ly/*"
zone_name = "short.ly"
2. GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Type check
run: npm run type-check
- name: Deploy to staging
if: github.ref == 'refs/heads/staging'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
environment: 'staging'
- name: Deploy to production
if: github.ref == 'refs/heads/main'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
environment: 'production'
Testing Strategies
1. Unit Tests
// tests/utils.test.ts
import { describe, test, expect } from 'vitest';
import { generateShortCode, isValidUrl, sanitizeUrl } from '../src/utils';
describe('Utils', () => {
describe('generateShortCode', () => {
test('should generate code of correct length', () => {
const code = generateShortCode(8);
expect(code).toHaveLength(8);
});
test('should generate unique codes', () => {
const codes = new Set();
for (let i = 0; i < 1000; i++) {
codes.add(generateShortCode());
}
expect(codes.size).toBe(1000);
});
});
describe('isValidUrl', () => {
test('should validate correct URLs', () => {
expect(isValidUrl('https://example.com')).toBe(true);
expect(isValidUrl('http://localhost:3000')).toBe(true);
});
test('should reject invalid URLs', () => {
expect(isValidUrl('not-a-url')).toBe(false);
expect(isValidUrl('javascript:alert(1)')).toBe(false);
});
});
});
2. Integration Tests
// tests/api.test.ts
import { describe, test, expect, beforeAll } from 'vitest';
// Mock Cloudflare Workers environment
const mockEnv = {
URL_STORE: {
get: vi.fn(),
put: vi.fn(),
delete: vi.fn()
},
ANALYTICS_STORE: {
get: vi.fn(),
put: vi.fn(),
list: vi.fn()
}
};
describe('API Endpoints', () => {
beforeAll(() => {
global.URL_STORE = mockEnv.URL_STORE;
global.ANALYTICS_STORE = mockEnv.ANALYTICS_STORE;
});
test('POST /api/shorten should create short URL', async () => {
const app = (await import('../src/index')).default;
const req = new Request('https://test.com/api/shorten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://example.com/very/long/url'
})
});
mockEnv.URL_STORE.get.mockResolvedValue(null); // No existing URL
mockEnv.URL_STORE.put.mockResolvedValue(undefined);
const response = await app.fetch(req);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.shortUrl).toMatch(/https:\/\/short\.ly\/[a-zA-Z0-9]+/);
expect(data.originalUrl).toBe('https://example.com/very/long/url');
});
});
Performance Results ที่ได้จริง
Before (Express.js on AWS):
Location: Singapore → US East (Virginia)
-------------------------
Cold start: ~2000ms
Warm request: ~800ms
Database query: ~50ms
Network latency: ~400ms each way
Total average: ~860ms
99th percentile: ~1200ms
After (Hono + Cloudflare Workers):
Location: Singapore → Singapore Edge
-----------------------------------
Cold start: <1ms
Request processing: ~15ms
KV storage: ~5ms
Network latency: ~20ms
Total average: ~40ms
99th percentile: ~80ms
Performance improvement: 95% faster! 🚀
Production Lessons Learned
1. KV Storage Limitations
// ❌ อย่าใช้ KV สำหรับ high-frequency writes
async function incrementCounter(key: string) {
const current = await KV_STORE.get(key) || '0';
const newValue = parseInt(current) + 1;
await KV_STORE.put(key, newValue.toString()); // Race condition!
}
// ✅ ใช้ Durable Objects สำหรับ consistent state
export class Counter {
constructor(state: DurableObjectState) {
this.state = state;
}
async increment(key: string): Promise<number> {
let count = await this.state.storage.get(key) || 0;
count += 1;
await this.state.storage.put(key, count);
return count;
}
}
2. Global State Management
// ❌ อย่าใช้ global variables
let globalCache: Map<string, any> = new Map(); // ไม่ work ใน Workers
// ✅ ใช้ execution context หรือ external storage
app.use('*', async (c, next) => {
// ใช้ c.set() สำหรับ request-scoped data
c.set('startTime', Date.now());
await next();
const duration = Date.now() - c.get('startTime');
console.log(`Request duration: ${duration}ms`);
});
3. Error Recovery
// Graceful degradation
app.get('/:code', async (c) => {
try {
const urlData = await getUrlByCode(shortCode);
if (urlData) {
return c.redirect(urlData.originalUrl);
}
} catch (error) {
// KV storage error - fallback to external API
try {
const fallbackUrl = await getFallbackUrl(shortCode);
if (fallbackUrl) {
return c.redirect(fallbackUrl);
}
} catch (fallbackError) {
// Complete failure - show friendly error
}
}
return c.html(notFoundPage, 404);
});
Cost Analysis
AWS (Previous Setup):
EC2 t3.medium: $30/month
RDS PostgreSQL: $25/month
Load Balancer: $18/month
Total: ~$73/month
Cloudflare Workers (Current):
Workers: $5/month (1M requests included)
KV storage: $0.50/month
Custom domain: $0 (included)
Total: ~$5.50/month
Cost saving: 92% 💰
เครื่องมือและ Best Practices
1. Development Tools
# Local development
npm install -g wrangler
wrangler dev --local # Run locally
wrangler dev # Run with remote resources
# Testing
npm install vitest @cloudflare/workers-types
npm run test
npm run test:watch
# Deployment
wrangler deploy --env staging
wrangler deploy --env production
2. Monitoring
// Custom analytics
app.use('*', async (c, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
// Log metrics (async)
c.executionCtx.waitUntil(
logMetrics({
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration,
country: c.req.header('cf-ipcountry'),
datacenter: c.req.header('cf-colo')
})
);
});
3. Security Headers
app.use('*', async (c, next) => {
await next();
// Security headers
c.res.headers.set('X-Frame-Options', 'DENY');
c.res.headers.set('X-Content-Type-Options', 'nosniff');
c.res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
c.res.headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
});
สรุป: Edge Computing ที่เปลี่ยนทุกอย่าง
ก่อนใช้ Hono + Cloudflare Workers:
- API latency สูง (800ms+)
- Server costs แพง ($70+/month)
- Scaling ยาก
- Cold starts ช้า
หลังใช้ Hono + Cloudflare Workers:
- API latency ต่ำมาก (40ms average)
- Costs ถูกมาก ($5/month)
- Auto-scaling global
- Cold starts เกือบไม่มี
ข้อดีที่ได้จริง:
- Performance เร็วขึ้น 95%
- Cost ถูกลง 92%
- Scalability แบบ global อัตโนมัติ
- Developer Experience ดีมาก (TypeScript first)
- Deployment ง่ายและเร็ว
ข้อจำกัดที่ต้องรู้:
- CPU time จำกัด 50ms per request
- Memory จำกัด 128MB
- Storage eventual consistency (KV)
- WebSocket ไม่รองรับ
- File system ไม่มี
คำแนะนำ:
- เหมาะกับ API ที่ต้องการ low latency
- ไม่เหมาะกับ long-running processes
- ใช้ KV storage สำหรับ read-heavy workloads
- ใช้ Durable Objects สำหรับ stateful applications
- Plan for global ตั้งแต่แรก
Hono + Cloudflare Workers มันเหมือน เครื่องบินไอพ่น ของ web development
เร็ว ประหยัด และไปได้ทั่วโลก! ✈️🌍
ตอนนี้ไม่อยากกลับไปใช้ traditional servers อีกแล้ว เพราะ edge computing มันอนาคตจริงๆ! 🚀