Tăng Tốc Website Next.js: Từ 58 lên 71 Điểm Lighthouse Mobile, với Số Đo Thật
Không phải danh sách mẹo chung: đây là những gì mình làm trên chính vinclair.cloud, kèm số đo trước và sau, cả thay đổi có ích lẫn thay đổi làm trang tệ đi.

Mình không muốn viết thêm một danh sách mẹo chung. Bài này ghi lại những gì mình đã làm trên chính vinclair.cloud, một site Next.js xuất tĩnh (static export) chạy sau Caddy và Cloudflare, kèm số Lighthouse trước và sau. Có thay đổi làm điểm tăng, cũng có thay đổi làm trang tệ đi, và mình ghi lại cả hai.

Số đo trước và sau
Mình bắt đầu bằng kết quả để bạn thấy toàn cảnh. Đây là số Lighthouse 12.6 trên bản production ngày 14/10/2026, trước và sau một đợt tối ưu:
| Chỉ số | Trước | Sau |
|---|---|---|
| Performance mobile | 58 | 71 |
| LCP mobile | 8,3 s | 5,4 s |
| FCP mobile | 6,0 s | 3,1 s |
| Dung lượng trang | 1.129 KiB | 604 KiB |
| Accessibility | 96 | 100 |
| SEO | 66 | 100 |
| Performance desktop | 84 | 95 |
| LCP desktop | 2,3 s | 1,4 s |
Đợt này gồm: font tự host, bỏ CSS bên thứ ba, hiệu ứng đầu trang không che ảnh LCP, intro chỉ chạy trên desktop, và các sửa lỗi CLS.
Dù vậy, 71 điểm vẫn chưa đẹp, và LCP 5,4 s còn xa mốc 2,5 s mà web.dev coi là tốt (tính ở 75% lượt xem). Các phần dưới, mình đi qua từng việc đã làm và chỉ ra việc nào vẫn chưa đủ.
1. Bỏ CSS chặn render từ bên ngoài
Việc đầu tiên mình xử lý là CSS. Trước đợt này, trang tải CSS từ domain khác (font và một file CSS icon). CSS chặn render: trình duyệt chờ các file đó trước khi vẽ chữ đầu tiên.
Mình chuyển font sang next/font/google: font được tải về lúc build và phục vụ cùng site, không còn request sang domain khác. Icon chuyển thành SVG nội tuyến (bộ Phosphor). FCP mobile giảm từ 6,0 s xuống 3,1 s trong cùng đợt.
2. Ảnh LCP phải hiện từ khung đầu tiên
Sau CSS, mình quay sang ảnh. Trên trang chủ, ảnh lớn ở đầu trang là phần tử LCP. Nếu hiệu ứng mở đầu che hay cắt ảnh này, LCP chỉ được tính khi ảnh lộ ra. Vì thế mình đặt một luật: hiệu ứng được di chuyển hoặc phóng ảnh LCP, nhưng không được cắt hay ẩn nó.
Đi kèm luật đó là ba quy tắc về ảnh; có cái mình làm trước, có cái làm sau đợt đo ở trên:
- Ảnh LCP không bao giờ
loading="lazy". Ở bài nhật ký, ảnh báo cáo từng bị lazy-load; mình bỏ lazy và thêmfetchPriority="high"trong lần deploy sau đợt đo. - Mọi ảnh có
srcsetvàsizes, với các bản WebP-800và-560, để điện thoại không tải ảnh 1.200 px. - Chỉ ảnh dưới màn hình đầu tiên mới dùng
loading="lazy".
Vì là static export, next.config.js đặt images: { unoptimized: true }: không có bước tối ưu ảnh tự động của Next.js. Các bản WebP nhỏ được tạo sẵn và khai báo tay trong srcset. Ảnh nhật ký chuyển từ PNG sang WebP giảm tổng từ 2,2 MB còn 0,33 MB (việc này làm trong lần deploy trước đợt đo).
3. Chỉ chạy hiệu ứng ở nơi đáng chạy
Hiệu ứng là chỗ dễ làm chậm trang nhất, nên mình giới hạn nó lại. Intro chỉ chạy trên màn hình từ 981 px trở lên, một lần mỗi phiên, và bị bỏ qua khi người dùng bật giảm chuyển động (Reduced Motion). Trên điện thoại, nơi Lighthouse mobile đo, trang hiện ngay.
GSAP được cài từ npm và đóng gói cùng site thay vì tải từ CDN. Trong bản đang làm (chưa lên production), thẻ Google Analytics tải với strategy="lazyOnload", tức là sau khi trang tải xong.
4. Mỗi component client chỉ nhập icon nó dùng
Lỗi tiếp theo khó thấy hơn nhiều. Thanh menu là component client. Theo ghi chú trong repo, khi nó nhập cả bộ đăng ký icon, mọi trang mang thêm khoảng 40 kB JavaScript. Bây giờ component client chỉ nhập đúng vài icon nó vẽ từ @phosphor-icons/react/ssr, còn bộ icon đầy đủ ở lại phía server.
5. Đổi font mà không làm xô trang (CLS)
Đến đây tốc độ đã khá hơn, nhưng trang vẫn còn bị xô. CLS đo mức bố cục bị xô khi trang đang tải, và mình gặp hai nguồn xô:
- Font dự phòng khác cỡ font thật. Tiêu đề lớn đầu trang được chia sẵn thành ba dòng cố định, không cho xuống dòng tự do, nên khi font thật thay font dự phòng, số dòng không đổi và trang không bị đẩy xuống. Các nhãn nhỏ cũng không được xuống dòng.
- Một tùy chọn build tưởng là tối ưu. Ngày 01/11/2026 mình thử bật
experimental.inlineCsscủa Next.js để nhúng CSS thẳng vào HTML, bỏ một request. Tài liệu gọi đây là tính năng thử nghiệm, và ghi rằng nó có lợi nhất cho CSS dạng atomic như Tailwind. Trên bản build local, trang nhật ký mobile bị CLS 0,203 ở 6 trên 6 lần tải có giả lập mạng chậm; tắt tùy chọn thì 0 trên 6 lần. Mình tắt nó và ghi lý do ngay trongnext.config.jsđể người sau không bật lại.
Đo nhiều lần, vì một lần chạy dễ cho số may mắn
Bài học ở phần trên dẫn mình tới thói quen đo nhiều lần. Trang hướng dẫn đầu tiên của mình được đo trên bản build local ngày 01/11/2026, Lighthouse 12.6.1 mobile, ba lần: performance 91, 90, 91; LCP 3,2 s; CLS gần 0; accessibility, best practices và SEO đều 100. Cùng ngày, sau khi chỉ sửa thẻ tiêu đề, mô tả và robots, ba lần đo lại cho 88, 87, 87 và LCP 3,7 s. Không có CSS hay JavaScript nào đổi giữa hai đợt.

npx serve out), không phải bản production ngày 14/10. Chạy lại vài lần, điểm Performance sẽ dao động.Mình chưa tìm ra nguyên nhân, và cách đọc hợp lý nhất là độ dao động của phép đo trên cùng một máy. Đó cũng là lý do không tin một lần chạy. Số local thường đẹp hơn production vì không có độ trễ mạng thật. Mình sẽ đo lại sau khi trang lên site.
Khi số đo không như mong đợi
Trong lúc tối ưu, mình vấp vào ba tình huống mà bạn cũng có thể gặp với một site Next.js static export.
Điểm Lighthouse nhảy lên xuống dù không sửa gì
Đây là chuyện bình thường. Tài liệu Lighthouse về độ dao động liệt kê nhiều nguồn: mạng, phần cứng máy đo, tiến trình khác đang chạy, và cả chính trình duyệt. Mình chạy ít nhất ba lần, tắt extension (dùng cửa sổ ẩn danh), so trung vị chứ không so một lần chạy đẹp nhất.
next build báo lỗi khi bật output: 'export'
Static export không có server Node, nên một số tính năng không dùng được: rewrites, redirects, headers trong next.config.js, Server Actions, ISR, route động thiếu generateStaticParams(), và tối ưu ảnh bằng loader mặc định của next/image. Với ảnh, mình tự xuất WebP nhiều kích thước rồi dùng thẻ img với srcset; header cache thì đặt ở Caddy.
CLS tăng sau khi bật một tùy chọn "tăng tốc"
Mình gặp đúng chuyện này với experimental.inlineCss: CLS lên 0,203 ở cả sáu lần đo, như mình kể ở phần trên. Bài học là đo lại cả bốn chỉ số sau mỗi thay đổi, không chỉ nhìn Performance.
Việc còn dang dở
Dù đã làm khá nhiều, LCP mobile trên trang chủ vẫn trên 2,5 s. web.dev chia LCP thành bốn phần:
Bước tiếp theo của mình là đo từng phần trên production thay vì đoán, rồi mới quyết định giảm cỡ ảnh hay đổi cách cache. Mình cũng làm các sản phẩm của studio theo cùng cách: Glubbo, Roastior, Night Paws và Scovidex.
- Dung lượng trang mobile giảm gần một nửa: 1.129 KiB còn 604 KiB.
- LCP mobile giảm 2,9 giây, từ 8,3 s xuống 5,4 s, vẫn chưa đạt mốc 2,5 s.
- experimental.inlineCss làm CLS 0,203 ở 6 trên 6 lần đo, nên mình đã tắt nó.
- Ảnh LCP không bao giờ lazy-load, và hiệu ứng không được che nó.
Đọc tiếp
Claude CodeClaude Code + Remotion: Từ Prompt ra Video MP4Remotion biến component React thành video, Claude Code viết và sửa nó bằng chat. Quy trình 4 cấp độ, cách chọn phong cách và 9 lỗi cần kiểm trước khi render.→
Claude CodeHook Claude Code: Vì Sao Exit 1 Không Chặn ĐượcHook PreToolUse chặn rm -rf trước khi Claude Code chạy. Cấu hình, script Node đã thử với 12 lệnh mẫu, và 3 hiểu lầm làm hook tưởng chặn mà không chặn.→
Claude CodeQuảng Cáo Giảm 65% Token, Đo Thật Chỉ 8,5%Một skill hứa giảm 65% token, phép đo của JetBrains chỉ thấy 8,5%. Các cách miễn phí có sẵn trong Claude Code, và cách đọc phép đo trước khi cài công cụ ngoài.→