NextJS – Performance Optimization & Core Web Vitals

 

Core Web Vitals là bộ chỉ số Google dùng để đánh giá UX: LCP (loading), FID/INP (interactivity), CLS (visual stability). NextJS có nhiều tính năng built-in giúp đạt điểm cao — bài này hướng dẫn các kỹ thuật tối ưu thực tế.

1. Đo lường trước khi tối ưu

# Lighthouse CLI
npm install -g lighthouse
lighthouse https://yoursite.com --output html --output-path report.html

# Web Vitals trong code
// app/layout.tsx — Đo Web Vitals
'use client';
import { useReportWebVitals } from 'next/web-vitals';

export default function WebVitalsReporter() {
  useReportWebVitals((metric) => {
    console.log(metric); // { name: 'LCP', value: 1200, rating: 'good' }
    // Gửi lên analytics
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify(metric),
    });
  });
  return null;
}

2. Image Optimization

next/image tự động: WebP/AVIF conversion, lazy loading, srcset responsive.

import Image from 'next/image';

// ✅ Dùng next/image thay vì <img>
export function ProductCard({ product }) {
  return (
    <div>
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={400}
        height={300}
        priority={product.isFeatured}  // LCP image → load trước
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        placeholder="blur"
        blurDataURL="data:image/png;base64,..."  // Skeleton trong khi load
      />
    </div>
  );
}

// Remote images — cấu hình domain
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.shopxyz.com' },
      { protocol: 'https', hostname: 'images.unsplash.com' },
    ],
  },
};

3. Font Optimization

// app/layout.tsx
import { Inter, Be_Vietnam_Pro } from 'next/font/google';

// next/font tự host font — không request tới Google Fonts
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',   // Tránh FOIT (Flash of Invisible Text)
  variable: '--font-inter',
});

const beVietnam = Be_Vietnam_Pro({
  weight: ['400', '500', '700'],
  subsets: ['vietnamese'],
  display: 'swap',
  variable: '--font-be-vietnam',
});

export default function RootLayout({ children }) {
  return (
    <html lang="vi" className={`${inter.variable} ${beVietnam.variable}`}>
      <body>{children}</body>
    </html>
  );
}

4. Server Components — Giảm JavaScript bundle

// ✅ Server Component (default) — Không gửi JS xuống client
// app/products/page.tsx
async function ProductsPage() {
  // Fetch trực tiếp — không cần useEffect hay loading state
  const products = await fetch('https://api.shop.com/products', {
    next: { revalidate: 60 }, // ISR: cache 60 giây
  }).then(r => r.json());

  return (
    <div>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

// ❌ Tránh 'use client' khi không cần interactivity
// Chỉ dùng 'use client' cho: useState, useEffect, event handlers, browser APIs

5. Code Splitting & Dynamic Import

import dynamic from 'next/dynamic';

// Lazy load heavy component — không load khi page init
const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), {
  loading: () => <div className="animate-pulse h-64 bg-gray-100 rounded" />,
  ssr: false,  // Chỉ render ở client (editor dùng browser API)
});

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>Đang tải biểu đồ...</p>,
});

// Chỉ load khi user cần
export function ProductPage() {
  const [showReviews, setShowReviews] = useState(false);
  const ReviewsSection = dynamic(() => import('./ReviewsSection'));

  return (
    <div>
      <ProductInfo />
      <button onClick={() => setShowReviews(true)}>Xem đánh giá</button>
      {showReviews && <ReviewsSection />}
    </div>
  );
}

6. Caching Strategy

// app/products/[id]/page.tsx
export async function generateStaticParams() {
  // Pre-generate top 100 products (SSG)
  const products = await fetch('/api/products/top-100').then(r => r.json());
  return products.map(p => ({ id: p.id }));
}

async function ProductDetailPage({ params }) {
  const product = await fetch(`/api/products/${params.id}`, {
    next: {
      revalidate: 300,  // ISR: revalidate sau 5 phút
      tags: [`product-${params.id}`],  // Tag-based revalidation
    },
  }).then(r => r.json());

  return <ProductDetail product={product} />;
}

// Revalidate theo tag (khi admin cập nhật product)
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(req: Request) {
  const { productId } = await req.json();
  revalidateTag(`product-${productId}`);
  return Response.json({ revalidated: true });
}

7. Bundle Analyzer

npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // ...config
});
ANALYZE=true npm run build
# Mở http://localhost:8888 — xem bundle size từng package

8. Prefetching & Navigation

import Link from 'next/link';

// ✅ Link tự prefetch khi xuất hiện trong viewport
<Link href="/products/123" prefetch={true}>
  Xem chi tiết
</Link>

// Programmatic prefetch
import { useRouter } from 'next/navigation';
const router = useRouter();

// Prefetch khi hover
<div onMouseEnter={() => router.prefetch('/products/123')}>
  Hover to prefetch
</div>

9. Minimize CLS (Cumulative Layout Shift)

// ❌ Gây CLS — size chưa biết trước
<img src={url} />

// ✅ Không gây CLS — đặt width/height hoặc aspect-ratio
<Image src={url} width={800} height={600} alt="..." />

// ❌ Font gây CLS
<link href="https://fonts.googleapis.com/..." rel="stylesheet" />

// ✅ next/font — không gây CLS
import { Inter } from 'next/font/google';

10. Kết luận

Kỹ thuật Cải thiện chỉ số
next/image LCP, CLS
next/font CLS, FID
Server Components TTI, Bundle size
Dynamic import TTI, Bundle size
ISR/SG LCP, TTFB
Prefetch FID, Navigation
  • Đo trước (Lighthouse, WebVitals), tối ưu sau — không đoán
  • Server Components là công cụ mạnh nhất để giảm JS bundle
  • next/imagenext/font giải quyết 80% vấn đề CLS
  • ISR kết hợp tag-based revalidation = UX nhanh + data fresh