Mẹo tối ưu hiệu suất Next.js: Từ tốt đến hoàn hảo trên Lighthouse

Table of Contents
Hiệu suất không phải là một danh sách kiểm tra—đó là một thực hành liên tục. Một trang web nhanh cải thiện SEO, giảm tỷ lệ thoát và trực tiếp tăng chuyển đổi. Next.js loại bỏ hầu hết các trở ngại, nhưng bạn vẫn phải hiểu tại sao mỗi tính năng lại quan trọng và khi nào nên sử dụng nó.
Hiểu các chỉ số quan trọng
Trước khi tối ưu hóa, hãy biết bạn đang đo lường điều gì. Core Web Vitals là tiêu chuẩn ngành mà Google sử dụng trong xếp hạng:
| Chỉ số | Nó đo lường điều gì | Ngưỡng tốt | Tác động |
|---|---|---|---|
| LCP | Largest Contentful Paint — khi nội dung chính xuất hiện | < 2.5s | Tốc độ tải cảm nhận được |
| INP | Interaction to Next Paint — khả năng phản hồi | < 200ms | Khả năng tương tác của người dùng |
| CLS | Cumulative Layout Shift — độ ổn định hình ảnh | < 0.1 | Giật hình / độ tin cậy |
| FID | First Input Delay — độ trễ nhấp/nhấn phím đầu tiên | < 100ms | Cảm giác tương tác đầu tiên |
Lưu ý về INP
FID đã được thay thế bằng INP vào tháng 3 năm 2024. INP đo lường khả năng phản hồi trên toàn bộ vòng đời trang, không chỉ đầu vào đầu tiên. Đó là một mục tiêu lớn hơn—và việc tối ưu hóa có ý nghĩa giúp ích cho cả hai.
Bộ công cụ tối ưu hóa
Mô hình tư duy
Mọi kỹ thuật hiệu suất trong Next.js đều hướng tới một trong hai mục tiêu: hiển thị các pixel đầu tiên trên màn hình nhanh hơn hoặc gửi ít JavaScript hơn đến client. Hãy ghi nhớ điều đó và "tại sao" sẽ trở nên rõ ràng.
1. Tối ưu hóa hình ảnh — Thành công số 1 cho LCP
Sử dụng thành phần <Image> từ next/image, không phải thẻ <img> thô. Nó tự động thực hiện ba việc:
- Chuyển đổi WebP/AVIF tự động — tệp nhỏ hơn mà không giảm chất lượng
- Kiểm soát kích thước/chất lượng — phục vụ hình ảnh 300px trên di động, 1200px trên máy tính để bàn
- Ngăn chặn dịch chuyển bố cục — yêu cầu chiều rộng/chiều cao để trình duyệt dành không gian phù hợp
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Dashboard overview"
width={1200}
height={675}
priority
className="rounded-xl"
/>
);
}
Quan trọng: Luôn cung cấp chiều rộng và chiều cao
Nếu không có cả hai, Next.js không thể dành không gian bố cục và CLS sẽ bị ảnh hưởng. Sử dụng kích thước nội tại thực của hình ảnh của bạn. Nếu nghi ngờ, hãy kiểm tra tệp hình ảnh trong trình duyệt tệp hệ điều hành của bạn hoặc sử dụng trình chỉnh sửa hình ảnh.
2. Tối ưu hóa phông chữ — Loại bỏ FOIT
Sử dụng mô-đun next/font. Nó tải xuống phông chữ tại thời điểm build và phục vụ chúng cục bộ—không có yêu cầu bên thứ ba, không có dịch chuyển bố cục.
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // ✅ Shows fallback text instantly
variable: '--font-inter',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
weight: ['400', '700'],
});
Những gì bạn nhận được miễn phí
- Tệp phông chữ tự lưu trữ — không có yêu cầu bên ngoài tới Google
font-display: swaptự động — văn bản hiển thị trong khi phông chữ tải- Tập hợp con theo
subsets— bỏ các ký tự không sử dụng (ví dụ: bỏcyrillicnếu không sử dụng) - Biến CSS để tích hợp Tailwind dễ dàng
import localFont from 'next/font/local';
const geist = localFont({
src: './fonts/GeistVF.woff2',
display: 'swap',
variable: '--font-geist',
});
Khi nào nên sử dụng phông chữ cục bộ
Nếu bạn đã có tệp phông chữ (đã mua hoặc tự lưu trữ), next/font/local mang lại cho bạn những lợi ích không dịch chuyển tương tự mà không cần thông qua CDN của Google.
3. Nhập động — Chia nhỏ mã nặng
Không tải các thành phần không hiển thị trên lần hiển thị ban đầu. Sử dụng next/dynamic để chia mã ở cấp thành phần:
// Heavy chart library: ~200KB gzipped
import dynamic from 'next/dynamic';
// Load it only when needed (client-side, no SSR)
const RevenueChart = dynamic(
() => import('@/components/RevenueChart'),
{
ssr: false, // ✅ Skip server rendering for this component
loading: () => <Skeleton />, // ✅ Show placeholder instantly
}
);
export function Dashboard() {
return <RevenueChart />;
}
Khi nào nên bỏ qua SSR (`ssr: false`)
- Biểu đồ/đồ thị dựa vào
windowhoặc API trình duyệt - Thư viện hoạt hình nặng (GSAP, Framer Motion)
- Tiện ích của bên thứ ba (Google Maps, Stripe Elements)
- Chuyển đổi chế độ tối — đợi đã, cái này đủ rẻ để render trên máy chủ
Nếu không có import động, các bundler có thể nhóm mọi thứ vào một tệp JS duy nhất. Với import động, thành phần sẽ tải trong một chunk riêng biệt chỉ được tìm nạp khi cần. Đây là công cụ giảm kích thước bundle lớn nhất cho các ứng dụng vừa và lớn.
Sử dụng bộ phân tích bundle (bước 6) để tìm các thành phần nặng nhất của bạn. Thông thường: trình chỉnh sửa văn bản phong phú, biểu đồ, trình xem 3D, tiện ích xác thực. Đừng chia nhỏ quá mức—import động thêm một vòng mạng. Chia nhỏ các thành phần ngăn chặn việc tải xuống JS ban đầu có ý nghĩa.
4. Server Components — Mặc định không gửi JS
Trong App Router, tất cả các thành phần đều là Server Components theo mặc định. Đây là đòn bẩy hiệu suất lớn nhất của bạn—bạn nhận được kết quả render mà không gửi JS của thành phần đó đến trình duyệt.
// This entire file runs on the server
// → Zero JavaScript sent to the browser for this component
async function BlogPost({ slug }: { slug: string }) {
const post = await db.posts.findUnique({ where: { slug } });
return (
<article>
<h1>{post.title}</h1>
<MDXContent source={post.content} />
</article>
);
}
Need a useState, useEffect, onClick, or window?
└── Yes → Add "use client" (Client Component)
└── No → It's already a Server Component (best default)
Server Component delivers:
✅ HTML directly to browser (fast FCP)
✅ Data fetching on server (no client fetch waterfalls)
✅ Zero JS bundle cost for that component
| Kịch bản | Nên là | Ghi chú |
|---|---|---|
| Vỏ bố cục trang | Máy chủ | Không cần tương tác |
| Trình bao bọc tìm nạp dữ liệu | Máy chủ | Chạy truy vấn trên máy chủ |
| Nút có onClick | Client | Ranh giới tối thiểu |
| Biểu mẫu có useState | Client | Giữ biểu mẫu nhỏ, nâng trạng thái lên |
| Nút chuyển đổi chủ đề | Client | Chỉ nút, không phải trang |
Bộ nhớ đệm và luồng dữ liệu
Các lớp bộ nhớ đệm của Next.js
Next.js có nhiều cơ chế bộ nhớ đệm. Hiểu sự khác biệt giúp ngăn chặn 90% sự nhầm lẫn "tại sao dữ liệu của tôi lại cũ?".
Ghi nhớ yêu cầu
Khi bạn gọi await fetch(...) bên trong một Server Component, Next.js sẽ loại bỏ các yêu cầu giống hệt nhau trong một lần render. Một yêu cầu giống hệt thứ hai trong cùng một lần render sẽ trả về kết quả được lưu trong bộ nhớ đệm mà không cần truy cập mạng lại.
Bộ nhớ đệm dữ liệu
Next.js mở rộng fetch với bộ nhớ đệm dữ liệu tích hợp. Kết quả được duy trì (theo mặc định, vĩnh viễn). Các lần render tiếp theo trên các yêu cầu sẽ đọc từ bộ nhớ đệm.
// Force revalidation every hour
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // seconds
});
// No revalidation = cache until you deploy
fetch('https://api.example.com/data');
// Always fresh (no caching)
fetch('https://api.example.com/data', {
cache: 'no-store'
});
Bộ nhớ đệm toàn bộ tuyến đường
Các trang tĩnh được render tại thời điểm build và được lưu vào bộ nhớ đệm dưới dạng HTML. Một trang tĩnh được phục vụ ngay lập tức—không có tính toán máy chủ tại thời điểm yêu cầu.
Bộ nhớ đệm Router (phía Client)
Bộ nhớ đệm router phía client lưu trữ các trang đã render trong bộ nhớ (và localStorage để điều hướng tiến/lùi). Điều hướng trở lại cảm thấy tức thì vì trang tải từ bộ nhớ đệm.
Server Actions làm mờ ranh giới giữa client và server. Gọi chúng từ Client Components, nhưng chúng thực thi trên server với quyền truy cập cơ sở dữ liệu đầy đủ.
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.posts.create({ data: { title } });
revalidatePath('/posts'); // Invalidate the posts cache
}
Tại sao lại là Server Actions?
Trước Server Actions, "tạo bài đăng" yêu cầu xây dựng một tuyến API đầy đủ, xử lý xác thực trong một lớp riêng biệt và xác thực lại trên client bằng logic thẻ bộ nhớ đệm phức tạp. Server Actions cho phép bạn viết mã phía máy chủ nội tuyến — không có mã API rườm rà, không có quản lý trạng thái phía client cho chính thao tác đó.
Thay vì chờ tất cả dữ liệu tải, hãy truyền từng phần HTML khi chúng sẵn sàng:
// Slow data component — this is the bottleneck
async function SlowChart() {
const data = await fetch('/api/slow-data').then(r => r.json());
return <Results data={data} />;
}
// Skeleton shows immediately, content streams in
export default async function DashboardPage() {
return (
<div>
<header>Dashboard</header> {/* ✅ Instant */}
<RecentActivity /> {/* ✅ Fast */}
<Suspense fallback={<ChartSkeleton />}> {/* Shows skeleton */}
<SlowChart /> {/* Streams when ready */}
</Suspense>
</div>
);
}
Cách SSR + Streaming hoạt động cùng nhau
Server Components vẫn chạy trên máy chủ. Sự khác biệt với Suspense là phản hồi truyền các chunk khi chúng sẵn sàng. Vỏ gửi ngay lập tức, dữ liệu chậm gửi sau. Người dùng thấy nội dung dần dần thay vì chờ một phản hồi HTML lớn.
Phân tích gói nâng cao
Biết bạn đang gửi gì
Một import không được tối ưu hóa có thể thêm hơn 200KB vào gói client của bạn. Bộ phân tích gói làm cho những thứ vô hình trở nên hữu hình.
Cài đặt @next/bundle-analyzer
Thêm nó vào dự án của bạn. Nó tạo ra một treemap tương tác của mọi chunk JS:
npm install -D @next/bundle-analyzer
Thiết lập next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
Chạy bộ phân tích
ANALYZE=true npm run build
Mở URL mà nó in ra (thường là http://localhost:3000/_next/analyze/...). Bạn sẽ thấy một treemap trong đó các hộp lớn hơn = các gói lớn hơn. Di chuột để xem cái gì đang chiếm không gian.
Các nạn nhân phổ biến
| Mẫu chống | Vấn đề | Cách tiếp cận tốt hơn |
|---|---|---|
import { format } from 'date-fns' | Kéo toàn bộ thư viện (~100+ hàm) | Sử dụng tree-shaking của date-fns + import cụ thể, hoặc dayjs |
import { debounce } from 'lodash' | Toàn bộ lodash (~500KB) | Sử dụng lodash-es hoặc viết một debounce 3 dòng |
import * as Icons from 'lucide-react' | Kéo mọi biểu tượng nhưng chỉ sử dụng 1 | Sử dụng import có tên: import { ChevronRight } from 'lucide-react' |
| Các thư viện biểu đồ nặng ở cấp cao nhất | Không bao giờ được sử dụng phía trên màn hình | Import động như trong bước 3 |
Nhưng chờ đã, còn nhiều hơn thế
<Link> từ next/link tự động tìm nạp trước các trang được liên kết trong khung nhìn (khi người dùng có kết nối nhanh):
import Link from 'next/link';
<Link href="/en/blog/nextjs-performance"> {/* ✅ Prefetched when visible */}
Read more
</Link>
<Link href="/heavy-page" prefetch={false}> {/* ⬇️ Skip prefetch */}
Heavy page (saves bandwidth on mobile)
</Link>
Tìm nạp dữ liệu song song giúp giảm đáng kể các thác nước:
// ❌ Sequential: 200ms + 300ms = 500ms total
async function SlowPage() {
const user = await fetch('/api/user').then(r => r.json()); // 200ms
const posts = await fetch('/api/posts').then(r => r.json()); // 300ms
return <Dashboard user={user} posts={posts} />;
}
// ✅ Parallel: max(200ms, 300ms) = 300ms total
async function FastPage() {
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);
return <Dashboard user={user} posts={posts} />;
}
Kiểm soát hành vi render trên mỗi trang với cấu hình phân đoạn tuyến đường:
export const dynamic = 'force-static'; // Always static, even in dev
export const revalidate = 3600; // Revalidate every hour
export const fetchCache = 'force-no-store'; // Never use data cache
// For one-off routes without creating a separate layout:
export const metadata = {
title: 'My Page',
robots: { index: false }, // Prevent indexing this page
};
Danh sách kiểm tra tối ưu hóa
| Kiểm tra | Trạng thái | Tác động |
|---|---|---|
Sử dụng <Image> không phải <img> ở mọi nơi | published | Cao — LCP + CLS |
Phông chữ qua next/font, không có stylesheet bên ngoài | published | Cao — FCP + CLS |
| Không có thư viện nặng không được chia nhỏ trong gói đầu vào | draft | Cao — TTI + TBT |
| Server Components > Client Components | draft | Cao — Tổng JS |
Tìm nạp dữ liệu song song với Promise.all | draft | Trung bình — Thời gian tải lần đầu |
| Tìm nạp trước liên kết được điều chỉnh cho thiết bị di động | draft | Trung bình — Tốc độ điều hướng |
Đo lường kết quả của bạn
Đừng đoán. Hãy đo lường. Chạy các thử nghiệm này trong môi trường sản xuất và theo dõi điểm số theo thời gian:
Lighthouse CI
Kiểm tra hiệu suất tự động trong CI
Đặt ngân sách hiệu suất. CI sẽ thất bại nếu một PR làm giảm LCP dưới ngưỡng của bạn. Điều này rất quan trọng để ngăn chặn sự suy thoái.
lighthouse https://yoursite.com --budget-path=budget.json
Vercel Analytics
Tích hợp vào triển khai của bạn
Core Web Vitals thực tế từ người dùng thực. Hiển thị phân phối thực tế: p75, p90 LCP — trung thực hơn các chỉ số phòng thí nghiệm.
SpeedCurve
Giám sát hiệu suất liên tục
Theo dõi các lượt xem filmstrip + chỉ số theo thời gian. Tuyệt vời để tương quan các triển khai với các thay đổi hiệu suất.
PageSpeed Insights
Công cụ chính thức miễn phí của Google
Dữ liệu phòng thí nghiệm + thực địa nhanh chóng. Tốt cho các kiểm toán ban đầu và chụp ảnh màn hình "trước" cho PR của bạn.
Tóm tắt
4 điều hàng đầu cần làm ngay hôm nay
- Thay thế mọi
<img>bằng<Image>— kiểm tra xem có thiếu chiều rộng/chiều cao không - Chuyển việc tải phông chữ sang
next/font - Chạy
ANALYZE=true npm run buildvà tìm kiếm các chunk lớn nhất - Tìm một ứng cử viên động trong codebase của bạn và chuyển đổi sang
next/dynamic
Bốn hành động này thường xuyên giảm 30-50% kích thước gói và đưa LCP từ 6 giây xuống dưới 2 giây.
Đọc thêm: Để đảm bảo chất lượng mã và hiệu suất trong các monorepo lớn, hãy xem Hiệu suất kiểm thử đơn vị Vitest Monorepo.
- So sánh quản lý trạng thái Zustand vs Jotai
- Hướng dẫn xác thực lại động Next.js App Router
- Các mẫu tối ưu hóa hiệu suất Custom React Hook
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Next.js 15 Server Actions vs Route Handlers: So sánh kiến trúc chuyên sâu
Nắm vững khi nào nên chọn Server Actions so với Route Handlers trong Next.js 15, đi sâu vào progressive enhancement, hành vi caching, giao thức RPC và các ranh giới bảo mật.
Read morePostgreSQL Vacuum & Bloat Index: Phát hiện, Giảm thiểu và Tinh chỉnh Tự động
Chẩn đoán và loại bỏ tình trạng phình (bloat) bảng và index trong PostgreSQL. Nắm vững các công thức tinh chỉnh autovacuum, nén dữ liệu không downtime với pg_repack, và cơ chế visibility map của MVCC.
Read more
Tối ưu hóa bộ nhớ Redis: Nội bộ, mã hóa cấu trúc dữ liệu và lập hồ sơ bộ nhớ
Giảm tới 70% mức sử dụng RAM Redis của bạn bằng cách tìm hiểu sâu về ziplists, listpacks, quicklists, chi phí SDS của chuỗi và giảm thiểu phân mảnh bộ nhớ tự động.
Read more