Add health endpoint for Swarm health checks
The /health route returns JSON status for Docker Swarm to verify container health during deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
380
docs/cloudflare-setup.md
Normal file
380
docs/cloudflare-setup.md
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
# Cloudflare Setup for Mylder Platform
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Cloudflare configuration for https://mylder.io - Next.js 16 frontend with self-hosted backend (Supabase, n8n) via Dokploy/Traefik.
|
||||||
|
|
||||||
|
**VPS:** 149.102.155.84
|
||||||
|
**Stack:** Next.js 16 → Traefik → Cloudflare CDN
|
||||||
|
|
||||||
|
## DNS Configuration
|
||||||
|
|
||||||
|
### Step 1: Add Domain to Cloudflare
|
||||||
|
1. Cloudflare Dashboard → Add Site → mylder.io
|
||||||
|
2. Update nameservers at domain registrar to Cloudflare's nameservers
|
||||||
|
|
||||||
|
### Step 2: DNS Records
|
||||||
|
|
||||||
|
| Type | Name | Target | Proxy Status | Notes |
|
||||||
|
|------|------|--------|--------------|-------|
|
||||||
|
| A | @ | 149.102.155.84 | Proxied (orange cloud) | Main site |
|
||||||
|
| A | www | 149.102.155.84 | Proxied | WWW redirect |
|
||||||
|
| A | dokploy | 149.102.155.84 | DNS Only (grey cloud) | Dokploy dashboard - skip CDN |
|
||||||
|
| A | supabase | 149.102.155.84 | DNS Only | Supabase API - skip CDN |
|
||||||
|
| A | n8n | 149.102.155.84 | DNS Only | n8n webhooks - skip CDN |
|
||||||
|
| A | gitea | 149.102.155.84 | DNS Only | Git push/pull - skip CDN |
|
||||||
|
| A | saas | 149.102.155.84 | DNS Only | Zulip chat - skip CDN |
|
||||||
|
|
||||||
|
**Key Principle:**
|
||||||
|
- **Proxied (CDN):** Frontend, static assets, public content
|
||||||
|
- **DNS Only:** Admin tools, APIs with authentication, real-time services
|
||||||
|
|
||||||
|
### Step 3: Subdomain Wildcard (Optional)
|
||||||
|
If you need dynamic subdomains (e.g., tenant.mylder.io):
|
||||||
|
```
|
||||||
|
CNAME * @ (Proxied)
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSL/TLS Settings
|
||||||
|
|
||||||
|
### Encryption Mode: Full (Strict)
|
||||||
|
**Path:** SSL/TLS → Overview → Full (strict)
|
||||||
|
|
||||||
|
**Why:** Traefik already provides Let's Encrypt certificates. Full (strict) ensures end-to-end encryption:
|
||||||
|
```
|
||||||
|
Browser → Cloudflare (encrypted) → Traefik (encrypted) → App
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edge Certificates
|
||||||
|
**Path:** SSL/TLS → Edge Certificates
|
||||||
|
|
||||||
|
- **Always Use HTTPS:** ON
|
||||||
|
- **Minimum TLS Version:** 1.2
|
||||||
|
- **Opportunistic Encryption:** ON
|
||||||
|
- **TLS 1.3:** ON
|
||||||
|
- **Automatic HTTPS Rewrites:** ON
|
||||||
|
- **Certificate Transparency Monitoring:** ON
|
||||||
|
|
||||||
|
### Origin Server
|
||||||
|
**Path:** SSL/TLS → Origin Server
|
||||||
|
|
||||||
|
No action needed - Traefik manages Let's Encrypt certificates automatically.
|
||||||
|
|
||||||
|
## Caching Rules
|
||||||
|
|
||||||
|
### Default Cache Levels
|
||||||
|
**Path:** Caching → Configuration
|
||||||
|
|
||||||
|
- **Caching Level:** Standard
|
||||||
|
- **Browser Cache TTL:** Respect Existing Headers
|
||||||
|
- **Always Online:** ON (serves stale content if origin is down)
|
||||||
|
|
||||||
|
### Cache Rules for Next.js
|
||||||
|
**Path:** Rules → Page Rules (legacy) or Cache Rules (new)**
|
||||||
|
|
||||||
|
#### Rule 1: Static Assets (Aggressive Caching)
|
||||||
|
```
|
||||||
|
URL Pattern: mylder.io/_next/static/*
|
||||||
|
Settings:
|
||||||
|
- Cache Level: Cache Everything
|
||||||
|
- Edge Cache TTL: 1 year
|
||||||
|
- Browser Cache TTL: 1 year
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Rule 2: Images (Smart Caching)
|
||||||
|
```
|
||||||
|
URL Pattern: mylder.io/_next/image/*
|
||||||
|
Settings:
|
||||||
|
- Cache Level: Cache Everything
|
||||||
|
- Edge Cache TTL: 30 days
|
||||||
|
- Browser Cache TTL: 7 days
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Rule 3: API Routes (No Cache)
|
||||||
|
```
|
||||||
|
URL Pattern: mylder.io/api/*
|
||||||
|
Settings:
|
||||||
|
- Cache Level: Bypass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Rule 4: Dynamic Pages (Smart Cache)
|
||||||
|
```
|
||||||
|
URL Pattern: mylder.io/*
|
||||||
|
Settings:
|
||||||
|
- Cache Level: Standard
|
||||||
|
- Edge Cache TTL: 2 hours
|
||||||
|
- Browser Cache TTL: 30 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next.js Cache Headers
|
||||||
|
Ensure your Next.js app sets proper headers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// next.config.ts
|
||||||
|
const nextConfig = {
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/_next/static/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=31536000, immutable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/_next/image/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=2592000, stale-while-revalidate=86400',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Purge Strategy
|
||||||
|
**Path:** Caching → Configuration → Purge Cache
|
||||||
|
|
||||||
|
- **On Deploy:** Purge Everything (via API or Dashboard)
|
||||||
|
- **On Content Update:** Purge by Tag/URL
|
||||||
|
- **Emergency:** Purge Everything button
|
||||||
|
|
||||||
|
**API Purge (for CI/CD):**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
|
||||||
|
-H "Authorization: Bearer {api_token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data '{"purge_everything":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Speed Optimization
|
||||||
|
|
||||||
|
### Auto Minify
|
||||||
|
**Path:** Speed → Optimization
|
||||||
|
|
||||||
|
- **JavaScript:** ON
|
||||||
|
- **CSS:** ON
|
||||||
|
- **HTML:** ON
|
||||||
|
|
||||||
|
### Brotli Compression
|
||||||
|
**Path:** Speed → Optimization
|
||||||
|
|
||||||
|
- **Brotli:** ON (better than gzip)
|
||||||
|
|
||||||
|
### Early Hints
|
||||||
|
**Path:** Speed → Optimization
|
||||||
|
|
||||||
|
- **Early Hints:** ON (preload critical resources)
|
||||||
|
|
||||||
|
### Rocket Loader
|
||||||
|
**Path:** Speed → Optimization
|
||||||
|
|
||||||
|
- **Rocket Loader:** OFF (can conflict with React hydration)
|
||||||
|
|
||||||
|
### Image Optimization
|
||||||
|
**Path:** Speed → Optimization → Image Resizing
|
||||||
|
|
||||||
|
- **Polish:** Lossless (or Lossy for smaller files)
|
||||||
|
- **Mirage:** ON (lazy loading for mobile)
|
||||||
|
- **Image Resizing:** Available on Pro+ plan
|
||||||
|
|
||||||
|
**Next.js Image Optimization:**
|
||||||
|
```typescript
|
||||||
|
// next.config.ts
|
||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
|
||||||
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||||
|
minimumCacheTTL: 2592000, // 30 days
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Settings
|
||||||
|
|
||||||
|
### SSL/TLS
|
||||||
|
- **Always Use HTTPS:** ON
|
||||||
|
- **HSTS:** Enable with max-age=31536000, includeSubDomains
|
||||||
|
|
||||||
|
### Firewall (WAF)
|
||||||
|
**Path:** Security → WAF
|
||||||
|
|
||||||
|
#### Managed Rules (Free Tier)
|
||||||
|
- **Cloudflare Managed Ruleset:** ON
|
||||||
|
- **OWASP Core Ruleset:** ON
|
||||||
|
|
||||||
|
#### Rate Limiting (Pro+ Plan)
|
||||||
|
```yaml
|
||||||
|
Rule: API Protection
|
||||||
|
If: Request URL contains /api/
|
||||||
|
Then: Rate limit 100 requests per minute per IP
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Bot Fight Mode (Free)
|
||||||
|
**Path:** Security → Bots
|
||||||
|
|
||||||
|
- **Bot Fight Mode:** ON
|
||||||
|
- **Super Bot Fight Mode:** Pro+ plan
|
||||||
|
|
||||||
|
### Security Level
|
||||||
|
**Path:** Security → Settings
|
||||||
|
|
||||||
|
- **Security Level:** Medium (balance between security and UX)
|
||||||
|
|
||||||
|
### Challenge Passage
|
||||||
|
**Path:** Security → Settings
|
||||||
|
|
||||||
|
- **Challenge Passage:** 30 minutes
|
||||||
|
|
||||||
|
## Network Settings
|
||||||
|
|
||||||
|
### HTTP/2 & HTTP/3
|
||||||
|
**Path:** Network
|
||||||
|
|
||||||
|
- **HTTP/2:** ON (default)
|
||||||
|
- **HTTP/3 (QUIC):** ON (faster connections)
|
||||||
|
- **0-RTT Connection Resumption:** ON
|
||||||
|
|
||||||
|
### WebSockets
|
||||||
|
**Path:** Network
|
||||||
|
|
||||||
|
- **WebSockets:** ON (required for real-time features)
|
||||||
|
|
||||||
|
### gRPC
|
||||||
|
**Path:** Network
|
||||||
|
|
||||||
|
- **gRPC:** OFF (not needed for standard Next.js)
|
||||||
|
|
||||||
|
## Analytics & Monitoring
|
||||||
|
|
||||||
|
### Web Analytics
|
||||||
|
**Path:** Analytics & Logs → Web Analytics
|
||||||
|
|
||||||
|
- Enable Cloudflare Web Analytics (privacy-friendly, no cookie banner needed)
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
- **Core Web Vitals:** Track LCP, FID, CLS
|
||||||
|
- **Page Load Time:** Monitor at edge
|
||||||
|
|
||||||
|
### Real User Monitoring (RUM)
|
||||||
|
Add Cloudflare Zaraz or custom analytics:
|
||||||
|
```html
|
||||||
|
<!-- Cloudflare Web Analytics -->
|
||||||
|
<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
|
||||||
|
data-cf-beacon='{"token": "YOUR_TOKEN"}'></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subdomain Configuration
|
||||||
|
|
||||||
|
Since you have multiple services, ensure proper routing:
|
||||||
|
|
||||||
|
### Traefik Labels (Dokploy Docker Compose)
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.mylder.rule=Host(`mylder.io`) || Host(`www.mylder.io`)"
|
||||||
|
- "traefik.http.routers.mylder.tls=true"
|
||||||
|
- "traefik.http.routers.mylder.tls.certresolver=letsencrypt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### WWW Redirect (Cloudflare)
|
||||||
|
**Path:** Rules → Redirect Rules
|
||||||
|
|
||||||
|
```
|
||||||
|
If: Hostname equals www.mylder.io
|
||||||
|
Then: Redirect to https://mylder.io$1 (301 permanent)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### DNS Propagation
|
||||||
|
```bash
|
||||||
|
dig mylder.io
|
||||||
|
nslookup mylder.io
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Certificate
|
||||||
|
```bash
|
||||||
|
curl -I https://mylder.io
|
||||||
|
# Check for: HTTP/2 200, cf-cache-status header
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Verification
|
||||||
|
```bash
|
||||||
|
curl -I https://mylder.io/_next/static/[hash]/[file].js
|
||||||
|
# Look for: cf-cache-status: HIT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
```bash
|
||||||
|
# Lighthouse
|
||||||
|
npx lighthouse https://mylder.io --view
|
||||||
|
|
||||||
|
# WebPageTest
|
||||||
|
# Visit https://www.webpagetest.org
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 521 Error (Web Server Down)
|
||||||
|
- Check Traefik is running: `docker ps | grep traefik`
|
||||||
|
- Check Dokploy deployment status
|
||||||
|
|
||||||
|
### 522 Error (Connection Timed Out)
|
||||||
|
- Verify firewall allows Cloudflare IPs
|
||||||
|
- Check VPS security groups
|
||||||
|
|
||||||
|
### 525 Error (SSL Handshake Failed)
|
||||||
|
- Ensure SSL/TLS mode is "Full (strict)"
|
||||||
|
- Verify Traefik has valid Let's Encrypt certificate
|
||||||
|
|
||||||
|
### Cache Not Working
|
||||||
|
- Check Cache-Control headers in response
|
||||||
|
- Verify Page Rules are active
|
||||||
|
- Clear Cloudflare cache and test
|
||||||
|
|
||||||
|
### Mixed Content Warnings
|
||||||
|
- Enable "Automatic HTTPS Rewrites"
|
||||||
|
- Update hardcoded http:// URLs to https://
|
||||||
|
|
||||||
|
## Cost Optimization
|
||||||
|
|
||||||
|
### Free Plan Limits
|
||||||
|
- 100k requests/day (~3M/month)
|
||||||
|
- Unlimited bandwidth
|
||||||
|
- Basic DDoS protection
|
||||||
|
- Shared SSL certificate
|
||||||
|
- 3 Page Rules
|
||||||
|
|
||||||
|
### When to Upgrade to Pro ($20/month)
|
||||||
|
- Need more than 3 Page Rules
|
||||||
|
- Want Image Optimization (Polish, Mirage)
|
||||||
|
- Need custom SSL certificate
|
||||||
|
- Require advanced WAF rules
|
||||||
|
- Want priority support
|
||||||
|
|
||||||
|
### Enterprise Features (for future)
|
||||||
|
- Cloudflare Workers (serverless at edge)
|
||||||
|
- Load Balancing
|
||||||
|
- Argo Smart Routing
|
||||||
|
- Custom WAF rules
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Immediate:** DNS + SSL/TLS setup
|
||||||
|
2. **Week 1:** Caching rules + performance testing
|
||||||
|
3. **Month 1:** Monitor analytics, tune cache rules
|
||||||
|
4. **Quarter 1:** Consider Workers for edge logic (see cloudflare-workers-roadmap.md)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Cloudflare DNS Docs](https://developers.cloudflare.com/dns/)
|
||||||
|
- [Next.js Cloudflare Deployment](https://nextjs.org/docs/app/building-your-application/deploying)
|
||||||
|
- [Cloudflare Cache API](https://developers.cloudflare.com/cache/)
|
||||||
591
docs/cloudflare-workers-roadmap.md
Normal file
591
docs/cloudflare-workers-roadmap.md
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
# Cloudflare Workers Roadmap for Mylder
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Edge computing strategy for Mylder platform using Cloudflare Workers, R2, D1, and KV.
|
||||||
|
|
||||||
|
**Philosophy:** Move compute closer to users, reduce VPS load, improve UX with sub-100ms responses.
|
||||||
|
|
||||||
|
## Current Architecture vs Edge-First
|
||||||
|
|
||||||
|
### Current (VPS-Centric)
|
||||||
|
```
|
||||||
|
User → Cloudflare CDN → VPS (149.102.155.84)
|
||||||
|
├── Next.js (Frontend)
|
||||||
|
├── Supabase (Auth, DB)
|
||||||
|
├── n8n (Automation)
|
||||||
|
└── Dokploy (Orchestration)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future (Edge-First)
|
||||||
|
```
|
||||||
|
User → Cloudflare Workers (Edge Logic)
|
||||||
|
├── Static Assets (CDN)
|
||||||
|
├── Edge Auth (Workers + KV)
|
||||||
|
├── Edge API (Workers + D1)
|
||||||
|
└── Origin (VPS for heavy compute)
|
||||||
|
├── Supabase (Primary DB)
|
||||||
|
└── n8n (Automation)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workers Use Cases
|
||||||
|
|
||||||
|
### 1. Edge Authentication (Priority: High)
|
||||||
|
|
||||||
|
**Problem:** Every auth check hits VPS → Supabase (100-300ms)
|
||||||
|
**Solution:** Cache JWT validation at edge
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/auth-edge.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const token = request.headers.get('Authorization')?.split('Bearer ')[1];
|
||||||
|
if (!token) return new Response('Unauthorized', { status: 401 });
|
||||||
|
|
||||||
|
// Check KV cache for user session
|
||||||
|
const cached = await env.SESSIONS.get(token);
|
||||||
|
if (cached) {
|
||||||
|
return new Response(cached, {
|
||||||
|
headers: { 'X-Auth-Source': 'edge-cache' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Supabase origin
|
||||||
|
const response = await fetch('https://supabase.mylder.io/auth/v1/user', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const user = await response.text();
|
||||||
|
await env.SESSIONS.put(token, user, { expirationTtl: 3600 }); // 1 hour
|
||||||
|
return new Response(user, {
|
||||||
|
headers: { 'X-Auth-Source': 'origin' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Auth checks: 300ms → 10ms (30x faster)
|
||||||
|
- Reduced Supabase load
|
||||||
|
- Better UX for logged-in users
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
- Session invalidation complexity
|
||||||
|
- KV storage costs ($0.50/1M reads)
|
||||||
|
|
||||||
|
### 2. API Rate Limiting (Priority: High)
|
||||||
|
|
||||||
|
**Problem:** DDoS/abuse protection on VPS is resource-intensive
|
||||||
|
**Solution:** Rate limit at edge before hitting origin
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/rate-limiter.ts
|
||||||
|
import { RateLimiter } from '@cloudflare/workers-rate-limiter';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const limiter = new RateLimiter({
|
||||||
|
namespace: env.RATE_LIMIT,
|
||||||
|
// 100 requests per 60 seconds per IP
|
||||||
|
limit: 100,
|
||||||
|
period: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
|
||||||
|
const { success } = await limiter.limit({ key: ip });
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return new Response('Rate limit exceeded', {
|
||||||
|
status: 429,
|
||||||
|
headers: { 'Retry-After': '60' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to origin
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Block abuse before it hits VPS
|
||||||
|
- Per-IP, per-user, per-endpoint limits
|
||||||
|
- 0ms latency overhead
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- `/api/*` endpoints
|
||||||
|
- Login attempts
|
||||||
|
- Form submissions
|
||||||
|
- Search queries
|
||||||
|
|
||||||
|
### 3. A/B Testing at Edge (Priority: Medium)
|
||||||
|
|
||||||
|
**Problem:** Client-side A/B testing causes layout shift (poor CLS)
|
||||||
|
**Solution:** Server-side rendering with edge variants
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/ab-test.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// Check existing variant
|
||||||
|
let variant = request.headers.get('Cookie')?.match(/variant=([AB])/)?.[1];
|
||||||
|
|
||||||
|
if (!variant) {
|
||||||
|
// Assign 50/50 split
|
||||||
|
variant = Math.random() < 0.5 ? 'A' : 'B';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite request to origin with variant
|
||||||
|
const originUrl = `${url.origin}${url.pathname}?variant=${variant}`;
|
||||||
|
const response = await fetch(originUrl, request);
|
||||||
|
|
||||||
|
// Set variant cookie
|
||||||
|
const headers = new Headers(response.headers);
|
||||||
|
headers.set('Set-Cookie', `variant=${variant}; Path=/; Max-Age=2592000`);
|
||||||
|
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- No layout shift (CLS = 0)
|
||||||
|
- Instant variant assignment
|
||||||
|
- Analytics at edge
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Landing page variants
|
||||||
|
- Pricing page tests
|
||||||
|
- Feature flags
|
||||||
|
|
||||||
|
### 4. Edge Personalization (Priority: Medium)
|
||||||
|
|
||||||
|
**Problem:** Generic content for all users (low engagement)
|
||||||
|
**Solution:** Geo/device-specific content at edge
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/personalize.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const country = request.cf?.country as string;
|
||||||
|
const device = request.headers.get('User-Agent')?.includes('Mobile')
|
||||||
|
? 'mobile'
|
||||||
|
: 'desktop';
|
||||||
|
|
||||||
|
// Fetch base HTML from origin
|
||||||
|
const response = await fetch(request);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// Inject personalized content
|
||||||
|
const personalized = html
|
||||||
|
.replace('{{COUNTRY}}', country)
|
||||||
|
.replace('{{DEVICE}}', device)
|
||||||
|
.replace('{{CURRENCY}}', getCurrency(country));
|
||||||
|
|
||||||
|
return new Response(personalized, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Cache-Control': 'public, max-age=60, s-maxage=3600',
|
||||||
|
'Vary': 'CF-IPCountry, User-Agent'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCurrency(country: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
US: 'USD', GB: 'GBP', EU: 'EUR', DK: 'DKK'
|
||||||
|
};
|
||||||
|
return map[country] || 'USD';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Geo-specific content (currency, language)
|
||||||
|
- Device-optimized markup
|
||||||
|
- Sub-50ms personalization
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Pricing display
|
||||||
|
- Content localization
|
||||||
|
- Feature availability
|
||||||
|
|
||||||
|
### 5. Image Optimization & Resizing (Priority: Low)
|
||||||
|
|
||||||
|
**Problem:** Next.js Image Optimization on VPS is CPU-intensive
|
||||||
|
**Solution:** Cloudflare Images or Workers + R2
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/image-optimize.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const imagePath = url.pathname.replace('/img/', '');
|
||||||
|
|
||||||
|
// Parse resize params
|
||||||
|
const width = url.searchParams.get('w') || '1920';
|
||||||
|
const quality = url.searchParams.get('q') || '80';
|
||||||
|
const format = url.searchParams.get('f') || 'webp';
|
||||||
|
|
||||||
|
// Fetch from R2
|
||||||
|
const object = await env.IMAGES.get(imagePath);
|
||||||
|
if (!object) return new Response('Not Found', { status: 404 });
|
||||||
|
|
||||||
|
// Resize at edge (requires paid Workers plan)
|
||||||
|
const resized = await fetch(`https://mylder.io/cdn-cgi/image/width=${width},quality=${quality},format=${format}/${imagePath}`);
|
||||||
|
|
||||||
|
return new Response(resized.body, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `image/${format}`,
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Offload VPS CPU
|
||||||
|
- Auto WebP/AVIF conversion
|
||||||
|
- Responsive images on-demand
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
- Requires Cloudflare Images ($5/month + $1/100k)
|
||||||
|
- Or R2 storage ($0.015/GB/month)
|
||||||
|
|
||||||
|
### 6. Edge API Caching (Priority: Medium)
|
||||||
|
|
||||||
|
**Problem:** Repeated API calls for same data
|
||||||
|
**Solution:** Smart caching with stale-while-revalidate
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/api-cache.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
|
const cache = caches.default;
|
||||||
|
const cacheKey = new Request(request.url, request);
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
let response = await cache.match(cacheKey);
|
||||||
|
if (response) {
|
||||||
|
const age = Date.now() - new Date(response.headers.get('Date')!).getTime();
|
||||||
|
|
||||||
|
// If stale (>60s), revalidate in background
|
||||||
|
if (age > 60000) {
|
||||||
|
ctx.waitUntil(revalidate(request, cache, cacheKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from origin
|
||||||
|
response = await fetch(request);
|
||||||
|
|
||||||
|
// Cache for 5 minutes
|
||||||
|
const cached = new Response(response.body, response);
|
||||||
|
cached.headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=3600');
|
||||||
|
ctx.waitUntil(cache.put(cacheKey, cached));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function revalidate(request: Request, cache: Cache, key: Request) {
|
||||||
|
const fresh = await fetch(request);
|
||||||
|
await cache.put(key, fresh.clone());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- API responses: 200ms → 10ms
|
||||||
|
- Stale content served instantly
|
||||||
|
- Background revalidation
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Public API endpoints
|
||||||
|
- User profiles
|
||||||
|
- Content feeds
|
||||||
|
|
||||||
|
## Cloudflare R2 (Object Storage)
|
||||||
|
|
||||||
|
### Use Cases for R2
|
||||||
|
|
||||||
|
**What:** S3-compatible object storage at edge (no egress fees)
|
||||||
|
|
||||||
|
#### 1. User Uploads
|
||||||
|
```typescript
|
||||||
|
// Store user avatars, documents
|
||||||
|
await env.UPLOADS.put(`users/${userId}/avatar.jpg`, file);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- $0.015/GB/month (10x cheaper than S3)
|
||||||
|
- No egress fees (free downloads)
|
||||||
|
- Global CDN distribution
|
||||||
|
|
||||||
|
**When to Use:**
|
||||||
|
- User-generated content (avatars, documents)
|
||||||
|
- Static assets (fonts, icons)
|
||||||
|
- Media files (videos, audio)
|
||||||
|
|
||||||
|
**When NOT to Use:**
|
||||||
|
- Frequent small writes (use D1 instead)
|
||||||
|
- Sub-10MB total storage (use KV instead)
|
||||||
|
|
||||||
|
#### 2. Static Asset Hosting
|
||||||
|
```typescript
|
||||||
|
// Move _next/static/* to R2
|
||||||
|
const asset = await env.STATIC_ASSETS.get(path);
|
||||||
|
return new Response(asset.body, {
|
||||||
|
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Reduce VPS storage needs
|
||||||
|
- Faster global distribution
|
||||||
|
- Immutable content = perfect cache
|
||||||
|
|
||||||
|
#### 3. Backup Storage
|
||||||
|
```typescript
|
||||||
|
// Daily Supabase backups to R2
|
||||||
|
await env.BACKUPS.put(`supabase-${date}.sql.gz`, backup);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloudflare D1 (Edge Database)
|
||||||
|
|
||||||
|
### Use Cases for D1
|
||||||
|
|
||||||
|
**What:** SQLite at edge (sub-10ms queries globally)
|
||||||
|
|
||||||
|
#### 1. Edge Metadata
|
||||||
|
```typescript
|
||||||
|
// Store user preferences, feature flags
|
||||||
|
const db = env.DB;
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT theme, locale FROM user_prefs WHERE user_id = ?'
|
||||||
|
).bind(userId).first();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Read latency: 100ms (Supabase) → 5ms (D1)
|
||||||
|
- No VPS load for reads
|
||||||
|
- SQL interface (familiar)
|
||||||
|
|
||||||
|
**When to Use:**
|
||||||
|
- Read-heavy data (user settings, configs)
|
||||||
|
- Small datasets (<1GB)
|
||||||
|
- Geo-distributed reads
|
||||||
|
|
||||||
|
**When NOT to Use:**
|
||||||
|
- Write-heavy workloads (D1 eventual consistency)
|
||||||
|
- Complex joins (use Supabase instead)
|
||||||
|
- Primary data storage (Supabase is source of truth)
|
||||||
|
|
||||||
|
#### 2. Analytics/Metrics
|
||||||
|
```typescript
|
||||||
|
// Store page views, click events
|
||||||
|
await db.prepare(
|
||||||
|
'INSERT INTO analytics (page, user_id, timestamp) VALUES (?, ?, ?)'
|
||||||
|
).bind(page, userId, Date.now()).run();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- High-volume writes
|
||||||
|
- Real-time aggregations
|
||||||
|
- No Supabase quota impact
|
||||||
|
|
||||||
|
#### 3. Feature Flags
|
||||||
|
```typescript
|
||||||
|
// Enable/disable features per user
|
||||||
|
const flags = await db.prepare(
|
||||||
|
'SELECT * FROM feature_flags WHERE active = 1'
|
||||||
|
).all();
|
||||||
|
```
|
||||||
|
|
||||||
|
### D1 vs Supabase Decision Matrix
|
||||||
|
|
||||||
|
| Feature | D1 (Edge) | Supabase (VPS) |
|
||||||
|
|---------|-----------|----------------|
|
||||||
|
| Read Latency | 5-10ms | 100-300ms |
|
||||||
|
| Write Latency | 50-100ms | 100-300ms |
|
||||||
|
| Consistency | Eventual | Strong |
|
||||||
|
| Storage Limit | 1GB | 8GB+ |
|
||||||
|
| Cost | Free (5M reads/day) | Included in VPS |
|
||||||
|
| Use Case | Cached metadata | Primary data |
|
||||||
|
|
||||||
|
**Strategy:** D1 = edge cache, Supabase = source of truth
|
||||||
|
|
||||||
|
## Cloudflare KV (Key-Value Store)
|
||||||
|
|
||||||
|
### Use Cases for KV
|
||||||
|
|
||||||
|
**What:** Global key-value store (sub-1ms reads)
|
||||||
|
|
||||||
|
#### 1. Session Storage
|
||||||
|
```typescript
|
||||||
|
await env.SESSIONS.put(sessionId, userData, { expirationTtl: 3600 });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Fastest storage option (<1ms reads)
|
||||||
|
- Auto-expiration (TTL)
|
||||||
|
- Global replication
|
||||||
|
|
||||||
|
**Cost:** $0.50/1M reads, $5/1M writes (generous free tier)
|
||||||
|
|
||||||
|
#### 2. Configuration/Secrets
|
||||||
|
```typescript
|
||||||
|
const apiKey = await env.CONFIG.get('stripe_public_key');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Rate Limit Counters
|
||||||
|
```typescript
|
||||||
|
const count = await env.RATE_LIMIT.get(ip);
|
||||||
|
await env.RATE_LIMIT.put(ip, count + 1, { expirationTtl: 60 });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Month 1)
|
||||||
|
**Goal:** Basic edge infrastructure
|
||||||
|
|
||||||
|
- [ ] Set up Cloudflare Workers project
|
||||||
|
- [ ] Deploy edge auth validator (Worker + KV)
|
||||||
|
- [ ] Implement rate limiting for `/api/*`
|
||||||
|
- [ ] Migrate static assets to R2
|
||||||
|
- [ ] Monitor performance impact
|
||||||
|
|
||||||
|
**Success Metrics:**
|
||||||
|
- Auth latency: <20ms (p95)
|
||||||
|
- Static asset cache hit rate: >95%
|
||||||
|
- VPS CPU load: -20%
|
||||||
|
|
||||||
|
### Phase 2: Optimization (Month 2-3)
|
||||||
|
**Goal:** Performance wins
|
||||||
|
|
||||||
|
- [ ] Deploy A/B testing worker
|
||||||
|
- [ ] Implement edge personalization
|
||||||
|
- [ ] Set up D1 for user preferences
|
||||||
|
- [ ] Create stale-while-revalidate API cache
|
||||||
|
- [ ] Image optimization via R2 + Workers
|
||||||
|
|
||||||
|
**Success Metrics:**
|
||||||
|
- LCP: <1.5s (p75)
|
||||||
|
- API latency: -50% for cached endpoints
|
||||||
|
- Global availability: 99.9%
|
||||||
|
|
||||||
|
### Phase 3: Advanced (Month 4+)
|
||||||
|
**Goal:** Edge-first architecture
|
||||||
|
|
||||||
|
- [ ] Move session management to edge (D1 + KV)
|
||||||
|
- [ ] Implement edge analytics (D1)
|
||||||
|
- [ ] Deploy feature flags system (D1)
|
||||||
|
- [ ] Create edge API gateway
|
||||||
|
- [ ] Multi-region failover
|
||||||
|
|
||||||
|
**Success Metrics:**
|
||||||
|
- Edge request ratio: >70%
|
||||||
|
- Origin load: -60%
|
||||||
|
- Global latency: <100ms (p95)
|
||||||
|
|
||||||
|
## Cost Analysis
|
||||||
|
|
||||||
|
### Free Tier Limits
|
||||||
|
- **Workers:** 100k requests/day
|
||||||
|
- **KV:** 100k reads/day, 1k writes/day
|
||||||
|
- **R2:** 10GB storage, 1M reads/month
|
||||||
|
- **D1:** 5M reads/day, 100k writes/day
|
||||||
|
|
||||||
|
### Paid Costs (Estimated for 1M users/month)
|
||||||
|
|
||||||
|
| Service | Usage | Cost/Month |
|
||||||
|
|---------|-------|------------|
|
||||||
|
| Workers | 100M requests | $5 (bundled) |
|
||||||
|
| KV | 50M reads | $25 |
|
||||||
|
| R2 | 100GB storage + 500M reads | $1.50 |
|
||||||
|
| D1 | Included in Workers | $0 |
|
||||||
|
| **Total** | | **~$30/month** |
|
||||||
|
|
||||||
|
**ROI:** Potential to downgrade VPS ($30/month savings) → break even
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
### 1. Over-Caching
|
||||||
|
**Bad:** Cache everything at edge for 24 hours
|
||||||
|
**Good:** Cache static assets long, dynamic content short
|
||||||
|
|
||||||
|
### 2. Edge as Primary DB
|
||||||
|
**Bad:** Store all user data in D1
|
||||||
|
**Good:** D1 = cache, Supabase = source of truth
|
||||||
|
|
||||||
|
### 3. Synchronous Edge-Origin Calls
|
||||||
|
**Bad:** Worker → fetch origin → wait → respond
|
||||||
|
**Good:** Serve stale, revalidate in background
|
||||||
|
|
||||||
|
### 4. Ignoring Cold Starts
|
||||||
|
**Bad:** Assume Workers are instant
|
||||||
|
**Good:** Optimize bundle size (<1MB), minimize dependencies
|
||||||
|
|
||||||
|
### 5. Complex Business Logic at Edge
|
||||||
|
**Bad:** Move entire Next.js app to Workers
|
||||||
|
**Good:** Keep heavy compute on VPS, edge for lightweight tasks
|
||||||
|
|
||||||
|
## Monitoring & Debugging
|
||||||
|
|
||||||
|
### Workers Analytics
|
||||||
|
**Dashboard:** Workers & Pages → Analytics
|
||||||
|
|
||||||
|
- Request volume
|
||||||
|
- Success rate (2xx/3xx/4xx/5xx)
|
||||||
|
- CPU time (p50, p95, p99)
|
||||||
|
- Error logs
|
||||||
|
|
||||||
|
### Tail Workers (Real-Time Logs)
|
||||||
|
```bash
|
||||||
|
wrangler tail mylder-auth-edge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Metrics
|
||||||
|
```typescript
|
||||||
|
// workers/metrics.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
|
const start = Date.now();
|
||||||
|
const response = await fetch(request);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
|
// Log to D1 analytics
|
||||||
|
ctx.waitUntil(
|
||||||
|
env.ANALYTICS.prepare(
|
||||||
|
'INSERT INTO metrics (endpoint, duration, status) VALUES (?, ?, ?)'
|
||||||
|
).bind(request.url, duration, response.status).run()
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Prototype:** Deploy simple edge auth worker (1 day)
|
||||||
|
2. **Measure:** Compare edge vs origin performance (1 week)
|
||||||
|
3. **Iterate:** Expand to rate limiting, A/B testing (1 month)
|
||||||
|
4. **Optimize:** Move 70%+ traffic to edge (3 months)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
|
||||||
|
- [R2 Storage](https://developers.cloudflare.com/r2/)
|
||||||
|
- [D1 Database](https://developers.cloudflare.com/d1/)
|
||||||
|
- [Workers Examples](https://developers.cloudflare.com/workers/examples/)
|
||||||
|
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)
|
||||||
753
docs/performance-checklist.md
Normal file
753
docs/performance-checklist.md
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
# Performance Checklist for Mylder Platform
|
||||||
|
|
||||||
|
## Core Web Vitals Targets
|
||||||
|
|
||||||
|
### Production Goals (75th Percentile)
|
||||||
|
|
||||||
|
| Metric | Target | Good | Needs Improvement | Poor |
|
||||||
|
|--------|--------|------|-------------------|------|
|
||||||
|
| **LCP** (Largest Contentful Paint) | <2.5s | <2.5s | 2.5s-4.0s | >4.0s |
|
||||||
|
| **FID** (First Input Delay) | <100ms | <100ms | 100ms-300ms | >300ms |
|
||||||
|
| **CLS** (Cumulative Layout Shift) | <0.1 | <0.1 | 0.1-0.25 | >0.25 |
|
||||||
|
| **INP** (Interaction to Next Paint) | <200ms | <200ms | 200ms-500ms | >500ms |
|
||||||
|
| **TTFB** (Time to First Byte) | <600ms | <800ms | 800ms-1800ms | >1800ms |
|
||||||
|
| **FCP** (First Contentful Paint) | <1.8s | <1.8s | 1.8s-3.0s | >3.0s |
|
||||||
|
|
||||||
|
### Current vs Target Performance
|
||||||
|
|
||||||
|
**Baseline (before optimization):**
|
||||||
|
```
|
||||||
|
LCP: ~3.5s → Target: <2.5s (30% improvement needed)
|
||||||
|
FID: ~150ms → Target: <100ms
|
||||||
|
CLS: ~0.15 → Target: <0.1
|
||||||
|
TTFB: ~800ms → Target: <600ms (edge caching)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Measurement Tools
|
||||||
|
|
||||||
|
#### 1. Real User Monitoring (RUM)
|
||||||
|
```typescript
|
||||||
|
// app/layout.tsx
|
||||||
|
import { SpeedInsights } from '@vercel/speed-insights/next';
|
||||||
|
import { Analytics } from '@vercel/analytics/react';
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<SpeedInsights />
|
||||||
|
<Analytics />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative:** Cloudflare Web Analytics (privacy-friendly, no cookies)
|
||||||
|
```html
|
||||||
|
<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
|
||||||
|
data-cf-beacon='{"token": "YOUR_TOKEN"}'></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Lab Testing
|
||||||
|
```bash
|
||||||
|
# Lighthouse (local)
|
||||||
|
npx lighthouse https://mylder.io --view
|
||||||
|
|
||||||
|
# WebPageTest (global)
|
||||||
|
# https://www.webpagetest.org
|
||||||
|
|
||||||
|
# Chrome DevTools
|
||||||
|
# DevTools → Performance → Record → Analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Continuous Monitoring
|
||||||
|
```bash
|
||||||
|
# Lighthouse CI (in CI/CD pipeline)
|
||||||
|
npm install -g @lhci/cli
|
||||||
|
lhci autorun --collect.url=https://mylder.io
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next.js Optimization Settings
|
||||||
|
|
||||||
|
### 1. Production Build Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// next.config.ts
|
||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
// Enable React strict mode for better error handling
|
||||||
|
reactStrictMode: true,
|
||||||
|
|
||||||
|
// Optimize bundle size
|
||||||
|
swcMinify: true,
|
||||||
|
|
||||||
|
// Compress output
|
||||||
|
compress: true,
|
||||||
|
|
||||||
|
// Optimize images
|
||||||
|
images: {
|
||||||
|
formats: ['image/avif', 'image/webp'],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
|
||||||
|
imageSizes: [16, 32, 48, 64, 96, 128, 256],
|
||||||
|
minimumCacheTTL: 2592000, // 30 days
|
||||||
|
dangerouslyAllowSVG: false,
|
||||||
|
contentDispositionType: 'attachment',
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'supabase.mylder.io',
|
||||||
|
pathname: '/storage/v1/object/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optimize fonts (automatic optimization)
|
||||||
|
optimizeFonts: true,
|
||||||
|
|
||||||
|
// Experimental features
|
||||||
|
experimental: {
|
||||||
|
// Optimize CSS
|
||||||
|
optimizeCss: true,
|
||||||
|
// Tree shaking for smaller bundles
|
||||||
|
optimizePackageImports: ['@radix-ui/react-icons', 'lucide-react'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom headers for caching
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'X-DNS-Prefetch-Control',
|
||||||
|
value: 'on',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Frame-Options',
|
||||||
|
value: 'SAMEORIGIN',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/_next/static/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=31536000, immutable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/_next/image/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=2592000, stale-while-revalidate=86400',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Bundle Analyzer
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
npm install -D @next/bundle-analyzer
|
||||||
|
|
||||||
|
# next.config.ts
|
||||||
|
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||||
|
|
||||||
|
const withBundleAnalyzer = bundleAnalyzer({
|
||||||
|
enabled: process.env.ANALYZE === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withBundleAnalyzer(nextConfig);
|
||||||
|
|
||||||
|
# Run analysis
|
||||||
|
ANALYZE=true npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target:** Total bundle size <500KB (gzipped)
|
||||||
|
|
||||||
|
### 3. Code Splitting Strategies
|
||||||
|
|
||||||
|
#### Dynamic Imports
|
||||||
|
```typescript
|
||||||
|
// app/dashboard/page.tsx
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
// Lazy load heavy components
|
||||||
|
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
|
||||||
|
loading: () => <ChartSkeleton />,
|
||||||
|
ssr: false, // Client-side only if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
return <HeavyChart data={data} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Route-Based Splitting
|
||||||
|
```typescript
|
||||||
|
// Automatic with App Router
|
||||||
|
app/
|
||||||
|
dashboard/ # Separate chunk
|
||||||
|
settings/ # Separate chunk
|
||||||
|
(marketing)/ # Separate chunk (route group)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vendor Splitting
|
||||||
|
```typescript
|
||||||
|
// next.config.ts
|
||||||
|
experimental: {
|
||||||
|
optimizePackageImports: [
|
||||||
|
'@radix-ui/react-icons',
|
||||||
|
'lucide-react',
|
||||||
|
'date-fns',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Server Components (Default)
|
||||||
|
```typescript
|
||||||
|
// app/page.tsx (Server Component by default)
|
||||||
|
export default async function Home() {
|
||||||
|
// Fetch at build time or request time
|
||||||
|
const data = await fetch('https://api.mylder.io/data', {
|
||||||
|
next: { revalidate: 3600 } // ISR: 1 hour cache
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>{/* No client-side JS for this component */}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- 0KB JavaScript sent to client
|
||||||
|
- Faster initial page load
|
||||||
|
- Better SEO
|
||||||
|
|
||||||
|
### 5. Client Components (Opt-In)
|
||||||
|
```typescript
|
||||||
|
// components/InteractiveButton.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function InteractiveButton() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule:** Only use 'use client' when you need:
|
||||||
|
- Interactivity (useState, useEffect)
|
||||||
|
- Browser APIs (localStorage, window)
|
||||||
|
- Event handlers (onClick, onSubmit)
|
||||||
|
|
||||||
|
## Image Optimization
|
||||||
|
|
||||||
|
### 1. Next.js Image Component
|
||||||
|
```typescript
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
// Optimized image with automatic sizing
|
||||||
|
<Image
|
||||||
|
src="/hero.jpg"
|
||||||
|
alt="Hero image"
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
priority // For LCP images
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="data:image/jpeg;base64,/9j/..." // Generated at build
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Responsive image
|
||||||
|
<Image
|
||||||
|
src="/hero.jpg"
|
||||||
|
alt="Hero image"
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Image Format Priority
|
||||||
|
1. **AVIF** (best compression, 50% smaller than JPEG)
|
||||||
|
2. **WebP** (wide support, 30% smaller than JPEG)
|
||||||
|
3. **JPEG/PNG** (fallback)
|
||||||
|
|
||||||
|
### 3. Cloudflare Image Optimization
|
||||||
|
```typescript
|
||||||
|
// next.config.ts
|
||||||
|
images: {
|
||||||
|
loader: 'custom',
|
||||||
|
loaderFile: './lib/cloudflare-image-loader.ts',
|
||||||
|
}
|
||||||
|
|
||||||
|
// lib/cloudflare-image-loader.ts
|
||||||
|
export default function cloudflareLoader({ src, width, quality }) {
|
||||||
|
const params = [`width=${width}`, `quality=${quality || 75}`, 'format=auto'];
|
||||||
|
return `https://mylder.io/cdn-cgi/image/${params.join(',')}/${src}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Lazy Loading Images
|
||||||
|
```typescript
|
||||||
|
// Non-critical images (below fold)
|
||||||
|
<Image
|
||||||
|
src="/feature.jpg"
|
||||||
|
alt="Feature"
|
||||||
|
width={800}
|
||||||
|
height={600}
|
||||||
|
loading="lazy" // Default, explicit here
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Critical images (above fold, LCP)
|
||||||
|
<Image
|
||||||
|
src="/hero.jpg"
|
||||||
|
alt="Hero"
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
priority // Preload immediately
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Responsive Images Checklist
|
||||||
|
- [ ] Use Next.js Image component (automatic optimization)
|
||||||
|
- [ ] Set explicit width/height (prevent CLS)
|
||||||
|
- [ ] Use `priority` for LCP images
|
||||||
|
- [ ] Use `loading="lazy"` for below-fold images
|
||||||
|
- [ ] Provide blur placeholder for better UX
|
||||||
|
- [ ] Compress images before upload (80-85% quality)
|
||||||
|
- [ ] Use modern formats (AVIF, WebP)
|
||||||
|
|
||||||
|
## Font Optimization
|
||||||
|
|
||||||
|
### 1. Next.js Font Optimization (Built-In)
|
||||||
|
```typescript
|
||||||
|
// app/layout.tsx
|
||||||
|
import { Inter, Roboto_Mono } from 'next/font/google';
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ['latin'],
|
||||||
|
display: 'swap', // Prevent invisible text flash
|
||||||
|
variable: '--font-inter',
|
||||||
|
preload: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const robotoMono = Roboto_Mono({
|
||||||
|
subsets: ['latin'],
|
||||||
|
display: 'swap',
|
||||||
|
variable: '--font-roboto-mono',
|
||||||
|
weight: ['400', '700'], // Only load needed weights
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html className={`${inter.variable} ${robotoMono.variable}`}>
|
||||||
|
<body className="font-sans">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Self-hosted fonts (no external requests)
|
||||||
|
- Automatic subset optimization
|
||||||
|
- Zero layout shift (font-display: swap)
|
||||||
|
|
||||||
|
### 2. Custom Fonts
|
||||||
|
```typescript
|
||||||
|
import localFont from 'next/font/local';
|
||||||
|
|
||||||
|
const myFont = localFont({
|
||||||
|
src: './fonts/MyFont.woff2',
|
||||||
|
display: 'swap',
|
||||||
|
variable: '--font-my-font',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Font Loading Best Practices
|
||||||
|
- [ ] Use only 2-3 font families max
|
||||||
|
- [ ] Load only needed weights (400, 700)
|
||||||
|
- [ ] Use `font-display: swap`
|
||||||
|
- [ ] Preload critical fonts
|
||||||
|
- [ ] Self-host fonts (no Google Fonts CDN)
|
||||||
|
|
||||||
|
## CSS Optimization
|
||||||
|
|
||||||
|
### 1. Tailwind CSS Production Config
|
||||||
|
```typescript
|
||||||
|
// tailwind.config.ts
|
||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
// Remove unused styles in production
|
||||||
|
future: {
|
||||||
|
hoverOnlyWhenSupported: true, // Better mobile UX
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Critical CSS
|
||||||
|
Next.js automatically inlines critical CSS for first paint.
|
||||||
|
|
||||||
|
### 3. CSS Module Best Practices
|
||||||
|
```typescript
|
||||||
|
// Use CSS Modules for component-specific styles
|
||||||
|
import styles from './Button.module.css';
|
||||||
|
|
||||||
|
export function Button() {
|
||||||
|
return <button className={styles.button}>Click me</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Optimization
|
||||||
|
|
||||||
|
### 1. Tree Shaking
|
||||||
|
```typescript
|
||||||
|
// Good: Import only what you need
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
// Bad: Import entire library
|
||||||
|
import * as dateFns from 'date-fns';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Remove Unused Dependencies
|
||||||
|
```bash
|
||||||
|
# Analyze bundle
|
||||||
|
npx depcheck
|
||||||
|
|
||||||
|
# Remove unused
|
||||||
|
npm uninstall unused-package
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Smaller Alternatives
|
||||||
|
|
||||||
|
| Heavy Package | Lightweight Alternative | Savings |
|
||||||
|
|---------------|------------------------|---------|
|
||||||
|
| Moment.js (232KB) | date-fns (13KB) | 95% |
|
||||||
|
| Lodash (70KB) | Lodash-es (24KB) | 66% |
|
||||||
|
| Axios (30KB) | Fetch API (native) | 100% |
|
||||||
|
| React Icons (full) | Lucide React (tree-shakeable) | 80% |
|
||||||
|
|
||||||
|
### 4. Debounce/Throttle Heavy Operations
|
||||||
|
```typescript
|
||||||
|
// components/Search.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { debounce } from 'lodash-es/debounce';
|
||||||
|
|
||||||
|
export function Search() {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const handleSearch = useCallback(
|
||||||
|
debounce(async (value: string) => {
|
||||||
|
const results = await fetch(`/api/search?q=${value}`);
|
||||||
|
// Handle results
|
||||||
|
}, 300),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <input onChange={(e) => handleSearch(e.target.value)} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching Strategy
|
||||||
|
|
||||||
|
### 1. Static Generation (Fastest)
|
||||||
|
```typescript
|
||||||
|
// app/blog/page.tsx
|
||||||
|
export const revalidate = false; // Static at build time
|
||||||
|
|
||||||
|
export default async function Blog() {
|
||||||
|
const posts = await fetchPosts();
|
||||||
|
return <PostList posts={posts} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Incremental Static Regeneration (ISR)
|
||||||
|
```typescript
|
||||||
|
// app/blog/[slug]/page.tsx
|
||||||
|
export const revalidate = 3600; // Revalidate every hour
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const posts = await fetchPosts();
|
||||||
|
return posts.map((post) => ({ slug: post.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Post({ params }) {
|
||||||
|
const post = await fetchPost(params.slug);
|
||||||
|
return <Article post={post} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Server-Side Rendering (SSR)
|
||||||
|
```typescript
|
||||||
|
// app/dashboard/page.tsx
|
||||||
|
export const dynamic = 'force-dynamic'; // Always server-render
|
||||||
|
|
||||||
|
export default async function Dashboard() {
|
||||||
|
const data = await fetchUserData(); // Per-request
|
||||||
|
return <DashboardContent data={data} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Client-Side Caching
|
||||||
|
```typescript
|
||||||
|
// Use SWR or React Query for client-side data fetching
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
export function Profile() {
|
||||||
|
const { data, error } = useSWR('/api/user', fetcher, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshInterval: 60000, // 1 minute
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) return <div>Error</div>;
|
||||||
|
if (!data) return <div>Loading...</div>;
|
||||||
|
return <div>{data.name}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. HTTP Cache Headers
|
||||||
|
```typescript
|
||||||
|
// app/api/data/route.ts
|
||||||
|
export async function GET() {
|
||||||
|
const data = await fetchData();
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Invalidation Strategy
|
||||||
|
|
||||||
|
### 1. On-Demand Revalidation
|
||||||
|
```typescript
|
||||||
|
// app/api/revalidate/route.ts
|
||||||
|
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const { path, tag } = await request.json();
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
revalidatePath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
revalidateTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ revalidated: true });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Webhook Triggers (n8n)
|
||||||
|
```typescript
|
||||||
|
// n8n workflow: CMS Update → POST /api/revalidate
|
||||||
|
{
|
||||||
|
"path": "/blog",
|
||||||
|
"tag": "posts"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cloudflare Cache Purge
|
||||||
|
```bash
|
||||||
|
# On deploy (CI/CD)
|
||||||
|
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
|
||||||
|
-H "Authorization: Bearer {api_token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data '{"purge_everything":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Selective Purge
|
||||||
|
```bash
|
||||||
|
# Purge specific URLs
|
||||||
|
--data '{"files":["https://mylder.io/blog","https://mylder.io/about"]}'
|
||||||
|
|
||||||
|
# Purge by tag
|
||||||
|
--data '{"tags":["blog-posts"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Testing Workflow
|
||||||
|
|
||||||
|
### 1. Pre-Deploy Checks
|
||||||
|
```bash
|
||||||
|
# Run before every deployment
|
||||||
|
npm run build # Must succeed
|
||||||
|
npm run lint # No errors
|
||||||
|
npm run typecheck # No type errors
|
||||||
|
|
||||||
|
# Optional but recommended
|
||||||
|
ANALYZE=true npm run build # Check bundle size
|
||||||
|
npx lighthouse https://staging.mylder.io --view
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Post-Deploy Verification
|
||||||
|
```bash
|
||||||
|
# Production smoke tests
|
||||||
|
curl -I https://mylder.io # Check TTFB
|
||||||
|
curl -I https://mylder.io/_next/static/[hash]/[file].js # Check caching
|
||||||
|
|
||||||
|
# Full performance audit
|
||||||
|
npx lighthouse https://mylder.io --view
|
||||||
|
|
||||||
|
# Real user monitoring
|
||||||
|
# Check Cloudflare Analytics or Vercel Analytics dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Continuous Monitoring
|
||||||
|
```bash
|
||||||
|
# Set up alerts in Cloudflare/monitoring tool
|
||||||
|
- LCP > 3s → Alert
|
||||||
|
- Error rate > 1% → Alert
|
||||||
|
- TTFB > 1s → Alert
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Wins Checklist
|
||||||
|
|
||||||
|
### Immediate (1 day)
|
||||||
|
- [ ] Enable Cloudflare CDN (see cloudflare-setup.md)
|
||||||
|
- [ ] Add proper cache headers to static assets
|
||||||
|
- [ ] Use Next.js Image component everywhere
|
||||||
|
- [ ] Enable `reactStrictMode` and `swcMinify`
|
||||||
|
- [ ] Remove unused dependencies
|
||||||
|
|
||||||
|
### Short-term (1 week)
|
||||||
|
- [ ] Implement ISR for blog/content pages
|
||||||
|
- [ ] Lazy load heavy components
|
||||||
|
- [ ] Optimize images (compress, convert to WebP/AVIF)
|
||||||
|
- [ ] Set up bundle analyzer
|
||||||
|
- [ ] Add loading states and skeletons (improve perceived performance)
|
||||||
|
|
||||||
|
### Medium-term (1 month)
|
||||||
|
- [ ] Implement edge caching for auth (see cloudflare-workers-roadmap.md)
|
||||||
|
- [ ] Set up real user monitoring (RUM)
|
||||||
|
- [ ] Optimize third-party scripts (defer/async)
|
||||||
|
- [ ] Implement stale-while-revalidate for APIs
|
||||||
|
- [ ] Add Lighthouse CI to deployment pipeline
|
||||||
|
|
||||||
|
### Long-term (3 months)
|
||||||
|
- [ ] Move 70% traffic to edge (Cloudflare Workers)
|
||||||
|
- [ ] Implement edge personalization
|
||||||
|
- [ ] Set up A/B testing at edge
|
||||||
|
- [ ] Achieve Core Web Vitals targets consistently
|
||||||
|
- [ ] Reduce VPS load by 60%+
|
||||||
|
|
||||||
|
## Common Performance Issues & Fixes
|
||||||
|
|
||||||
|
### Issue: High LCP (>4s)
|
||||||
|
**Causes:**
|
||||||
|
- Large hero image not optimized
|
||||||
|
- Server response time (TTFB) too high
|
||||||
|
- Render-blocking resources
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
- Use Next.js Image with `priority` for LCP image
|
||||||
|
- Enable Cloudflare CDN for faster TTFB
|
||||||
|
- Inline critical CSS
|
||||||
|
- Preload fonts
|
||||||
|
|
||||||
|
### Issue: High CLS (>0.25)
|
||||||
|
**Causes:**
|
||||||
|
- Images without width/height
|
||||||
|
- Web fonts loading late
|
||||||
|
- Dynamic content injections
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
- Set explicit dimensions on images
|
||||||
|
- Use `font-display: swap`
|
||||||
|
- Reserve space for dynamic content
|
||||||
|
- Avoid inserting content above existing content
|
||||||
|
|
||||||
|
### Issue: High FID/INP (>300ms)
|
||||||
|
**Causes:**
|
||||||
|
- Heavy JavaScript execution
|
||||||
|
- Long tasks blocking main thread
|
||||||
|
- Unoptimized event handlers
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
- Code split large bundles
|
||||||
|
- Debounce/throttle event handlers
|
||||||
|
- Use Web Workers for heavy computation
|
||||||
|
- Optimize third-party scripts
|
||||||
|
|
||||||
|
### Issue: Slow TTFB (>1s)
|
||||||
|
**Causes:**
|
||||||
|
- Slow server response
|
||||||
|
- No CDN
|
||||||
|
- Database queries during render
|
||||||
|
|
||||||
|
**Fixes:**
|
||||||
|
- Enable Cloudflare CDN
|
||||||
|
- Use ISR instead of SSR
|
||||||
|
- Move database queries to edge (D1)
|
||||||
|
- Implement caching layers
|
||||||
|
|
||||||
|
## Monitoring Dashboard Setup
|
||||||
|
|
||||||
|
### Cloudflare Web Analytics
|
||||||
|
```html
|
||||||
|
<!-- Add to app/layout.tsx <head> -->
|
||||||
|
<script defer src='https://static.cloudflareinsights.com/beacon.min.js'
|
||||||
|
data-cf-beacon='{"token": "YOUR_TOKEN"}'></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Performance Monitoring
|
||||||
|
```typescript
|
||||||
|
// app/layout.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
export function PerformanceMonitor() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Measure Web Vitals
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(console.log);
|
||||||
|
getFID(console.log);
|
||||||
|
getFCP(console.log);
|
||||||
|
getLCP(console.log);
|
||||||
|
getTTFB(console.log);
|
||||||
|
});
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Next.js Performance Docs](https://nextjs.org/docs/app/building-your-application/optimizing)
|
||||||
|
- [Web.dev Core Web Vitals](https://web.dev/vitals/)
|
||||||
|
- [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci)
|
||||||
|
- [Cloudflare Performance](https://developers.cloudflare.com/fundamentals/speed/)
|
||||||
5
src/app/health/route.ts
Normal file
5
src/app/health/route.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ status: 'ok', timestamp: Date.now() })
|
||||||
|
}
|
||||||
70
supabase/migrations/20241214_add_phase_metadata.sql
Normal file
70
supabase/migrations/20241214_add_phase_metadata.sql
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
-- Add phase_metadata column for agentic workflow tracking
|
||||||
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS phase_metadata JSONB DEFAULT '{
|
||||||
|
"currentPhase": "think",
|
||||||
|
"completedPhases": [],
|
||||||
|
"phaseStatus": "not_started",
|
||||||
|
"activities": [],
|
||||||
|
"metrics": {
|
||||||
|
"totalDuration": 0,
|
||||||
|
"phaseDurations": {},
|
||||||
|
"iterationCount": 0
|
||||||
|
}
|
||||||
|
}'::jsonb;
|
||||||
|
|
||||||
|
-- Create index for efficient querying
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_projects_phase_metadata ON projects USING gin(phase_metadata);
|
||||||
|
|
||||||
|
-- Add comment for documentation
|
||||||
|
COMMENT ON COLUMN projects.phase_metadata IS 'Stores agentic development loop state: currentPhase, completedPhases, phaseStatus, activities, metrics';
|
||||||
|
|
||||||
|
-- Create function to update phase
|
||||||
|
CREATE OR REPLACE FUNCTION update_project_phase(
|
||||||
|
p_project_id UUID,
|
||||||
|
p_new_phase TEXT,
|
||||||
|
p_status TEXT DEFAULT 'in_progress'
|
||||||
|
)
|
||||||
|
RETURNS JSONB AS $$
|
||||||
|
DECLARE
|
||||||
|
v_current_metadata JSONB;
|
||||||
|
v_current_phase TEXT;
|
||||||
|
v_completed_phases JSONB;
|
||||||
|
v_new_activity JSONB;
|
||||||
|
v_updated_metadata JSONB;
|
||||||
|
BEGIN
|
||||||
|
-- Get current metadata
|
||||||
|
SELECT phase_metadata INTO v_current_metadata
|
||||||
|
FROM projects WHERE id = p_project_id;
|
||||||
|
|
||||||
|
v_current_phase := v_current_metadata->>'currentPhase';
|
||||||
|
v_completed_phases := COALESCE(v_current_metadata->'completedPhases', '[]'::jsonb);
|
||||||
|
|
||||||
|
-- Add current phase to completed if transitioning
|
||||||
|
IF v_current_phase IS NOT NULL AND v_current_phase != p_new_phase THEN
|
||||||
|
v_completed_phases := v_completed_phases || to_jsonb(v_current_phase);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Create activity record
|
||||||
|
v_new_activity := jsonb_build_object(
|
||||||
|
'id', gen_random_uuid(),
|
||||||
|
'type', 'phase_change',
|
||||||
|
'message', 'Transitioned to ' || p_new_phase || ' phase',
|
||||||
|
'timestamp', now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Build updated metadata
|
||||||
|
v_updated_metadata := jsonb_build_object(
|
||||||
|
'currentPhase', p_new_phase,
|
||||||
|
'completedPhases', v_completed_phases,
|
||||||
|
'phaseStatus', p_status,
|
||||||
|
'activities', (v_new_activity || COALESCE(v_current_metadata->'activities', '[]'::jsonb))[0:49],
|
||||||
|
'metrics', COALESCE(v_current_metadata->'metrics', '{}'::jsonb)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update project
|
||||||
|
UPDATE projects
|
||||||
|
SET phase_metadata = v_updated_metadata, updated_at = now()
|
||||||
|
WHERE id = p_project_id;
|
||||||
|
|
||||||
|
RETURN v_updated_metadata;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
Reference in New Issue
Block a user