Lê Ngọc Tú← Về trang chủ

Đăng ngày 06/11/2026

Từ 60 lên 99 điểm Lighthouse trên mobile: mình đã sửa những gì và đo ra sao

Ghi lại cách giảm giật trang portfolio Next.js trên điện thoại: nguyên nhân thật, từng thay đổi, số đo trước và sau, và cả những lần sửa sai.

  • Hiệu năng
  • Next.js
  • Lighthouse
  • GSAP
Từ 60 lên 99 điểm Lighthouse trên mobile: mình đã sửa những gì và đo ra sao

Trang portfolio này chạy Next.js 16, React 19, Tailwind v4 và GSAP. Khi mình chạy PageSpeed Insights trên điện thoại, kết quả là Performance 60, Total Blocking Time 1.360 ms, Speed Index 4,4 giây, 46 hoạt ảnh không được composite và khoảng 859 KiB ảnh có thể tiết kiệm. Cuộn trên điện thoại thật cũng bị giật. Bài này ghi lại mình đã tìm nguyên nhân và sửa như thế nào, kèm số đo của từng bước.

Cách đo

Mình đo trên bản production chạy local bằng next start, dùng Lighthouse 13.6.0 với giả lập mobile 412×823, DPR 1,75, mạng Slow 4G mô phỏng và CPU chậm 4 lần. Ngoài ra mình viết thêm một script điều khiển Chrome qua giao thức DevTools để cuộn trang và đo khoảng cách giữa các frame, CLS và long task.

Có hai lưu ý để đọc số cho đúng. Thứ nhất, số đo local không bằng PageSpeed Insights: cùng một phiên bản cũ, local ra 85 đến 86 điểm còn PSI ra 60, vì máy chạy khác nhau. Vì vậy so sánh trước và sau phải làm trên cùng một môi trường. Thứ hai, mình từng bị một lần đo sai: server cũ còn chạy từ bản build trước, trả về trang lỗi, và Lighthouse vẫn chấm 100 điểm cho trang lỗi đó. Trước khi tin một con số, hãy kiểm tra trang được đo có đúng là trang mình định đo.

Những nguyên nhân tìm ra

Chữ ánh kim chạy mãi. Hiệu ứng chữ chuyển màu animate thuộc tính background-position, loại thuộc tính phải vẽ lại mỗi frame và không chạy được trên GPU. Nó chiếm 44 trong 46 hoạt ảnh không composite, và vẫn chạy cả khi phần chữ đã cuộn ra khỏi màn hình. Cách sửa: mặc định chữ đứng yên, chỉ chạy hiệu ứng trên màn hình từ 768px có chuột và người dùng không bật giảm chuyển động, và chỉ chạy cho phần tử đang nằm trong khung nhìn nhờ một IntersectionObserver bật tắt một lớp CSS.

Canvas vẽ lại mỗi frame. Một canvas lưới hình học vẽ lại liên tục, chiếm khoảng 2 giây trên tổng 7 giây CPU khi giả lập CPU chậm 4 lần. Trên màn hình nhỏ giờ nó chỉ vẽ một frame tĩnh.

Vầng sáng nền làm mờ bằng bộ lọc nặng. Các khối màu nền dùng filter: blur(125px) kèm tween GSAP lặp vô hạn. Mình thay bộ lọc bằng gradient hình tròn, chỉ chạy tween trên màn hình từ 1024px, và tạm dừng khi khối đó ra khỏi khung nhìn.

Màn hình chờ làm trang trông chậm. Lớp chờ nằm sẵn trong HTML từ máy chủ và chỉ biến mất sau khi trang hydrate, nên ảnh chụp tiến trình đầu tiên của Lighthouse là màn hình chờ và Speed Index bị kéo lên. Trên mobile mình bỏ hẳn, trên máy tính rút còn khoảng 1,4 giây.

Băng chữ chạy tạo tween mỗi lần cuộn. Mỗi nhịp cuộn tạo một tween mới. Mình chuyển sang gsap.quickTo và tạm dừng khi ngoài khung nhìn. Ngoài ra mình bỏ backdrop-filter và đổ bóng của các hình trang trí trên mobile.

Ảnh

Mình đổi 25 ảnh PNG sang WebP, tổng dung lượng giảm từ khoảng 1.466 KB xuống khoảng 430 KB, và kiểm tra độ giống bằng PSNR, trường hợp xấu nhất vẫn ở mức 37 dB. Sau đó mình thu nhỏ ba ảnh quá khổ, trong đó có ảnh chân dung kích thước 819×1456 nhưng chỉ hiển thị ở 34×34 trên thanh menu, tiết kiệm thêm khoảng 310 KiB. Cuối cùng cho ảnh dưới màn hình đầu tiên tải lười.

Một lần sửa sai

Tải lười ảnh dự án lúc đầu làm CLS tăng lên 0,102 và chiều cao trang bị co lại lúc mới tải. Lý do là ảnh chưa tải thì không giữ chỗ: lớp CSS w-auto ghi đè thuộc tính width, và khung bao ngoài co theo nội dung nên có chiều rộng bằng không. Mình sửa bằng cách ghi sẵn kích thước thật của từng ảnh trong một bảng sinh tự động (script npm run image-sizes đọc phần đầu tệp PNG và WebP), rồi đặt chiều rộng cố định kèm aspect-ratio. Sau đó mình đo chiều cao các mục lúc mới tải và sau khi cuộn hết trang: hai số khớp nhau.

Kết quả

MốcPerformanceTBTGhi chú
Ban đầu (local)85 đến 86360 đến 410 ms46 hoạt ảnh không composite, 27 thẻ preload ảnh, 1.156 KiB tải về
Sau vòng sửa hoạt ảnh92 đến 94210 đến 260 ms0 hoạt ảnh không composite, Speed Index 4,0 đến 4,7 giây
Sau đổi WebP và danh sách tĩnh97 đến 98100 đến 130 msCLS bằng 0
Sau tối ưu ảnh và tải lười99 (cả 4 lần đo)100 đến 140 msSpeed Index 2,0 đến 2,1 giây, tổng 531 KiB, còn 2 thẻ preload ảnh

Về độ mượt khi cuộn trên mobile giả lập: bản đầu chỉ đạt 34 đến 50 khung hình mỗi giây với 27 đến 138 frame giật; bản cuối đạt khoảng 98 khung hình mỗi giây với 0 đến 2 frame giật và CLS khi cuộn bằng 0.

Những gì mình rút ra

Đo trước khi sửa. Mình đoán được nghi phạm từ đọc mã, nhưng chỉ khi đếm hoạt ảnh theo từng thuộc tính mới biết một lớp chữ ánh kim chiếm gần hết con số 46.

Hoạt ảnh lặp vô hạn cần điều kiện để bật: loại thiết bị, kích thước màn hình và việc phần tử có đang nằm trong khung nhìn hay không. Một hiệu ứng đẹp trên máy tính có thể là gánh nặng trên điện thoại tầm trung.

Tải lười chỉ an toàn khi đã giữ chỗ cho nội dung. Không có kích thước ghi sẵn thì mỗi ảnh vừa tải xong sẽ đẩy cả trang xuống.

Có những chỗ nên bỏ tính năng thay vì cố tối ưu. Mục thành tựu trên điện thoại từng là một cảnh 3D ghim khung và gắn theo thanh cuộn; mình đổi thành danh sách hiển thị đầy đủ các chặng, vừa mượt hơn vừa dễ đọc hơn.

Hạn chế cần nói rõ

Nếu bạn đang tối ưu một trang có nhiều hiệu ứng, thứ tự mình khuyên là: đếm hoạt ảnh không composite, tắt những hoạt ảnh chạy ngoài khung nhìn, rồi mới đến ảnh. Mình rất muốn nghe cách bạn đo và sửa những trang tương tự.

Tác giả

Lê Ngọc Tú, AI Engineer. Viết lại những gì mình đã làm với RAG, AI Agent và mô hình tiếng Việt.

Muốn trao đổi về bài viết này?

Nếu bạn có câu hỏi hoặc góp ý, hãy liên hệ trực tiếp.