Frontend Performance Frameworks

Frontend Performance & Modern Frameworks — เพิ่มความเร็วและเลือก Framework ที่ใช่ ⚡Frontend Performance & Modern Frameworks — Get Faster and Pick the Framework That Fits ⚡

DevOps & Vibe Coding 2026 ~25 min read
Frontend Performance — เพิ่มความเร็วและเลือก Framework ที่ใช่
ในบทความนี้
  1. 🐕 ทำไม Frontend Performance สำคัญ?
  2. 🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR
  3. ⚛️ Modern Frameworks — React vs Vue vs Svelte
  4. ⚡ Core Web Vitals — Google's Performance Metrics
  5. 🖼️ Image Optimization — ปัญหา #1 ของ Performance
  6. 📦 Bundle Optimization — ส่ง JS น้อยลง
  7. 🚀 Caching Strategies
  8. 📱 Progressive Web App (PWA)
  9. 🎨 CSS & Font Optimization
  10. 📋 Performance Optimization Checklist
  11. 🔑 Key Takeaways
In this post
  1. 🐕 Why Does Frontend Performance Matter?
  2. 🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR
  3. ⚛️ Modern Frameworks — React vs Vue vs Svelte
  4. ⚡ Core Web Vitals — Google's Performance Metrics
  5. 🖼️ Image Optimization — Performance Problem #1
  6. 📦 Bundle Optimization — Ship Less JS
  7. 🚀 Caching Strategies
  8. 📱 Progressive Web App (PWA)
  9. 🎨 CSS & Font Optimization
  10. 📋 Performance Optimization Checklist
  11. 🔑 Key Takeaways

🐕 ทำไม Frontend Performance สำคัญ?

ในโลกที่ทุกคนใช้มือถือเข้าเว็บ ความเร็วของเว็บไซต์ไม่ใช่แค่ "nice to have" แต่เป็นสิ่งที่ส่งผลโดยตรงต่อรายได้ อัตราการ bounce และ SEO ranking ของคุณ Google ใช้ Core Web Vitals เป็นปัจจัยในการจัดอันดับ ซึ่งหมายความว่าเว็บที่ช้าจะถูกลดอันดับในผลการค้นหาโดยอัตโนมัติ

ลองดูตัวเลขจริงที่ Google และบริษัทชั้นนำรายงาน — จะเห็นว่าแม้แค่ 100ms ก็ส่งผลกระทบมหาศาล:

⏱️ Every Second Counts
Load TimeBounce RateRevenue 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
📊 Real stats (Google):
  • 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
⚡ Performance IS a feature — not an afterthought

🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR

The Big Picture

Rendering Strategies
CSR (Client-Side Rendering)

Server → Empty HTML + JS bundle → Browser downloads JS → Executes → Renders

blank
JS download
✅ visible
❌ Slow first paint ❌ Bad SEO ✅ Rich interactivity ✅ Simple hosting
SSR (Server-Side Rendering)

Server → Renders HTML → Sends full page → Browser shows → Downloads JS → Hydrates

✅ visible immediately
JS
✅ interactive
✅ Fast first paint ✅ Great SEO ⚠️ Server load per request ⚠️ TTFB can be slow
SSG (Static Site Generation)

Build time → Pre-render ALL pages → Deploy → Serve static HTML from CDN

✅ instant from CDN!
JS
✅ interactive
✅ Fastest possible ✅ Great SEO ✅ Cheapest hosting ❌ Stale until rebuild
ISR (Incremental Static Regeneration)

Like SSG but pages regenerate in background — serve stale → rebuild → serve fresh

✅ Fast like SSG ✅ Fresh content ✅ No full rebuild ⚠️ Slightly stale for first visitor

Comparison Table

เปรียบเทียบทั้ง 4 strategies แบบ side-by-side — ดูง่ายๆ ว่าแต่ละตัวเด่นตรงไหน และเหมาะกับ use case แบบไหน:

CSRSSRSSGISR
First Paint🐢 Slow⚡ Fast⚡ Fast⚡ Fast
SEO❌✅✅✅
Interactivity⚡ Fast⚠️ Wait⚠️ Wait⚠️ Wait
Server LoadNoneHighNoneLow
Freshness✅ Live✅ Live❌ Build⚡ Near
Hosting Cost💰💰💰💰💰💰💰
Build TimeFastN/A🐢 SlowFast
Best For:
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
Released2013 (Meta)2014 (Evan)2016 (Rich)
ApproachLibraryFrameworkCompiler
RenderingVirtual DOMVirtual DOMNo VDOM!
Bundle Size~40 KB~33 KB~2 KB
Learning⭐⭐ Medium⭐ Easy⭐ Easy
State MgmtRedux/ZustandPinia/VuexBuilt-in
Meta-frameworkNext.jsNuxtSvelteKit
Ecosystem⭐⭐⭐ Huge⭐⭐ Large⭐ Growing
Jobs⭐⭐⭐ Most⭐⭐ Many⭐ Growing
Performance⭐⭐ Good⭐⭐ Good⭐⭐⭐ Best
💡 2026 Recommendation:
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 เปล่าๆ:

⚛️
Next.js (React)
  • 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
💚
Nuxt (Vue)
  • Auto-imports (no import statements!)
  • File-based routing
  • SSR / SSG / SPA / Hybrid
  • Server API routes (Nitro)
  • 50+ official modules
  • Deploy: Vercel, Netlify, Cloudflare
🔥
SvelteKit (Svelte)
  • Smallest bundle sizes
  • File-based routing
  • SSR / SSG / SPA
  • Form actions (progressive enhancement)
  • Built-in adapters (Node, CF, Vercel)
  • Best DX (Developer Experience)
🚀
Astro (Multi-framework)
  • 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

LCP
Largest Contentful Paint
"เวลาที่ content หลักโหลดเสร็จ"
✅ < 2.5s ⚠️ 2.5–4.0s ❌ > 4.0s
Triggers: <img> hero, <video> poster, CSS bg-image, large text
INP
Interaction to Next Paint
"เวลาตอบสนองเมื่อ user คลิก/พิมพ์"
✅ < 200ms ⚠️ 200–500ms ❌ > 500ms
Worst: Click → long JS, Type → heavy handlers, Scroll → re-renders
CLS
Cumulative Layout Shift
"หน้าจอกระโดดไปมาไหม?"
✅ < 0.1 ⚠️ 0.1–0.25 ❌ > 0.25
Causes: No width/height, late ads, font swap (FOUT), dynamic content
📊 CWV affects Google search ranking! 📏 Measure: PageSpeed Insights, Lighthouse, CrUX

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

❌ Before
<img src="hero.jpg">
  • 5 MB JPEG
  • 4000×3000 px (displayed at 800×600)
  • No lazy loading
  • No responsive sizes
  • LCP: 8 seconds 💀
✅ After
  • 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

FormatSizeQualitySupportUse Case
JPEG⭐⭐⭐⭐✅ AllPhotos (legacy)
PNG⭐⭐⭐⭐✅ AllTransparency
WebP⭐⭐⭐⭐⭐⭐✅ 97%+Photos (modern)
AVIF⭐⭐⭐⭐⭐⭐⭐⚠️ 93%Best ratio
SVGTiny∞✅ AllIcons, logos
💡 Priority: AVIF > WebP > JPEG — Use <picture> for fallbacks

Next.js Image Component

❌ Without Code Splitting
bundle.js (2.5 MB)

Everything loaded upfront — user visits /home → downloads entire app

✅ With Code Splitting
layout (100 KB) home (50 KB) about (30 KB) dashboard (80 KB) admin (120 KB) chart (200 KB)

Visit /home → loads layout + home only (150 KB)


📦 Bundle Optimization — ส่ง JS น้อยลง

Code Splitting & Lazy Loading

📦 Bundle Size Budget
🎯 Targets (gzipped):
  • 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
📏 Common Sizes (min+gz):
  • 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
💡 Every KB matters on mobile (3G: 1 KB ≈ 10ms)
// ═══════════════════════════════
// 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):

1. Browser Cache (HTTP Headers)
Static assets (JS, CSS, images):
Cache-Control: public, max-age=31536000, immutable
→ Cache 1 year, use content hash: main.a1b2c3d4.js
HTML pages: Cache-Control: no-cache → Always check with server
API responses: Cache-Control: private, max-age=60 → Cache 60s, user-specific
2. Service Worker (Offline-first)
Request → Service Worker → Cache? ✅/❌ → Return / Fetch
Cache First — cache, fallback network Network First — network, fallback cache Stale While Revalidate — return cache, update bg Cache Only — offline content
3. CDN Cache
🇹🇭 Bangkok → 🇸🇬 CDN Edge (cached!) → 🌐 Origin

Providers: CloudFlare, CloudFront, Fastly — Reduce latency from 200ms to 20ms


🚀 Caching Strategies

Browser Caching

📱 PWA — Progressive Web App

Web App ที่ทำงานเหมือน Native App

📥
Installable
เพิ่มลง home screen
📡
Offline
ทำงานได้แม้ไม่มี internet
🔔
Push Notifications
แจ้งเตือนได้
🔄
Background Sync
sync เมื่อกลับมาออนไลน์
⚡
Fast
cached assets, instant load
📐
Responsive
ทุก screen size
Requirements: HTTPS · Service Worker · Web App Manifest · Responsive design
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 แล้วเห็นผลลัพธ์ที่น่าทึ่ง:

📋 Frontend Performance Checklist
🖼️ Images
  • 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
📦 JavaScript
  • 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
🎨 CSS
  • Inline critical CSS
  • Async load non-critical CSS
  • Remove unused CSS (PurgeCSS/Tailwind)
  • Minimize CSS-in-JS
🔤 Fonts
  • Self-host fonts
  • font-display: swap/optional
  • Preload critical fonts
  • Subset fonts
  • Max 2 font families
🌐 Network
  • CDN for static assets
  • HTTP/2 or HTTP/3
  • Gzip/Brotli compression
  • Cache headers (immutable)
  • Preconnect & DNS prefetch
⚡ Rendering & 📊 Monitoring
  • 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 ควรจำ:

  1. SSR สำหรับ SEO + LCP, CSR สำหรับ interactivity → ใช้ Next.js/Nuxt/SvelteKit ที่รวมทุกอย่าง
  2. Core Web Vitals = Google ranking factor → LCP < 2.5s, INP < 200ms, CLS < 0.1
  3. Images = ปัญหา #1 → WebP/AVIF, responsive sizes, lazy load, width/height attributes
  4. Code splitting = ลด initial load → Lazy import components, route-based splitting
  5. Tree shaking = ลบ dead code → import { x } from 'lib' แทน import lib
  6. Bundle budget < 100 KB → วัดทุก library ก่อน install (bundlephobia.com)
  7. CDN + Cache = เร็วขึ้น 10x → Static assets cache 1 year, HTML no-cache
  8. Font optimization = ป้องกัน CLS → Self-host, font-display: swap, max 2 fonts
  9. PWA = native-like experience → Offline, installable, push notifications
  10. วัด ก่อน optimize → ใช้ Lighthouse, web-vitals, RUM — อย่าเดา!

*📚 DevOps Series ทั้งหมด:*

  1. *Git Branching* | 2. *CI/CD Pipeline* | 3. *Docker vs VMs*
  2. *API Lifecycle* | 5. *Kubernetes* | 6. *Networking*
  3. *Infrastructure as Code* | 8. *Monitoring & Observability*
  4. *Linux & Shell* | 10. *DevSecOps & Security*
  5. *Testing Strategies* | 12. *GitOps & ArgoCD*
  6. *Cloud Architecture* | 14. *SRE*
  7. *GitHub Actions & GitHub Flow* | 16. *Automated Testing with GitHub*
  8. *Database Design & SQL* | 18. *Authentication & Authorization*
  9. *Web Architecture & Design Patterns*
  10. *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:

⏱️ Every Second Counts
Load TimeBounce RateRevenue 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
📊 Real stats (Google):
  • 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
⚡ Performance IS a feature — not an afterthought

🖥️ Rendering Strategies — CSR vs SSR vs SSG vs ISR

The Big Picture

Rendering Strategies
CSR (Client-Side Rendering)

Server → Empty HTML + JS bundle → Browser downloads JS → Executes → Renders

blank
JS download
✅ visible
❌ Slow first paint ❌ Bad SEO ✅ Rich interactivity ✅ Simple hosting
SSR (Server-Side Rendering)

Server → Renders HTML → Sends full page → Browser shows → Downloads JS → Hydrates

✅ visible immediately
JS
✅ interactive
✅ Fast first paint ✅ Great SEO ⚠️ Server load per request ⚠️ TTFB can be slow
SSG (Static Site Generation)

Build time → Pre-render ALL pages → Deploy → Serve static HTML from CDN

✅ instant from CDN!
JS
✅ interactive
✅ Fastest possible ✅ Great SEO ✅ Cheapest hosting ❌ Stale until rebuild
ISR (Incremental Static Regeneration)

Like SSG but pages regenerate in background — serve stale → rebuild → serve fresh

✅ Fast like SSG ✅ Fresh content ✅ No full rebuild ⚠️ Slightly stale for first visitor

Comparison Table

All four strategies side by side — an easy read of where each one is strong, and which use case it suits:

CSRSSRSSGISR
First Paint🐢 Slow⚡ Fast⚡ Fast⚡ Fast
SEO❌✅✅✅
Interactivity⚡ Fast⚠️ Wait⚠️ Wait⚠️ Wait
Server LoadNoneHighNoneLow
Freshness✅ Live✅ Live❌ Build⚡ Near
Hosting Cost💰💰💰💰💰💰💰
Build TimeFastN/A🐢 SlowFast
Best For:
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
Released2013 (Meta)2014 (Evan)2016 (Rich)
ApproachLibraryFrameworkCompiler
RenderingVirtual DOMVirtual DOMNo VDOM!
Bundle Size~40 KB~33 KB~2 KB
Learning⭐⭐ Medium⭐ Easy⭐ Easy
State MgmtRedux/ZustandPinia/VuexBuilt-in
Meta-frameworkNext.jsNuxtSvelteKit
Ecosystem⭐⭐⭐ Huge⭐⭐ Large⭐ Growing
Jobs⭐⭐⭐ Most⭐⭐ Many⭐ Growing
Performance⭐⭐ Good⭐⭐ Good⭐⭐⭐ Best
💡 2026 Recommendation:
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:

⚛️
Next.js (React)
  • 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
💚
Nuxt (Vue)
  • Auto-imports (no import statements!)
  • File-based routing
  • SSR / SSG / SPA / Hybrid
  • Server API routes (Nitro)
  • 50+ official modules
  • Deploy: Vercel, Netlify, Cloudflare
🔥
SvelteKit (Svelte)
  • Smallest bundle sizes
  • File-based routing
  • SSR / SSG / SPA
  • Form actions (progressive enhancement)
  • Built-in adapters (Node, CF, Vercel)
  • Best DX (Developer Experience)
🚀
Astro (Multi-framework)
  • 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

LCP
Largest Contentful Paint
"How long the main content takes to finish loading"
✅ < 2.5s ⚠️ 2.5–4.0s ❌ > 4.0s
Triggers: <img> hero, <video> poster, CSS bg-image, large text
INP
Interaction to Next Paint
"How quickly the page answers a click or a keystroke"
✅ < 200ms ⚠️ 200–500ms ❌ > 500ms
Worst: Click → long JS, Type → heavy handlers, Scroll → re-renders
CLS
Cumulative Layout Shift
"Does the screen jump around?"
✅ < 0.1 ⚠️ 0.1–0.25 ❌ > 0.25
Causes: No width/height, late ads, font swap (FOUT), dynamic content
📊 CWV affects Google search ranking! 📏 Measure: PageSpeed Insights, Lighthouse, CrUX

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

❌ Before
<img src="hero.jpg">
  • 5 MB JPEG
  • 4000×3000 px (displayed at 800×600)
  • No lazy loading
  • No responsive sizes
  • LCP: 8 seconds 💀
✅ After
  • 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

FormatSizeQualitySupportUse Case
JPEG⭐⭐⭐⭐✅ AllPhotos (legacy)
PNG⭐⭐⭐⭐✅ AllTransparency
WebP⭐⭐⭐⭐⭐⭐✅ 97%+Photos (modern)
AVIF⭐⭐⭐⭐⭐⭐⭐⚠️ 93%Best ratio
SVGTiny∞✅ AllIcons, logos
💡 Priority: AVIF > WebP > JPEG — Use <picture> for fallbacks

Next.js Image Component

❌ Without Code Splitting
bundle.js (2.5 MB)

Everything loaded upfront — user visits /home → downloads entire app

✅ With Code Splitting
layout (100 KB) home (50 KB) about (30 KB) dashboard (80 KB) admin (120 KB) chart (200 KB)

Visit /home → loads layout + home only (150 KB)


📦 Bundle Optimization — Ship Less JS

Code Splitting & Lazy Loading

📦 Bundle Size Budget
🎯 Targets (gzipped):
  • 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
📏 Common Sizes (min+gz):
  • 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
💡 Every KB matters on mobile (3G: 1 KB ≈ 10ms)
// ═══════════════════════════════
// 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):

1. Browser Cache (HTTP Headers)
Static assets (JS, CSS, images):
Cache-Control: public, max-age=31536000, immutable
→ Cache 1 year, use content hash: main.a1b2c3d4.js
HTML pages: Cache-Control: no-cache → Always check with server
API responses: Cache-Control: private, max-age=60 → Cache 60s, user-specific
2. Service Worker (Offline-first)
Request → Service Worker → Cache? ✅/❌ → Return / Fetch
Cache First — cache, fallback network Network First — network, fallback cache Stale While Revalidate — return cache, update bg Cache Only — offline content
3. CDN Cache
🇹🇭 Bangkok → 🇸🇬 CDN Edge (cached!) → 🌐 Origin

Providers: CloudFlare, CloudFront, Fastly — Reduce latency from 200ms to 20ms


🚀 Caching Strategies

Browser Caching

📱 PWA — Progressive Web App

A web app that behaves like a native app

📥
Installable
Add it to the home screen
📡
Offline
Works with no internet
🔔
Push Notifications
It can notify you
🔄
Background Sync
sync when back online
⚡
Fast
cached assets, instant load
📐
Responsive
Every screen size
Requirements: HTTPS · Service Worker · Web App Manifest · Responsive design
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:

📋 Frontend Performance Checklist
🖼️ Images
  • 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
📦 JavaScript
  • 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
🎨 CSS
  • Inline critical CSS
  • Async load non-critical CSS
  • Remove unused CSS (PurgeCSS/Tailwind)
  • Minimize CSS-in-JS
🔤 Fonts
  • Self-host fonts
  • font-display: swap/optional
  • Preload critical fonts
  • Subset fonts
  • Max 2 font families
🌐 Network
  • CDN for static assets
  • HTTP/2 or HTTP/3
  • Gzip/Brotli compression
  • Cache headers (immutable)
  • Preconnect & DNS prefetch
⚡ Rendering & 📊 Monitoring
  • 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:

  1. SSR for SEO + LCP, CSR for interactivity → use Next.js/Nuxt/SvelteKit, which give you all of it
  2. Core Web Vitals = Google ranking factor → LCP < 2.5s, INP < 200ms, CLS < 0.1
  3. Images = problem #1 → WebP/AVIF, responsive sizes, lazy load, width/height attributes
  4. Code splitting = a smaller initial load → Lazy import components, route-based splitting
  5. Tree shaking = dead code removed → import { x } from 'lib' instead of import lib
  6. Bundle budget < 100 KB → measure every library before you install it (bundlephobia.com)
  7. CDN + Cache = 10x faster → Static assets cache 1 year, HTML no-cache
  8. Font optimization = CLS prevented → Self-host, font-display: swap, max 2 fonts
  9. PWA = native-like experience → Offline, installable, push notifications
  10. Measure before you optimize → use Lighthouse, web-vitals, RUM — never guess!

*📚 The complete DevOps series:*

  1. *Git Branching* | 2. *CI/CD Pipeline* | 3. *Docker vs VMs*
  2. *API Lifecycle* | 5. *Kubernetes* | 6. *Networking*
  3. *Infrastructure as Code* | 8. *Monitoring & Observability*
  4. *Linux & Shell* | 10. *DevSecOps & Security*
  5. *Testing Strategies* | 12. *GitOps & ArgoCD*
  6. *Cloud Architecture* | 14. *SRE*
  7. *GitHub Actions & GitHub Flow* | 16. *Automated Testing with GitHub*
  8. *Database Design & SQL* | 18. *Authentication & Authorization*
  9. *Web Architecture & Design Patterns*
  10. *Frontend Performance & Modern Frameworks* ← 📍 you are here
บทความจากซีรีส์ DevOps & Vibe Coding 2026A post from the DevOps & Vibe Coding 2026 series
← Previous
Web Architecture & Design Patterns — ออกแบบระบบให้ Scale ได้และดูแลง่าย
Next →
Deployment & Hosting Strategies — จาก Code สู่ Production