ในบทความนี้
- 🐕 ทำไม Frontend Performance สำคัญ?
- 🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR
- ⚛️ Modern Frameworks — React vs Vue vs Svelte
- ⚡ Core Web Vitals — Google's Performance Metrics
- 🖼️ Image Optimization — ปัญหา #1 ของ Performance
- 📦 Bundle Optimization — ส่ง JS น้อยลง
- 🚀 Caching Strategies
- 📱 Progressive Web App (PWA)
- 🎨 CSS & Font Optimization
- 📋 Performance Optimization Checklist
- 🔑 Key Takeaways
In this post
- 🐕 Why Does Frontend Performance Matter?
- 🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR
- ⚛️ Modern Frameworks — React vs Vue vs Svelte
- ⚡ Core Web Vitals — Google's Performance Metrics
- 🖼️ Image Optimization — Performance Problem #1
- 📦 Bundle Optimization — Ship Less JS
- 🚀 Caching Strategies
- 📱 Progressive Web App (PWA)
- 🎨 CSS & Font Optimization
- 📋 Performance Optimization Checklist
- 🔑 Key Takeaways
🐕 ทำไม Frontend Performance สำคัญ?
ในโลกที่ทุกคนใช้มือถือเข้าเว็บ ความเร็วของเว็บไซต์ไม่ใช่แค่ "nice to have" แต่เป็นสิ่งที่ส่งผลโดยตรงต่อรายได้ อัตราการ bounce และ SEO ranking ของคุณ Google ใช้ Core Web Vitals เป็นปัจจัยในการจัดอันดับ ซึ่งหมายความว่าเว็บที่ช้าจะถูกลดอันดับในผลการค้นหาโดยอัตโนมัติ
ลองดูตัวเลขจริงที่ Google และบริษัทชั้นนำรายงาน — จะเห็นว่าแม้แค่ 100ms ก็ส่งผลกระทบมหาศาล:
| Load Time | Bounce Rate | Revenue Impact |
|---|---|---|
| 0 – 1 sec | ~10% | ✅ Best experience |
| 1 – 3 sec | ~30% | ⚠️ Acceptable |
| 3 – 5 sec | ~50% | ❌ Losing customers |
| 5 – 10 sec | ~70% | 💀 Disaster |
| 10+ sec | ~90% | ☠️ Everyone left |
- 53% of mobile users leave if load > 3 seconds
- 1 sec slower = 7% drop in conversions
- Amazon: 100ms slower = 1% less revenue
- Google: 500ms slower = 20% less traffic
🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR
The Big Picture
Server → Empty HTML + JS bundle → Browser downloads JS → Executes → Renders
Server → Renders HTML → Sends full page → Browser shows → Downloads JS → Hydrates
Build time → Pre-render ALL pages → Deploy → Serve static HTML from CDN
Like SSG but pages regenerate in background — serve stale → rebuild → serve fresh
Comparison Table
เปรียบเทียบทั้ง 4 strategies แบบ side-by-side — ดูง่ายๆ ว่าแต่ละตัวเด่นตรงไหน และเหมาะกับ use case แบบไหน:
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| First Paint | 🐢 Slow | ⚡ Fast | ⚡ Fast | ⚡ Fast |
| SEO | ❌ | ✅ | ✅ | ✅ |
| Interactivity | ⚡ Fast | ⚠️ Wait | ⚠️ Wait | ⚠️ Wait |
| Server Load | None | High | None | Low |
| Freshness | ✅ Live | ✅ Live | ❌ Build | ⚡ Near |
| Hosting Cost | 💰 | 💰💰💰 | 💰 | 💰💰 |
| Build Time | Fast | N/A | 🐢 Slow | Fast |
CSR → Dashboards, admin panels, internal tools
SSR → E-commerce, social media, dynamic content
SSG → Blogs, docs, marketing sites, portfolios
ISR → E-commerce product pages, news sites
Next.js — All Strategies in One Framework
Next.js เป็น framework ที่รวมทุก rendering strategy ไว้ในที่เดียว — คุณสามารถใช้ SSG, ISR, SSR และ CSR ในโปรเจคเดียวกัน โดยเลือกแต่ละหน้าว่าจะใช้ strategy ไหน ดูตัวอย่างโค้ดจริง:
// ═══════════════════════════════
// SSG — Static at build time
// ═══════════════════════════════
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug); // Runs at BUILD time
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// ═══════════════════════════════
// ISR — Static + Revalidate
// ═══════════════════════════════
// app/products/[id]/page.tsx
export const revalidate = 60; // Regenerate every 60 seconds
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id); // Cached, revalidated
return (
<div>
<h1>{product.name}</h1>
<p>฿{product.price}</p>
</div>
);
}
// ═══════════════════════════════
// SSR — Fresh on every request
// ═══════════════════════════════
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // No caching
export default async function Dashboard() {
const stats = await fetchStats(); // Runs on EVERY request
return (
<div>
<h1>Dashboard</h1>
<p>Active users: {stats.activeUsers}</p>
</div>
);
}
// ═══════════════════════════════
// CSR — Client component
// ═══════════════════════════════
// app/chat/ChatWindow.tsx
'use client'; // ← This makes it CSR
import { useState, useEffect } from 'react';
export default function ChatWindow() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://api.myapp.com/chat');
ws.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
return () => ws.close();
}, []);
return <div>{messages.map(m => <p key={m.id}>{m.text}</p>)}</div>;
}
⚛️ Modern Frameworks — React vs Vue vs Svelte
Framework Comparison
| ⚛️ React | 💚 Vue | 🔥 Svelte | |
|---|---|---|---|
| Released | 2013 (Meta) | 2014 (Evan) | 2016 (Rich) |
| Approach | Library | Framework | Compiler |
| Rendering | Virtual DOM | Virtual DOM | No VDOM! |
| Bundle Size | ~40 KB | ~33 KB | ~2 KB |
| Learning | ⭐⭐ Medium | ⭐ Easy | ⭐ Easy |
| State Mgmt | Redux/Zustand | Pinia/Vuex | Built-in |
| Meta-framework | Next.js | Nuxt | SvelteKit |
| Ecosystem | ⭐⭐⭐ Huge | ⭐⭐ Large | ⭐ Growing |
| Jobs | ⭐⭐⭐ Most | ⭐⭐ Many | ⭐ Growing |
| Performance | ⭐⭐ Good | ⭐⭐ Good | ⭐⭐⭐ Best |
Need jobs/ecosystem → React (Next.js)
Beginner friendly → Vue (Nuxt)
Performance-first → Svelte (SvelteKit)
All three are great — pick one and master it!
Same Component in Each Framework
วิธีที่ดีที่สุดในการเปรียบเทียบ framework คือดู component เดียวกันเขียนในแต่ละตัว — Counter component ง่ายๆ ที่มีปุ่มเพิ่ม-ลด จะเห็นชัดว่า Svelte เขียนสั้นที่สุด ไม่ต้อง import อะไรเลย:
// ═══════════════════════════════
// React — Counter Component
// ═══════════════════════════════
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
}
<!-- ═══════════════════════════════ -->
<!-- Vue — Counter Component -->
<!-- ═══════════════════════════════ -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="count++">+1</button>
<button @click="count--">-1</button>
</div>
</template>
<!-- ═══════════════════════════════ -->
<!-- Svelte — Counter Component -->
<!-- ═══════════════════════════════ -->
<script>
let count = 0;
</script>
<div>
<p>Count: {count}</p>
<button on:click={() => count++}>+1</button>
<button on:click={() => count--}>-1</button>
</div>
<!-- ✨ ง่ายที่สุด! No imports, no hooks, no ref() -->
Meta-Frameworks — Full-Stack Capabilities
Meta-framework คือ framework ที่สร้างอยู่บน UI framework อีกที — ให้ความสามารถ full-stack เช่น routing, SSR, API routes, deployment และ optimization ต่างๆ แบบ out of the box ถ้าจะเริ่มโปรเจคจริงในปี 2026 ควรใช้ meta-framework มากกว่า UI framework เปล่าๆ:
- App Router (React Server Components)
- SSR / SSG / ISR / CSR — all in one
- API Routes (full-stack)
- Image & font optimization
- Middleware (edge functions)
- Deploy: Vercel, self-hosted, Docker
- Auto-imports (no import statements!)
- File-based routing
- SSR / SSG / SPA / Hybrid
- Server API routes (Nitro)
- 50+ official modules
- Deploy: Vercel, Netlify, Cloudflare
- Smallest bundle sizes
- File-based routing
- SSR / SSG / SPA
- Form actions (progressive enhancement)
- Built-in adapters (Node, CF, Vercel)
- Best DX (Developer Experience)
- Zero JS by default (Islands Architecture)
- Use React + Vue + Svelte together!
- Content collections (MDX, Markdown)
- Perfect for content-heavy sites
- Best performance for static sites
⚡ Core Web Vitals — Google's Performance Metrics
The 3 Metrics That Matter
Measuring Performance
ก่อนจะ optimize ต้องวัดก่อน! มีเครื่องมือหลายตัวที่ช่วยวัด Core Web Vitals ทั้งแบบ lab testing (Lighthouse, PageSpeed Insights) และ field data จาก user จริง (CrUX, web-vitals library) ควรใช้ทั้ง 2 แบบ เพราะ lab testing อาจไม่ตรงกับ real-world performance:
# ═══════════════════════════════
# Tools for Measuring
# ═══════════════════════════════
# 1. Google PageSpeed Insights (online)
# → https://pagespeed.web.dev/
# 2. Lighthouse (Chrome DevTools)
# → DevTools → Lighthouse tab → Analyze
# 3. Web Vitals JavaScript library
npm install web-vitals
# 4. Chrome DevTools → Performance tab
# → Record → Interact → Stop → Analyze flamechart
// ═══════════════════════════════
// Track Core Web Vitals in your app
// ═══════════════════════════════
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = {
name: metric.name, // "LCP", "INP", "CLS"
value: metric.value, // 2500, 150, 0.05
rating: metric.rating, // "good", "needs-improvement", "poor"
delta: metric.delta,
id: metric.id,
page: window.location.pathname,
};
// Send to your analytics
navigator.sendBeacon('/api/analytics/vitals', JSON.stringify(body));
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
🖼️ Image Optimization — ปัญหา #1 ของ Performance
Before vs After
<img src="hero.jpg">
- 5 MB JPEG
- 4000×3000 px (displayed at 800×600)
- No lazy loading
- No responsive sizes
- LCP: 8 seconds 💀
- 50 KB WebP (100× smaller!)
- Right size for each screen
- Lazy loaded (below fold)
- Width/height prevents CLS
- fetchpriority for LCP image
- LCP: 0.8 seconds ⚡
Image Formats Comparison
| Format | Size | Quality | Support | Use Case |
|---|---|---|---|---|
| JPEG | ⭐⭐ | ⭐⭐ | ✅ All | Photos (legacy) |
| PNG | ⭐ | ⭐⭐⭐ | ✅ All | Transparency |
| WebP | ⭐⭐⭐ | ⭐⭐⭐ | ✅ 97%+ | Photos (modern) |
| AVIF | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⚠️ 93% | Best ratio |
| SVG | Tiny | ∞ | ✅ All | Icons, logos |
Next.js Image Component
Everything loaded upfront — user visits /home → downloads entire app
Visit /home → loads layout + home only (150 KB)
📦 Bundle Optimization — ส่ง JS น้อยลง
Code Splitting & Lazy Loading
- Initial JS: < 100 KB (ideal: < 50 KB)
- Total JS: < 300 KB
- CSS: < 50 KB
- Images: < 200 KB per page
- Fonts: < 100 KB (1-2 fonts max)
- Total page weight: < 1 MB
- React + ReactDOM: ~42 KB
- Vue 3: ~33 KB
- Svelte: ~2 KB ✅
- Lodash (full): ~25 KB ❌ use lodash-es
- Moment.js: ~72 KB ❌ use dayjs (~2 KB)
- Axios: ~5 KB — or use native fetch
// ═══════════════════════════════
// React — Lazy Loading Components
// ═══════════════════════════════
import { lazy, Suspense } from 'react';
// ❌ Static import (included in main bundle)
// import Dashboard from './Dashboard';
// import AdminPanel from './AdminPanel';
// import ChartLibrary from './ChartLibrary';
// ✅ Dynamic import (separate chunks)
const Dashboard = lazy(() => import('./Dashboard'));
const AdminPanel = lazy(() => import('./AdminPanel'));
const ChartLibrary = lazy(() => import('./ChartLibrary'));
export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} /> {/* Always loaded */}
<Route
path="/dashboard"
element={
<Suspense fallback={<LoadingSpinner />}>
<Dashboard /> {/* Loaded on navigate */}
</Suspense>
}
/>
<Route
path="/admin"
element={
<Suspense fallback={<LoadingSpinner />}>
<AdminPanel /> {/* Loaded on navigate */}
</Suspense>
}
/>
</Routes>
);
}
// ═══════════════════════════════
// Next.js — Dynamic Import
// ═══════════════════════════════
import dynamic from 'next/dynamic';
// Heavy component loaded only when needed
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Don't render on server (client-only)
});
// Conditional loading
const AdminTools = dynamic(() => import('@/components/AdminTools'), {
loading: () => null,
});
export default function Dashboard({ user }) {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart data={chartData} />
{user.role === 'admin' && <AdminTools />}
</div>
);
}
ใน React ใช้ lazy() + Suspense สำหรับ component-level code splitting ส่วน Next.js ใช้ dynamic() import ที่รองรับ SSR control ด้วย ดูตัวอย่าง:
Tree Shaking — ลบ code ที่ไม่ใช้
Tree shaking คือการที่ bundler (Webpack, Rollup, Vite) วิเคราะห์ว่า code ส่วนไหนที่ import มาแต่ไม่ได้ใช้จริง แล้วลบออกจาก bundle อัตโนมัติ แต่มันจะทำงานได้ก็ต่อเมื่อคุณ import แบบ named import (ไม่ใช่ import ทั้ง library):
// ═══════════════════════════════
// Tree Shaking — Dead code elimination
// ═══════════════════════════════
// ❌ Import ทั้ง library
import _ from 'lodash'; // 71 KB ทั้งหมด!
_.debounce(fn, 300); // ใช้แค่ 1 function
// ✅ Import เฉพาะที่ใช้
import debounce from 'lodash/debounce'; // 1.5 KB!
// หรือ
import { debounce } from 'lodash-es'; // ESM tree-shakeable
// ❌ Import entire icon library
import { icons } from 'lucide-react'; // 500+ icons loaded!
// ✅ Import specific icons
import { Search, Menu, X } from 'lucide-react'; // Only 3 icons
// ═══════════════════════════════
// Check bundle size
// ═══════════════════════════════
// 1. bundlephobia.com — check BEFORE installing
// "lodash" = 71.5 KB (minified + gzipped: 25.2 KB)
// "lodash-es" = tree-shakeable!
// "date-fns" (2 KB per function) vs "moment" (72 KB total)
// 2. Webpack Bundle Analyzer
// npm install --save-dev webpack-bundle-analyzer
// → Visual treemap of your bundle
// 3. Next.js built-in
// next build → shows route sizes automatically
Bundle Size Budget
ทุกโปรเจคควรมี performance budget — กำหนดขนาดสูงสุดที่ยอมรับได้ และ fail CI/CD ถ้าเกิน ช่วยป้องกันไม่ให้ bundle size ค่อยๆ โตจนควบคุมไม่ได้ (performance regression):
Cache-Control: public, max-age=31536000, immutable→ Cache 1 year, use content hash: main.a1b2c3d4.js
Cache-Control: no-cache → Always check with serverCache-Control: private, max-age=60 → Cache 60s, user-specificProviders: CloudFlare, CloudFront, Fastly — Reduce latency from 200ms to 20ms
🚀 Caching Strategies
Browser Caching
Web App ที่ทำงานเหมือน Native App
Success stories: Twitter Lite (+65% pages/session) · Pinterest (+60% engagement) · Starbucks (2× DAU) · Uber (works on 2G!)
Next.js Caching
Next.js มีระบบ caching built-in ที่ทรงพลังมาก — fetch() ใน Server Components จะ cache โดย default คุณสามารถกำหนด revalidation time หรือ cache tag สำหรับ on-demand revalidation ผ่าน webhook:
// ═══════════════════════════════
// Next.js — Built-in Caching
// ═══════════════════════════════
// 1. fetch() caching (Server Components)
async function getProducts() {
// Cached by default (static)
const res = await fetch('https://api.example.com/products');
return res.json();
}
async function getUser(id: string) {
// Revalidate every 60 seconds
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 60 },
});
return res.json();
}
async function getNotifications() {
// No cache (always fresh)
const res = await fetch('https://api.example.com/notifications', {
cache: 'no-store',
});
return res.json();
}
// 2. Route segment caching
// app/products/page.tsx
export const revalidate = 3600; // Revalidate every hour
export const dynamic = 'force-static'; // Force SSG
// 3. On-demand revalidation (webhook)
// 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); // Revalidate specific page
if (tag) revalidateTag(tag); // Revalidate by tag
return Response.json({ revalidated: true });
}
📱 Progressive Web App (PWA)
PWA คือเทคโนโลยีที่ทำให้เว็บทำงานได้เหมือน native app — install ลง home screen ได้, ทำงาน offline ได้, ส่ง push notification ได้ และ sync data เมื่อกลับมาออนไลน์ ทั้งหมดนี้โดยไม่ต้องผ่าน App Store เลย บริษัทอย่าง Twitter, Pinterest, Starbucks ใช้ PWA แล้วเห็นผลลัพธ์ที่น่าทึ่ง:
- Use WebP/AVIF format
- Responsive sizes (srcset)
- Lazy load below-fold images
- Set width/height (prevent CLS)
- Priority hint for LCP image
- Use next/image or auto-optimization
- Code splitting (route-based)
- Lazy load heavy components
- Tree shake unused imports
- Avoid large libraries (lodash → lodash-es)
- Bundle budget < 100 KB initial
- Analyze with bundle-analyzer
- Inline critical CSS
- Async load non-critical CSS
- Remove unused CSS (PurgeCSS/Tailwind)
- Minimize CSS-in-JS
- Self-host fonts
- font-display: swap/optional
- Preload critical fonts
- Subset fonts
- Max 2 font families
- CDN for static assets
- HTTP/2 or HTTP/3
- Gzip/Brotli compression
- Cache headers (immutable)
- Preconnect & DNS prefetch
- SSR/SSG for content pages
- CSR for interactive features
- React Server Components
- Track Core Web Vitals
- Lighthouse CI
- Performance budget alerts
การสร้าง PWA ต้องมี 3 องค์ประกอบหลัก: HTTPS, Web App Manifest (metadata สำหรับ install) และ Service Worker (caching + offline) มาดู manifest.json ก่อน:
// manifest.json — PWA metadata
{
"name": "My Awesome App",
"short_name": "MyApp",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#0D1229",
"theme_color": "#2196F3",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
// ═══════════════════════════════
// Service Worker — Caching Strategy
// ═══════════════════════════════
// sw.js
const CACHE_NAME = 'myapp-v1';
const STATIC_ASSETS = [
'/',
'/offline.html',
'/css/app.css',
'/js/app.js',
'/icons/icon-192.png',
];
// Install — cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
});
// Fetch — Stale While Revalidate
self.addEventListener('fetch', event => {
// Skip non-GET requests
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then(cached => {
// Return cached + fetch fresh in background
const fetchPromise = fetch(event.request).then(response => {
const cache = caches.open(CACHE_NAME);
cache.then(c => c.put(event.request, response.clone()));
return response;
}).catch(() => {
// Offline — return offline page
if (event.request.destination === 'document') {
return caches.match('/offline.html');
}
});
return cached || fetchPromise;
})
);
});
// Activate — clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
);
});
🎨 CSS & Font Optimization
Critical CSS
<!-- ═══════════════════════════════ -->
<!-- Critical CSS — Inline above-fold styles -->
<!-- ═══════════════════════════════ -->
<head>
<!-- ✅ Critical CSS inlined (above-fold styles) -->
<style>
/* Only styles needed for first viewport */
body { margin: 0; font-family: system-ui; }
.hero { height: 100vh; display: flex; align-items: center; }
.nav { position: fixed; top: 0; width: 100%; }
</style>
<!-- ✅ Non-critical CSS loaded async -->
<link rel="preload" href="/css/app.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/app.css"></noscript>
</head>
Font Optimization
Custom fonts เป็นสาเหตุหลักของ CLS (layout shift) — เมื่อ font โหลดเสร็จ text จะ re-render ด้วยขนาดที่ต่างจาก fallback font ทำให้ layout กระโดด ใช้ font-display property เพื่อควบคุมพฤติกรรม:
/* ═══════════════════════════════ */
/* Font Loading — Prevent CLS */
/* ═══════════════════════════════ */
/* ❌ Bad: Flash of Unstyled Text (FOUT) / Flash of Invisible Text (FOIT) */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
/* No font-display → browser default (usually FOIT = invisible text!) */
}
/* ✅ Good: font-display: swap */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Show fallback → swap when loaded */
}
/* ✅ Better: font-display: optional */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional; /* Use if cached, skip if not */
}
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/inter-var.woff2"
as="font" type="font/woff2" crossorigin>
// ═══════════════════════════════
// Next.js — Automatic font optimization
// ═══════════════════════════════
import { Inter, Noto_Sans_Thai } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const notoThai = Noto_Sans_Thai({
subsets: ['thai'],
display: 'swap',
variable: '--font-thai',
});
// Next.js automatically:
// ├── Self-hosts fonts (no Google Fonts request!)
// ├── Preloads critical fonts
// ├── Generates optimal @font-face
// └── Prevents layout shift
ถ้าใช้ Next.js, next/font จะจัดการ font optimization ให้ทั้งหมด — self-host fonts, preload, optimal @font-face, prevent layout shift โดยอัตโนมัติ:
Tailwind CSS — Performance Tips
Tailwind CSS v3+ มี PurgeCSS built-in ซึ่งจะวิเคราะห์ว่าคุณใช้ class ไหนจริงๆ แล้วลบ class ที่ไม่ได้ใช้ออก ทำให้ CSS จาก 3 MB เหลือเพียง 3-10 KB:
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}', // Only scan used files
'./components/**/*.{js,ts,jsx,tsx}',
],
// Tailwind v3+ automatically removes unused CSS (PurgeCSS built-in)
// Result: 3-10 KB CSS instead of 3 MB!
};
📋 Performance Optimization Checklist
┌──────────────────────────────────────────────────────┐
│ Frontend Performance Checklist │
│ │
│ 🖼️ Images: │
│ ├── [x] Use WebP/AVIF format │
│ ├── [x] Responsive sizes (srcset) │
│ ├── [x] Lazy load below-fold images │
│ ├── [x] Set width/height (prevent CLS) │
│ ├── [x] Priority hint for LCP image │
│ └── [x] Use next/image or similar auto-optimization │
│ │
│ 📦 JavaScript: │
│ ├── [x] Code splitting (route-based) │
│ ├── [x] Lazy load heavy components │
│ ├── [x] Tree shake unused imports │
│ ├── [x] Avoid large libraries (lodash → lodash-es) │
│ ├── [x] Bundle budget < 100 KB initial │
│ └── [x] Analyze bundle with webpack-bundle-analyzer │
│ │
│ 🎨 CSS: │
│ ├── [x] Inline critical CSS │
│ ├── [x] Async load non-critical CSS │
│ ├── [x] Remove unused CSS (PurgeCSS/Tailwind) │
│ └── [x] Minimize CSS-in-JS (prefer Tailwind/CSS) │
│ │
│ 🔤 Fonts: │
│ ├── [x] Self-host fonts (no external requests) │
│ ├── [x] Use font-display: swap/optional │
│ ├── [x] Preload critical fonts │
│ ├── [x] Subset fonts (latin only if no Thai) │
│ └── [x] Max 2 font families │
│ │
│ 🌐 Network: │
│ ├── [x] CDN for static assets │
│ ├── [x] HTTP/2 or HTTP/3 │
│ ├── [x] Gzip/Brotli compression │
│ ├── [x] Cache headers (immutable for hashed assets) │
│ ├── [x] Preconnect to external origins │
│ └── [x] DNS prefetch for third-party domains │
│ │
│ ⚡ Rendering: │
│ ├── [x] SSR/SSG for content pages (SEO + LCP) │
│ ├── [x] CSR only for interactive-heavy features │
│ ├── [x] ISR for dynamic-but-cacheable content │
│ ├── [x] Streaming SSR for large pages │
│ └── [x] React Server Components (reduce JS sent) │
│ │
│ 📊 Monitoring: │
│ ├── [x] Track Core Web Vitals (web-vitals lib) │
│ ├── [x] Lighthouse CI in GitHub Actions │
│ ├── [x] Real User Monitoring (RUM) │
│ └── [x] Performance budget alerts │
│ │
└──────────────────────────────────────────────────────┘
🔑 Key Takeaways
สรุปสิ่งที่สำคัญที่สุดจากทั้งบทความ — 10 ข้อที่ทุก frontend developer ควรจำ:
- SSR สำหรับ SEO + LCP, CSR สำหรับ interactivity → ใช้ Next.js/Nuxt/SvelteKit ที่รวมทุกอย่าง
- Core Web Vitals = Google ranking factor → LCP < 2.5s, INP < 200ms, CLS < 0.1
- Images = ปัญหา #1 → WebP/AVIF, responsive sizes, lazy load, width/height attributes
- Code splitting = ลด initial load → Lazy import components, route-based splitting
- Tree shaking = ลบ dead code →
import { x } from 'lib'แทนimport lib - Bundle budget < 100 KB → วัดทุก library ก่อน install (bundlephobia.com)
- CDN + Cache = เร็วขึ้น 10x → Static assets cache 1 year, HTML no-cache
- Font optimization = ป้องกัน CLS → Self-host, font-display: swap, max 2 fonts
- PWA = native-like experience → Offline, installable, push notifications
- วัด ก่อน optimize → ใช้ Lighthouse, web-vitals, RUM — อย่าเดา!
*📚 DevOps Series ทั้งหมด:*
- *Git Branching* | 2. *CI/CD Pipeline* | 3. *Docker vs VMs*
- *API Lifecycle* | 5. *Kubernetes* | 6. *Networking*
- *Infrastructure as Code* | 8. *Monitoring & Observability*
- *Linux & Shell* | 10. *DevSecOps & Security*
- *Testing Strategies* | 12. *GitOps & ArgoCD*
- *Cloud Architecture* | 14. *SRE*
- *GitHub Actions & GitHub Flow* | 16. *Automated Testing with GitHub*
- *Database Design & SQL* | 18. *Authentication & Authorization*
- *Web Architecture & Design Patterns*
- *Frontend Performance & Modern Frameworks* ← 📍 อยู่ตรงนี้
🐕 Why Does Frontend Performance Matter?
In a world where everyone reaches the web from a phone, the speed of a site is not merely "nice to have" — it feeds straight into revenue, into your bounce rate and into your SEO ranking. Google uses Core Web Vitals as a ranking factor, which means a slow site is demoted in the search results automatically.
Look at the real numbers Google and the leading companies report — you will see that even 100ms carries an enormous consequence:
| Load Time | Bounce Rate | Revenue Impact |
|---|---|---|
| 0 – 1 sec | ~10% | ✅ Best experience |
| 1 – 3 sec | ~30% | ⚠️ Acceptable |
| 3 – 5 sec | ~50% | ❌ Losing customers |
| 5 – 10 sec | ~70% | 💀 Disaster |
| 10+ sec | ~90% | ☠️ Everyone left |
- 53% of mobile users leave if load > 3 seconds
- 1 sec slower = 7% drop in conversions
- Amazon: 100ms slower = 1% less revenue
- Google: 500ms slower = 20% less traffic
🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR
The Big Picture
Server → Empty HTML + JS bundle → Browser downloads JS → Executes → Renders
Server → Renders HTML → Sends full page → Browser shows → Downloads JS → Hydrates
Build time → Pre-render ALL pages → Deploy → Serve static HTML from CDN
Like SSG but pages regenerate in background — serve stale → rebuild → serve fresh
Comparison Table
All four strategies side by side — an easy read of where each one is strong, and which use case it suits:
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| First Paint | 🐢 Slow | ⚡ Fast | ⚡ Fast | ⚡ Fast |
| SEO | ❌ | ✅ | ✅ | ✅ |
| Interactivity | ⚡ Fast | ⚠️ Wait | ⚠️ Wait | ⚠️ Wait |
| Server Load | None | High | None | Low |
| Freshness | ✅ Live | ✅ Live | ❌ Build | ⚡ Near |
| Hosting Cost | 💰 | 💰💰💰 | 💰 | 💰💰 |
| Build Time | Fast | N/A | 🐢 Slow | Fast |
CSR → Dashboards, admin panels, internal tools
SSR → E-commerce, social media, dynamic content
SSG → Blogs, docs, marketing sites, portfolios
ISR → E-commerce product pages, news sites
Next.js — All Strategies in One Framework
Next.js is the framework that gathers every rendering strategy in one place — you can use SSG, ISR, SSR and CSR in the same project, choosing a strategy page by page. Here is what that looks like in real code:
// ═══════════════════════════════
// SSG — Static at build time
// ═══════════════════════════════
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug); // Runs at BUILD time
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// ═══════════════════════════════
// ISR — Static + Revalidate
// ═══════════════════════════════
// app/products/[id]/page.tsx
export const revalidate = 60; // Regenerate every 60 seconds
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id); // Cached, revalidated
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}
// ═══════════════════════════════
// SSR — Fresh on every request
// ═══════════════════════════════
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // No caching
export default async function Dashboard() {
const stats = await fetchStats(); // Runs on EVERY request
return (
<div>
<h1>Dashboard</h1>
<p>Active users: {stats.activeUsers}</p>
</div>
);
}
// ═══════════════════════════════
// CSR — Client component
// ═══════════════════════════════
// app/chat/ChatWindow.tsx
'use client'; // ← This makes it CSR
import { useState, useEffect } from 'react';
export default function ChatWindow() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://api.myapp.com/chat');
ws.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
return () => ws.close();
}, []);
return <div>{messages.map(m => <p key={m.id}>{m.text}</p>)}</div>;
}
⚛️ Modern Frameworks — React vs Vue vs Svelte
Framework Comparison
| ⚛️ React | 💚 Vue | 🔥 Svelte | |
|---|---|---|---|
| Released | 2013 (Meta) | 2014 (Evan) | 2016 (Rich) |
| Approach | Library | Framework | Compiler |
| Rendering | Virtual DOM | Virtual DOM | No VDOM! |
| Bundle Size | ~40 KB | ~33 KB | ~2 KB |
| Learning | ⭐⭐ Medium | ⭐ Easy | ⭐ Easy |
| State Mgmt | Redux/Zustand | Pinia/Vuex | Built-in |
| Meta-framework | Next.js | Nuxt | SvelteKit |
| Ecosystem | ⭐⭐⭐ Huge | ⭐⭐ Large | ⭐ Growing |
| Jobs | ⭐⭐⭐ Most | ⭐⭐ Many | ⭐ Growing |
| Performance | ⭐⭐ Good | ⭐⭐ Good | ⭐⭐⭐ Best |
Need jobs/ecosystem → React (Next.js)
Beginner friendly → Vue (Nuxt)
Performance-first → Svelte (SvelteKit)
All three are great — pick one and master it!
Same Component in Each Framework
The best way to compare frameworks is to read the same component written in each one — a plain Counter with an increment and a decrement button. Svelte is visibly the shortest of the three: it imports nothing at all:
// ═══════════════════════════════
// React — Counter Component
// ═══════════════════════════════
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
}
<!-- ═══════════════════════════════ -->
<!-- Vue — Counter Component -->
<!-- ═══════════════════════════════ -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="count++">+1</button>
<button @click="count--">-1</button>
</div>
</template>
<!-- ═══════════════════════════════ -->
<!-- Svelte — Counter Component -->
<!-- ═══════════════════════════════ -->
<script>
let count = 0;
</script>
<div>
<p>Count: {count}</p>
<button on:click={() => count++}>+1</button>
<button on:click={() => count--}>-1</button>
</div>
<!-- ✨ The simplest of the three! No imports, no hooks, no ref() -->
Meta-Frameworks — Full-Stack Capabilities
A meta-framework is a framework built on top of a UI framework — it hands you full-stack capability out of the box: routing, SSR, API routes, deployment and a range of optimizations. If you are starting a real project in 2026, reach for a meta-framework rather than a bare UI framework:
- App Router (React Server Components)
- SSR / SSG / ISR / CSR — all in one
- API Routes (full-stack)
- Image & font optimization
- Middleware (edge functions)
- Deploy: Vercel, self-hosted, Docker
- Auto-imports (no import statements!)
- File-based routing
- SSR / SSG / SPA / Hybrid
- Server API routes (Nitro)
- 50+ official modules
- Deploy: Vercel, Netlify, Cloudflare
- Smallest bundle sizes
- File-based routing
- SSR / SSG / SPA
- Form actions (progressive enhancement)
- Built-in adapters (Node, CF, Vercel)
- Best DX (Developer Experience)
- Zero JS by default (Islands Architecture)
- Use React + Vue + Svelte together!
- Content collections (MDX, Markdown)
- Perfect for content-heavy sites
- Best performance for static sites
⚡ Core Web Vitals — Google's Performance Metrics
The 3 Metrics That Matter
Measuring Performance
Measure before you optimize! Several tools report Core Web Vitals, both as lab testing (Lighthouse, PageSpeed Insights) and as field data from real users (CrUX, the web-vitals library). Use both, because lab testing does not always match real-world performance:
# ═══════════════════════════════
# Tools for Measuring
# ═══════════════════════════════
# 1. Google PageSpeed Insights (online)
# → https://pagespeed.web.dev/
# 2. Lighthouse (Chrome DevTools)
# → DevTools → Lighthouse tab → Analyze
# 3. Web Vitals JavaScript library
npm install web-vitals
# 4. Chrome DevTools → Performance tab
# → Record → Interact → Stop → Analyze flamechart
// ═══════════════════════════════
// Track Core Web Vitals in your app
// ═══════════════════════════════
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = {
name: metric.name, // "LCP", "INP", "CLS"
value: metric.value, // 2500, 150, 0.05
rating: metric.rating, // "good", "needs-improvement", "poor"
delta: metric.delta,
id: metric.id,
page: window.location.pathname,
};
// Send to your analytics
navigator.sendBeacon('/api/analytics/vitals', JSON.stringify(body));
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
🖼️ Image Optimization — Performance Problem #1
Before vs After
<img src="hero.jpg">
- 5 MB JPEG
- 4000×3000 px (displayed at 800×600)
- No lazy loading
- No responsive sizes
- LCP: 8 seconds 💀
- 50 KB WebP (100× smaller!)
- Right size for each screen
- Lazy loaded (below fold)
- Width/height prevents CLS
- fetchpriority for LCP image
- LCP: 0.8 seconds ⚡
Image Formats Comparison
| Format | Size | Quality | Support | Use Case |
|---|---|---|---|---|
| JPEG | ⭐⭐ | ⭐⭐ | ✅ All | Photos (legacy) |
| PNG | ⭐ | ⭐⭐⭐ | ✅ All | Transparency |
| WebP | ⭐⭐⭐ | ⭐⭐⭐ | ✅ 97%+ | Photos (modern) |
| AVIF | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⚠️ 93% | Best ratio |
| SVG | Tiny | ∞ | ✅ All | Icons, logos |
Next.js Image Component
Everything loaded upfront — user visits /home → downloads entire app
Visit /home → loads layout + home only (150 KB)
📦 Bundle Optimization — Ship Less JS
Code Splitting & Lazy Loading
- Initial JS: < 100 KB (ideal: < 50 KB)
- Total JS: < 300 KB
- CSS: < 50 KB
- Images: < 200 KB per page
- Fonts: < 100 KB (1-2 fonts max)
- Total page weight: < 1 MB
- React + ReactDOM: ~42 KB
- Vue 3: ~33 KB
- Svelte: ~2 KB ✅
- Lodash (full): ~25 KB ❌ use lodash-es
- Moment.js: ~72 KB ❌ use dayjs (~2 KB)
- Axios: ~5 KB — or use native fetch
// ═══════════════════════════════
// React — Lazy Loading Components
// ═══════════════════════════════
import { lazy, Suspense } from 'react';
// ❌ Static import (included in main bundle)
// import Dashboard from './Dashboard';
// import AdminPanel from './AdminPanel';
// import ChartLibrary from './ChartLibrary';
// ✅ Dynamic import (separate chunks)
const Dashboard = lazy(() => import('./Dashboard'));
const AdminPanel = lazy(() => import('./AdminPanel'));
const ChartLibrary = lazy(() => import('./ChartLibrary'));
export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} /> {/* Always loaded */}
<Route
path="/dashboard"
element={
<Suspense fallback={<LoadingSpinner />}>
<Dashboard /> {/* Loaded on navigate */}
</Suspense>
}
/>
<Route
path="/admin"
element={
<Suspense fallback={<LoadingSpinner />}>
<AdminPanel /> {/* Loaded on navigate */}
</Suspense>
}
/>
</Routes>
);
}
// ═══════════════════════════════
// Next.js — Dynamic Import
// ═══════════════════════════════
import dynamic from 'next/dynamic';
// Heavy component loaded only when needed
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Don't render on server (client-only)
});
// Conditional loading
const AdminTools = dynamic(() => import('@/components/AdminTools'), {
loading: () => null,
});
export default function Dashboard({ user }) {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart data={chartData} />
{user.role === 'admin' && <AdminTools />}
</div>
);
}
In React, use lazy() + Suspense for component-level code splitting; in Next.js, use the dynamic() import, which supports SSR control as well. See the example above:
Tree Shaking — Remove the Code You Never Use
Tree shaking is the bundler (Webpack, Rollup, Vite) working out which of the code you imported is never actually used, and dropping it from the bundle automatically. It only works when you use named imports — not when you import the whole library:
// ═══════════════════════════════
// Tree Shaking — Dead code elimination
// ═══════════════════════════════
// ❌ Import the whole library
import _ from 'lodash'; // all 71 KB of it!
_.debounce(fn, 300); // for just 1 function
// ✅ Import only what you use
import debounce from 'lodash/debounce'; // 1.5 KB!
// or
import { debounce } from 'lodash-es'; // ESM tree-shakeable
// ❌ Import entire icon library
import { icons } from 'lucide-react'; // 500+ icons loaded!
// ✅ Import specific icons
import { Search, Menu, X } from 'lucide-react'; // Only 3 icons
// ═══════════════════════════════
// Check bundle size
// ═══════════════════════════════
// 1. bundlephobia.com — check BEFORE installing
// "lodash" = 71.5 KB (minified + gzipped: 25.2 KB)
// "lodash-es" = tree-shakeable!
// "date-fns" (2 KB per function) vs "moment" (72 KB total)
// 2. Webpack Bundle Analyzer
// npm install --save-dev webpack-bundle-analyzer
// → Visual treemap of your bundle
// 3. Next.js built-in
// next build → shows route sizes automatically
Bundle Size Budget
Every project should carry a performance budget — the largest size you are willing to accept, and a CI/CD failure the moment you exceed it. It is what stops bundle size from creeping upward until it is beyond control (performance regression):
Cache-Control: public, max-age=31536000, immutable→ Cache 1 year, use content hash: main.a1b2c3d4.js
Cache-Control: no-cache → Always check with serverCache-Control: private, max-age=60 → Cache 60s, user-specificProviders: CloudFlare, CloudFront, Fastly — Reduce latency from 200ms to 20ms
🚀 Caching Strategies
Browser Caching
A web app that behaves like a native app
Success stories: Twitter Lite (+65% pages/session) · Pinterest (+60% engagement) · Starbucks (2× DAU) · Uber (works on 2G!)
Next.js Caching
Next.js ships a genuinely powerful built-in caching system — fetch() inside Server Components is cached by default. You can set a revalidation time, or a cache tag for on-demand revalidation through a webhook:
// ═══════════════════════════════
// Next.js — Built-in Caching
// ═══════════════════════════════
// 1. fetch() caching (Server Components)
async function getProducts() {
// Cached by default (static)
const res = await fetch('https://api.example.com/products');
return res.json();
}
async function getUser(id: string) {
// Revalidate every 60 seconds
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 60 },
});
return res.json();
}
async function getNotifications() {
// No cache (always fresh)
const res = await fetch('https://api.example.com/notifications', {
cache: 'no-store',
});
return res.json();
}
// 2. Route segment caching
// app/products/page.tsx
export const revalidate = 3600; // Revalidate every hour
export const dynamic = 'force-static'; // Force SSG
// 3. On-demand revalidation (webhook)
// 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); // Revalidate specific page
if (tag) revalidateTag(tag); // Revalidate by tag
return Response.json({ revalidated: true });
}
📱 Progressive Web App (PWA)
A PWA is the set of technologies that lets the web behave like a native app — installable to the home screen, working offline, able to send push notifications, syncing data when the connection returns, and all of it without going anywhere near an App Store. Companies like Twitter, Pinterest and Starbucks adopted PWAs and saw remarkable results:
- Use WebP/AVIF format
- Responsive sizes (srcset)
- Lazy load below-fold images
- Set width/height (prevent CLS)
- Priority hint for LCP image
- Use next/image or auto-optimization
- Code splitting (route-based)
- Lazy load heavy components
- Tree shake unused imports
- Avoid large libraries (lodash → lodash-es)
- Bundle budget < 100 KB initial
- Analyze with bundle-analyzer
- Inline critical CSS
- Async load non-critical CSS
- Remove unused CSS (PurgeCSS/Tailwind)
- Minimize CSS-in-JS
- Self-host fonts
- font-display: swap/optional
- Preload critical fonts
- Subset fonts
- Max 2 font families
- CDN for static assets
- HTTP/2 or HTTP/3
- Gzip/Brotli compression
- Cache headers (immutable)
- Preconnect & DNS prefetch
- SSR/SSG for content pages
- CSR for interactive features
- React Server Components
- Track Core Web Vitals
- Lighthouse CI
- Performance budget alerts
Building a PWA takes three main pieces: HTTPS, a Web App Manifest (the metadata that makes it installable) and a Service Worker (caching + offline). Start with manifest.json:
// manifest.json — PWA metadata
{
"name": "My Awesome App",
"short_name": "MyApp",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#0D1229",
"theme_color": "#2196F3",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
// ═══════════════════════════════
// Service Worker — Caching Strategy
// ═══════════════════════════════
// sw.js
const CACHE_NAME = 'myapp-v1';
const STATIC_ASSETS = [
'/',
'/offline.html',
'/css/app.css',
'/js/app.js',
'/icons/icon-192.png',
];
// Install — cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
});
// Fetch — Stale While Revalidate
self.addEventListener('fetch', event => {
// Skip non-GET requests
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then(cached => {
// Return cached + fetch fresh in background
const fetchPromise = fetch(event.request).then(response => {
const cache = caches.open(CACHE_NAME);
cache.then(c => c.put(event.request, response.clone()));
return response;
}).catch(() => {
// Offline — return offline page
if (event.request.destination === 'document') {
return caches.match('/offline.html');
}
});
return cached || fetchPromise;
})
);
});
// Activate — clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
);
});
🎨 CSS & Font Optimization
Critical CSS
<!-- ═══════════════════════════════ -->
<!-- Critical CSS — Inline above-fold styles -->
<!-- ═══════════════════════════════ -->
<head>
<!-- ✅ Critical CSS inlined (above-fold styles) -->
<style>
/* Only styles needed for first viewport */
body { margin: 0; font-family: system-ui; }
.hero { height: 100vh; display: flex; align-items: center; }
.nav { position: fixed; top: 0; width: 100%; }
</style>
<!-- ✅ Non-critical CSS loaded async -->
<link rel="preload" href="/css/app.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/app.css"></noscript>
</head>
Font Optimization
Custom fonts are the main cause of CLS (layout shift) — when the font finishes loading, the text re-renders at a size different from the fallback font and the layout jumps. Use the font-display property to control that behavior:
/* ═══════════════════════════════ */
/* Font Loading — Prevent CLS */
/* ═══════════════════════════════ */
/* ❌ Bad: Flash of Unstyled Text (FOUT) / Flash of Invisible Text (FOIT) */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
/* No font-display → browser default (usually FOIT = invisible text!) */
}
/* ✅ Good: font-display: swap */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Show fallback → swap when loaded */
}
/* ✅ Better: font-display: optional */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional; /* Use if cached, skip if not */
}
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/inter-var.woff2"
as="font" type="font/woff2" crossorigin>
// ═══════════════════════════════
// Next.js — Automatic font optimization
// ═══════════════════════════════
import { Inter, Noto_Sans_Thai } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const notoThai = Noto_Sans_Thai({
subsets: ['thai'],
display: 'swap',
variable: '--font-thai',
});
// Next.js automatically:
// ├── Self-hosts fonts (no Google Fonts request!)
// ├── Preloads critical fonts
// ├── Generates optimal @font-face
// └── Prevents layout shift
If you are on Next.js, next/font handles the whole of font optimization for you — self-hosted fonts, preloading, an optimal @font-face and no layout shift, all automatically:
Tailwind CSS — Performance Tips
Tailwind CSS v3+ ships PurgeCSS built in: it works out which classes you actually use and strips the ones you do not, taking the CSS from 3 MB down to just 3-10 KB:
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}', // Only scan used files
'./components/**/*.{js,ts,jsx,tsx}',
],
// Tailwind v3+ automatically removes unused CSS (PurgeCSS built-in)
// Result: 3-10 KB CSS instead of 3 MB!
};
📋 Performance Optimization Checklist
┌──────────────────────────────────────────────────────┐
│ Frontend Performance Checklist │
│ │
│ 🖼️ Images: │
│ ├── [x] Use WebP/AVIF format │
│ ├── [x] Responsive sizes (srcset) │
│ ├── [x] Lazy load below-fold images │
│ ├── [x] Set width/height (prevent CLS) │
│ ├── [x] Priority hint for LCP image │
│ └── [x] Use next/image or similar auto-optimization │
│ │
│ 📦 JavaScript: │
│ ├── [x] Code splitting (route-based) │
│ ├── [x] Lazy load heavy components │
│ ├── [x] Tree shake unused imports │
│ ├── [x] Avoid large libraries (lodash → lodash-es) │
│ ├── [x] Bundle budget < 100 KB initial │
│ └── [x] Analyze bundle with webpack-bundle-analyzer │
│ │
│ 🎨 CSS: │
│ ├── [x] Inline critical CSS │
│ ├── [x] Async load non-critical CSS │
│ ├── [x] Remove unused CSS (PurgeCSS/Tailwind) │
│ └── [x] Minimize CSS-in-JS (prefer Tailwind/CSS) │
│ │
│ 🔤 Fonts: │
│ ├── [x] Self-host fonts (no external requests) │
│ ├── [x] Use font-display: swap/optional │
│ ├── [x] Preload critical fonts │
│ ├── [x] Subset fonts (latin only if no Thai) │
│ └── [x] Max 2 font families │
│ │
│ 🌐 Network: │
│ ├── [x] CDN for static assets │
│ ├── [x] HTTP/2 or HTTP/3 │
│ ├── [x] Gzip/Brotli compression │
│ ├── [x] Cache headers (immutable for hashed assets) │
│ ├── [x] Preconnect to external origins │
│ └── [x] DNS prefetch for third-party domains │
│ │
│ ⚡ Rendering: │
│ ├── [x] SSR/SSG for content pages (SEO + LCP) │
│ ├── [x] CSR only for interactive-heavy features │
│ ├── [x] ISR for dynamic-but-cacheable content │
│ ├── [x] Streaming SSR for large pages │
│ └── [x] React Server Components (reduce JS sent) │
│ │
│ 📊 Monitoring: │
│ ├── [x] Track Core Web Vitals (web-vitals lib) │
│ ├── [x] Lighthouse CI in GitHub Actions │
│ ├── [x] Real User Monitoring (RUM) │
│ └── [x] Performance budget alerts │
│ │
└──────────────────────────────────────────────────────┘
🔑 Key Takeaways
The most important things from the whole post — 10 points every frontend developer should keep:
- SSR for SEO + LCP, CSR for interactivity → use Next.js/Nuxt/SvelteKit, which give you all of it
- Core Web Vitals = Google ranking factor → LCP < 2.5s, INP < 200ms, CLS < 0.1
- Images = problem #1 → WebP/AVIF, responsive sizes, lazy load, width/height attributes
- Code splitting = a smaller initial load → Lazy import components, route-based splitting
- Tree shaking = dead code removed →
import { x } from 'lib'instead ofimport lib - Bundle budget < 100 KB → measure every library before you install it (bundlephobia.com)
- CDN + Cache = 10x faster → Static assets cache 1 year, HTML no-cache
- Font optimization = CLS prevented → Self-host, font-display: swap, max 2 fonts
- PWA = native-like experience → Offline, installable, push notifications
- Measure before you optimize → use Lighthouse, web-vitals, RUM — never guess!
*📚 The complete DevOps series:*
- *Git Branching* | 2. *CI/CD Pipeline* | 3. *Docker vs VMs*
- *API Lifecycle* | 5. *Kubernetes* | 6. *Networking*
- *Infrastructure as Code* | 8. *Monitoring & Observability*
- *Linux & Shell* | 10. *DevSecOps & Security*
- *Testing Strategies* | 12. *GitOps & ArgoCD*
- *Cloud Architecture* | 14. *SRE*
- *GitHub Actions & GitHub Flow* | 16. *Automated Testing with GitHub*
- *Database Design & SQL* | 18. *Authentication & Authorization*
- *Web Architecture & Design Patterns*
- *Frontend Performance & Modern Frameworks* ← 📍 you are here