Tối ưu Performance Next.js: Hướng dẫn Toàn diện 2025

11 phút đọcCong Dinh
Tối ưu Performance Next.js: Hướng dẫn Toàn diện 2025

Tối ưu Performance Next.js: Hướng dẫn Toàn diện 2025

Performance không chỉ là về tốc độ - đó là về trải nghiệm người dùng, SEO ranking, và conversion rates. Trong bài viết này, chúng ta sẽ khám phá các techniques để tối ưu ứng dụng Next.js nhằm đạt được Lighthouse score trên 95 và cải thiện Core Web Vitals.

Core Web Vitals Overview

Core Web Vitals là tập hợp các metrics quan trọng do Google định nghĩa để đo lường user experience. Google sử dụng các metrics này như ranking factors cho SEO.

1. Largest Contentful Paint (LCP)

LCP đo thời gian để largest content element hiển thị trên viewport.

Target: < 2.5 giây

Ý nghĩa: User thấy nội dung chính nhanh như thế nào

LCP thường gồm:

  • Images
  • Video thumbnails
  • Background images với CSS
  • Block-level text elements
// Cải thiện LCP với Next.js Image
import Image from 'next/image';
 
export function Hero() {
  return (
    <div className="relative h-screen">
      {/* ✅ priority flag để preload hero image */}
      <Image
        src="/hero-background.jpg"
        alt="Hero Background"
        fill
        priority
        sizes="100vw"
        className="object-cover"
      />
      <h1 className="relative z-10">Welcome to our site</h1>
    </div>
  );
}

2. First Input Delay (FID) / Interaction to Next Paint (INP)

FID (đang được thay thế bởi INP) đo thời gian từ khi user tương tác đến khi browser phản hồi.

Target FID: < 100ms
Target INP: < 200ms

Ý nghĩa: Trang web responsive với user interactions như thế nào

Cách cải thiện:

  • Minimize JavaScript execution time
  • Code splitting để giảm main thread blocking
  • Use Web Workers cho heavy computations
// ❌ Bad: Heavy computation blocking main thread
function ExpensiveComponent({ data }: { data: number[] }) {
  const result = data.reduce((acc, val) => acc + Math.pow(val, 2), 0);
  return <div>Result: {result}</div>;
}
 
// ✅ Good: Memoize expensive calculations
import { useMemo } from 'react';
 
function ExpensiveComponent({ data }: { data: number[] }) {
  const result = useMemo(
    () => data.reduce((acc, val) => acc + Math.pow(val, 2), 0),
    [data]
  );
  return <div>Result: {result}</div>;
}

3. Cumulative Layout Shift (CLS)

CLS đo visual stability - bao nhiêu unexpected layout shifts xảy ra.

Target: < 0.1

Ý nghĩa: Nội dung có nhảy lung tung khi load không

Common causes:

  • Images không có dimensions
  • Ads, embeds, iframes không có reserved space
  • Web fonts causing FOIT/FOUT
  • Dynamic content insertion
// ❌ Bad: Không specify dimensions
<img src="/photo.jpg" alt="Photo" />
 
// ✅ Good: Always specify width và height
import Image from 'next/image';
 
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  // hoặc dùng fill với aspect-ratio container
/>

Image Optimization

Images thường chiếm 50-70% page weight. Next.js Image component giúp tối ưu tự động.

Next.js Image Component

import Image from 'next/image';
 
// ✅ Basic usage với automatic optimization
export function ProductImage() {
  return (
    <Image
      src="/products/laptop.jpg"
      alt="Gaming Laptop"
      width={800}
      height={600}
      quality={85} // Default: 75
    />
  );
}
 
// ✅ Responsive images với sizes
export function ResponsiveImage() {
  return (
    <Image
      src="/banner.jpg"
      alt="Banner"
      width={1200}
      height={400}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  );
}

Lazy Loading Images

Next.js lazy loads images by default khi chúng nằm ngoài viewport:

// ✅ Lazy load by default (except priority images)
export function Gallery({ images }: { images: string[] }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {images.map((src, index) => (
        <Image
          key={src}
          src={src}
          alt={`Gallery image ${index + 1}`}
          width={400}
          height={300}
          loading="lazy" // Explicit (nhưng default rồi)
        />
      ))}
    </div>
  );
}
 
// Above the fold images nên dùng priority
export function HeroSection() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1920}
      height={1080}
      priority // ✅ Preload hero image
    />
  );
}

Image Formats: WebP và AVIF

Next.js tự động convert images sang modern formats:

// next.config.ts
import type { NextConfig } from 'next';
 
const config: NextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'], // ✅ Try AVIF first, fallback to WebP
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};
 
export default config;

External Images

Nếu dùng external image CDN:

// next.config.ts
const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      },
    ],
  },
};

Image Loading Strategies

// ✅ Blur placeholder cho better UX
import Image from 'next/image';
import placeholder from './placeholder.jpg';
 
export function ImageWithPlaceholder() {
  return (
    <Image
      src="/photo.jpg"
      alt="Photo"
      width={800}
      height={600}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // hoặc import static image
    />
  );
}
 
// ✅ Dynamic blur với plaiceholder library
import { getPlaiceholder } from 'plaiceholder';
 
async function getImageWithPlaceholder(src: string) {
  const buffer = await fetch(src).then(res => res.arrayBuffer());
  const { base64 } = await getPlaiceholder(Buffer.from(buffer));
  return base64;
}

Code Splitting

Code splitting giúp giảm initial bundle size bằng cách load code on-demand.

Dynamic Imports

// ✅ Dynamic import cho components không critical
import dynamic from 'next/dynamic';
 
// Load component only khi cần
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false, // Disable SSR nếu component chỉ chạy client-side
});
 
export function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart data={chartData} />
    </div>
  );
}

Route-based Code Splitting

Next.js tự động split code theo routes:

// app/dashboard/page.tsx
// Mỗi page tự động là một chunk riêng
export default function DashboardPage() {
  return <div>Dashboard</div>;
}
 
// app/settings/page.tsx
// Code của page này chỉ load khi user visit /settings
export default function SettingsPage() {
  return <div>Settings</div>;
}

Component-level Splitting

'use client';
 
import { useState } from 'react';
import dynamic from 'next/dynamic';
 
// ✅ Lazy load modal component
const EditModal = dynamic(() => import('./EditModal'), {
  ssr: false,
});
 
export function UserProfile() {
  const [showModal, setShowModal] = useState(false);
 
  return (
    <div>
      <button onClick={() => setShowModal(true)}>Edit Profile</button>
      {/* Modal component chỉ load khi user click button */}
      {showModal && <EditModal onClose={() => setShowModal(false)} />}
    </div>
  );
}

Third-party Library Splitting

// ❌ Bad: Import entire library
import _ from 'lodash';
 
function Component() {
  return _.debounce(() => {}, 300);
}
 
// ✅ Good: Import specific functions
import debounce from 'lodash/debounce';
 
function Component() {
  return debounce(() => {}, 300);
}
 
// ✅ Better: Dynamic import cho heavy libraries
async function heavyOperation() {
  const { default: moment } = await import('moment');
  return moment().format('YYYY-MM-DD');
}

Font Optimization

Fonts có thể cause significant layout shifts và delay rendering.

next/font - Built-in Font Optimization

// app/layout.tsx
import { Inter, Fira_Code } from 'next/font/google';
 
// ✅ Load Google Fonts with optimization
const inter = Inter({
  subsets: ['latin', 'vietnamese'], // Chỉ load subsets cần thiết
  display: 'swap', // Font display strategy
  variable: '--font-inter', // CSS variable
  preload: true,
});
 
const firaCode = Fira_Code({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-fira-code',
});
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="vi" className={`${inter.variable} ${firaCode.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  );
}
/* globals.css */
:root {
  --font-inter: 'Inter', sans-serif;
  --font-fira-code: 'Fira Code', monospace;
}
 
body {
  font-family: var(--font-inter);
}
 
code {
  font-family: var(--font-fira-code);
}

Local Fonts

// app/layout.tsx
import localFont from 'next/font/local';
 
const myFont = localFont({
  src: [
    {
      path: '../public/fonts/MyFont-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../public/fonts/MyFont-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-my-font',
  display: 'swap',
});
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="vi" className={myFont.variable}>
      <body>{children}</body>
    </html>
  );
}

Font Display Strategies

const font = Inter({
  subsets: ['latin'],
  display: 'swap', // Options: auto, block, swap, fallback, optional
});
 
/**
 * Display strategies:
 * - auto: Browser default
 * - block: Block text rendering (FOIT) - 3s max
 * - swap: Show fallback immediately (FOUT)
 * - fallback: 100ms block, 3s swap
 * - optional: 100ms block, no swap (use fallback if font not loaded)
 * 
 * Recommended: 'swap' cho most cases
 */

Measuring Performance

Lighthouse

# CLI tool
npm install -g lighthouse
 
# Run Lighthouse
lighthouse https://your-site.com --view
 
# Or in Chrome DevTools
# 1. Open DevTools (F12)
# 2. Navigate to "Lighthouse" tab
# 3. Click "Analyze page load"

Chrome DevTools Performance Tab

// Performance monitoring trong code
export function monitorPerformance() {
  if (typeof window !== 'undefined' && 'performance' in window) {
    // Navigation Timing
    const navTiming = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
    console.log('DOM Content Loaded:', navTiming.domContentLoadedEventEnd - navTiming.fetchStart);
    console.log('Page Load Time:', navTiming.loadEventEnd - navTiming.fetchStart);
    
    // Paint Timing
    const paintEntries = performance.getEntriesByType('paint');
    paintEntries.forEach(entry => {
      console.log(`${entry.name}:`, entry.startTime);
    });
  }
}

Web Vitals Library

// app/layout.tsx
import { Suspense } from 'react';
import { WebVitals } from '@/components/web-vitals';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="vi">
      <body>
        {children}
        <Suspense fallback={null}>
          <WebVitals />
        </Suspense>
      </body>
    </html>
  );
}
// components/web-vitals.tsx
'use client';
 
import { useEffect } from 'react';
import { onCLS, onFID, onFCP, onLCP, onTTFB, onINP } from 'web-vitals';
 
export function WebVitals() {
  useEffect(() => {
    // Track Core Web Vitals
    onCLS(metric => {
      console.log('CLS:', metric.value);
      // Send to analytics
      sendToAnalytics('CLS', metric.value);
    });
 
    onFID(metric => {
      console.log('FID:', metric.value);
      sendToAnalytics('FID', metric.value);
    });
 
    onINP(metric => {
      console.log('INP:', metric.value);
      sendToAnalytics('INP', metric.value);
    });
 
    onLCP(metric => {
      console.log('LCP:', metric.value);
      sendToAnalytics('LCP', metric.value);
    });
 
    onFCP(metric => {
      console.log('FCP:', metric.value);
      sendToAnalytics('FCP', metric.value);
    });
 
    onTTFB(metric => {
      console.log('TTFB:', metric.value);
      sendToAnalytics('TTFB', metric.value);
    });
  }, []);
 
  return null;
}
 
function sendToAnalytics(metric: string, value: number) {
  // Send to your analytics service
  if (typeof window !== 'undefined' && 'gtag' in window) {
    (window as any).gtag('event', metric, {
      value: Math.round(metric === 'CLS' ? value * 1000 : value),
      metric_id: `${metric}-${Date.now()}`,
      non_interaction: true,
    });
  }
}

Real User Monitoring (RUM)

// lib/analytics.ts
export function initRUM() {
  if (typeof window === 'undefined') return;
 
  // Track page views
  const sendPageView = () => {
    const url = window.location.href;
    const referrer = document.referrer;
    
    fetch('/api/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        event: 'pageview',
        url,
        referrer,
        timestamp: Date.now(),
      }),
    });
  };
 
  // Send on load
  sendPageView();
 
  // Track SPA navigations
  let lastUrl = window.location.href;
  new MutationObserver(() => {
    const currentUrl = window.location.href;
    if (currentUrl !== lastUrl) {
      lastUrl = currentUrl;
      sendPageView();
    }
  }).observe(document, { subtree: true, childList: true });
}

Performance Checklist

Images

  • [ ] Use Next.js Image component cho tất cả images
  • [ ] Specify width và height để avoid CLS
  • [ ] Use priority cho above-the-fold images
  • [ ] Lazy load below-the-fold images
  • [ ] Optimize image quality (75-85%)
  • [ ] Use modern formats (WebP, AVIF)
  • [ ] Implement blur placeholders

JavaScript

  • [ ] Enable code splitting với dynamic imports
  • [ ] Minimize third-party scripts
  • [ ] Use React.memo() cho expensive components
  • [ ] Implement useMemo/useCallback appropriately
  • [ ] Avoid unnecessary re-renders
  • [ ] Remove unused dependencies
  • [ ] Tree-shake với ES modules

Fonts

  • [ ] Use next/font cho font optimization
  • [ ] Preload critical fonts
  • [ ] Use font-display: swap
  • [ ] Subset fonts (chỉ load characters cần thiết)
  • [ ] Minimize font variants

Caching

  • [ ] Configure proper cache headers
  • [ ] Use ISR (Incremental Static Regeneration)
  • [ ] Implement stale-while-revalidate
  • [ ] Cache API responses
  • [ ] Use CDN cho static assets

Monitoring

  • [ ] Track Core Web Vitals
  • [ ] Set up Real User Monitoring
  • [ ] Monitor bundle sizes
  • [ ] Regular Lighthouse audits
  • [ ] Alert on performance regressions

Kết luận

Performance optimization là một continuous process, không phải one-time task. Bằng cách implement các techniques trong bài viết này, bạn có thể:

Key Achievements:

  • LCP < 2.5s: Fast content loading với image optimization
  • FID/INP < 100ms/200ms: Responsive interactions với code splitting
  • CLS < 0.1: Stable layouts với proper dimensions
  • Lighthouse Score > 95: Overall excellent performance

Best Practices tổng kết

  1. Measure First: Không optimize blind - luôn measure trước
  2. Focus on User Experience: Metrics chỉ là means, không phải end goal
  3. Automate Monitoring: Set up continuous performance monitoring
  4. Budget Performance: Set và enforce performance budgets
  5. Stay Updated: Next.js updates thường có performance improvements

Performance Budget Example

{
  "budgets": [
    {
      "resourceType": "script",
      "budget": 300
    },
    {
      "resourceType": "image",
      "budget": 500
    },
    {
      "resourceType": "total",
      "budget": 1000
    }
  ]
}

Tools để tiếp tục optimize

  • Lighthouse CI: Automate performance testing
  • Bundle Analyzer: Analyze bundle composition
  • WebPageTest: Detailed performance analysis
  • Chrome User Experience Report: Real-world performance data
  • Vercel Analytics: Built-in performance monitoring

Resources


Performance questions? Hãy comment bên dưới hoặc contact qua email. Happy optimizing! 🚀

Cong Dinh

Cong Dinh

Technology Consultant | Trainer | Solution Architect

Với hơn 10 năm kinh nghiệm trong phát triển web và cloud architecture, tôi giúp doanh nghiệp xây dựng giải pháp công nghệ hiện đại và bền vững. Chuyên môn: Next.js, TypeScript, AWS, và Solution Architecture.

Bài viết liên quan