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
- Measure First: Không optimize blind - luôn measure trước
- Focus on User Experience: Metrics chỉ là means, không phải end goal
- Automate Monitoring: Set up continuous performance monitoring
- Budget Performance: Set và enforce performance budgets
- 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
- Web.dev - Web Vitals
- Next.js - Optimizing Performance
- Google PageSpeed Insights
- Lighthouse Documentation
Performance questions? Hãy comment bên dưới hoặc contact qua email. Happy optimizing! 🚀

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
Bắt đầu với Next.js 15: Hướng dẫn Toàn diện
Khám phá các tính năng mới của Next.js 15 và cách bắt đầu xây dựng ứng dụng hiện đại với App Router, Server Components và Server Actions để tối ưu performance.
TypeScript Best Practices 2025: Viết Code Sạch và An toàn
Khám phá các pattern TypeScript hiện đại, utility types, và best practices để viết code type-safe và maintainable, giúp team phát triển hiệu quả hơn.

Tailwind CSS Tips và Tricks để Tăng Productivity
Khám phá các tips, tricks và best practices khi sử dụng Tailwind CSS để xây dựng UI nhanh hơn và maintainable hơn trong Next.js projects