Phase tối ưu tốc độ load toàn site (2026-10)
Ngày: 2026-10-18 · Trạng thái: PLAN — chờ user chốt mục 6
Tiền đề: PHASE-PERF-OPTIMIZATION.md (2026-06) — phần hạ tầng đã làm (kiểm tra lại mục 1.3). Plan này nhắm phần còn lại: trình duyệt + Cloudflare.
1. Đo thực tế (2026-10-18, prod)
1.1 Server rất nhanh — nút thắt không nằm ở backend
| Đo | Trên VPS (127.0.0.1) | Từ ngoài (qua Cloudflare + NPM) |
|---|---|---|
| HTML trang chủ / sản phẩm | 35–78 ms | 300–800 ms |
| API danh mục / sản phẩm / mẫu | 18–53 ms | 240–420 ms |
1.2 Trình duyệt phải tải (mobile 390px, cache trống)
| Trang | Dung lượng | Request | LCP (mạng thật) | Ghi chú |
|---|---|---|---|---|
/ |
4,4 MB (ảnh 3,9 MB) | 165 | 1,9 s | 26/26 ảnh là ảnh gốc 1.800–3.600px, hiển thị 110–360px |
/san-pham/[slug] |
2,8 MB (font mẫu 1,6 MB) | 221 | 2,1 s | 1 font mẫu 491 KB; JS route 366 KB |
/san-pham |
2,1 MB | 142 | 0,4 s | ảnh thumbnail mẫu 678 KB |
/gio-hang, /dang-nhap |
~0,45 MB | 89–122 | ~0,3 s | 18 file font, 38 chunk JS |
1.3 Lighthouse mobile (4G giả lập)
| Trang | Điểm | FCP | LCP | TBT | CLS |
|---|---|---|---|---|---|
/ |
61 | 3,8 s | 15,3 s | 180 ms | 0 |
/san-pham/thiep-cuoi-1 |
17 | 3,1 s | 11,0 s | 3.270 ms | 0,398 |
/dang-nhap |
52 | 3,6 s | 7,3 s | 390 ms | 0,156 |
1.4 Hạ tầng đã làm (plan 2026-06) — không cần làm lại
Postgres shared_buffers 2,5 GB / work_mem 16 MB / random_page_cost 1.1 ✅ · Redis maxmemory 512 MB + LRU ✅ · uvicorn 4 worker, celery 4 ✅ · NPM HTTP/2 + /_next/static cache 1 năm ✅ · nén Brotli (Cloudflare) ✅.
2. Nguyên nhân gốc (xếp theo tác động)
| # | Vấn đề | Bằng chứng | Ảnh hưởng |
|---|---|---|---|
| A | Ảnh gốc không qua tối ưu — mọi ảnh CMS / sản phẩm / mẫu là <img> thô trỏ /api/v1/cms-anh/*/file (JPEG gốc tới 2,7 MB) |
0/26 ảnh trang chủ qua /_next/image; cùng ảnh qua /_next/image?w=640 = 45 KB AVIF (nhỏ 60×) |
LCP trang chủ 15 s, 3,9 MB ảnh |
| B | Mọi khách mới tải trang 2 lần — CacheBuster (dọn SW 1 lần, 2026-06) thấy localStorage trống ⇒ location.reload() với mọi khách mới |
2 request document cho 1 lượt mở | Lần đầu vào chậm gấp đôi |
| C | Cloudflare không cache gì (cf-cache-status: DYNAMIC cho JS, font, ảnh) và ghi đè cache 1 năm của /_next/static thành 16 giờ |
header cache-control: max-age=57597 dù origin gửi immutable |
Mọi file tĩnh đi về tận VPS; khách quay lại vẫn tải lại sau 16 h |
| D | /api/auth/session gọi 14 lần / trang — apiClient gọi getSession() trước mỗi request, không cache |
14 fetch session trên /dang-nhap |
Thêm ~14 round-trip, chiếm kết nối |
| E | Trang sản phẩm nặng JS — preview tương tác (Konva) + tải toàn bộ font của mẫu (491 KB/font) ngay khi mở | TBT 3,3 s, JS exec 6,5 s, 1,6 MB font | Điểm 17, máy yếu đơ vài giây |
| F | Xô lệch bố cục (CLS) — khung preview / ảnh không giữ chỗ trước khi tải | CLS 0,398 (sản phẩm), 0,156 (đăng nhập) | Nội dung nhảy khi đang đọc / bấm |
| G | 18 file font giao diện (4 độ đậm × 3 bộ ký tự), không preload | 117–193 KB font mỗi trang | FCP chậm hơn |
3. Kế hoạch
Giai đoạn 1 — Sửa nhanh, tác động lớn (~1 ngày, chỉ frontend)
| Việc | Cách làm | Ước tính |
|---|---|---|
| B Bỏ tải lại trang với khách mới | CacheBuster: chỉ reload khi thực sự có service worker cũ / cache cũ; khách mới chỉ ghi khoá phiên bản |
15 phút |
D Cache session ở apiClient |
Ghi nhớ kết quả getSession() ~30 s (dùng chung 1 promise khi nhiều request song song); xoá khi đăng nhập/đăng xuất/401 |
45 phút + test |
A Ảnh qua /_next/image |
Helper anhToiUu(url, rong) dùng lại bộ tối ưu sẵn có (AVIF/WebP, đã cấu hình images trong next.config.ts); áp cho: thẻ danh mục, quạt ảnh hero, SanPhamCard, CarouselStrip, FeatureColumns, PromoBlocks, BannerNho, thumbnail mẫu (file-asset), giỏ hàng, gallery sản phẩm. Thêm loading="lazy" + sizes; ảnh LCP đầu trang fetchpriority="high" |
3–4 giờ |
| F Giữ chỗ chống xô lệch | aspect-ratio cố định cho khung preview sản phẩm, ảnh gallery, logo; kiểm tra lại CLS đăng nhập (nghi popup/thông báo chèn vào) |
1–2 giờ |
Kỳ vọng: trang chủ 4,4 MB → ~0,7 MB, LCP lab 15 s → ~3 s; khách mới không còn tải 2 lần; bớt ~13 request/trang.
Giai đoạn 2 — Cloudflare (cần bạn: quyền dashboard hoặc API token)
| Việc | Cài đặt |
|---|---|
| C1 Tôn trọng header cache của origin | Caching → Configuration → Browser Cache TTL = "Respect Existing Headers" |
| C2 Cache ở edge (gần khách VN) | Cache Rules: /_next/static/*, /_next/image*, /brand/*, /img/*, font → Eligible for cache, Edge TTL theo header origin; /api/v1/cms-anh/*/file → cache 1 ngày |
| C3 Không cache HTML / API động | Giữ mặc định (đang no-store) |
Kỳ vọng: file tĩnh trả từ edge Cloudflare (thường < 50 ms ở VN) thay vì về VPS; khách quay lại không tải lại JS/font.
Đổi cấu hình dịch vụ ngoài → mình không tự làm. Bạn cấp API token Cloudflare (quyền Zone → Cache Rules + Zone Settings, chỉ zone insacmauviet.vn) hoặc mình gửi hướng dẫn từng bước để bạn bấm.
Giai đoạn 3 — Trang sản phẩm nhẹ lại (~1 ngày)
| Việc | Cách làm |
|---|---|
| E1 Hiện ảnh preview tĩnh trước | Lần đầu mở: ảnh render sẵn của mẫu (đã có hinh_anh_url / thumbnail) qua /_next/image; chỉ tải Konva + preview tương tác khi khách bấm "Tuỳ chỉnh" hoặc sau khi trang rảnh (requestIdleCallback) |
| E2 Font mẫu tải theo nhu cầu | Chỉ tải font của trường đang hiển thị / đang sửa; dùng bản subset web (backend đã có cờ subset — migration 0071) thay font đủ; font-display: swap |
| E3 Tách bundle | dynamic(() => import(...), { ssr: false }) cho InteractiveTemplatePreview, BasicEditPanel, MockupPreview; kiểm tra lại JS route (366 KB → mục tiêu < 200 KB) |
Kỳ vọng: TBT 3,3 s → < 600 ms, điểm 17 → 60+.
Giai đoạn 4 — Font giao diện + dọn nhỏ (~2 giờ)
- Preload 2 file font chính (400 + 600, bộ
vietnamese+latin); cân nhắc bỏ độ đậm 500 (mapfont-medium→ 600 hoặc 400) — giảm ~25% byte font. - Bỏ request thừa:
/api/versiongọi 2 lần,/api/v1/cau-hinh-websitegọi nhiều nơi → dùng chung react-query key.
Giai đoạn 5 — Ảnh gốc nhẹ từ đầu (tuỳ chọn, backend + dữ liệu)
- Khi admin tải ảnh CMS: tự resize tối đa 2.000px + nén WebP; script một lần nén lại ảnh cũ trong MinIO (giữ bản gốc).
- Chỉ cần nếu sau GĐ1+2 vẫn thấy ảnh gốc bị tải (vd. link ảnh dùng ngoài web). Đụng dữ liệu prod → hỏi trước.
4. Đo lại & chống thoái lui
- Script đo (Playwright: dung lượng, request, ảnh quá cỡ, LCP/TTFB) + Lighthouse 3 trang → chạy trước/sau mỗi giai đoạn, ghi bảng vào file này.
- Mục tiêu (Lighthouse mobile):
/≥ 85,/san-pham/[slug]≥ 60,/dang-nhap≥ 85; CLS < 0,1; trang chủ < 1 MB. - Quy ước mới: ảnh nội dung luôn qua helper
anhToiUu(ghi vào CLAUDE.md mục cạm bẫy).
5. Thứ tự & ước lượng
| Giai đoạn | Thời gian | Phụ thuộc | Deploy |
|---|---|---|---|
| 1 Sửa nhanh (B, D, A, F) | ~1 ngày | — | riêng |
| 2 Cloudflare | 30 phút | quyền Cloudflare từ bạn | — |
| 3 Trang sản phẩm | ~1 ngày | GĐ1 | riêng |
| 4 Font + dọn nhỏ | ~2 giờ | — | gộp GĐ3 |
| 5 Nén ảnh gốc (tuỳ chọn) | ~0,5 ngày | xác nhận của bạn | riêng |
Không đổi DB schema, không thêm thư viện.
6. Cần bạn chốt
- Q1 — Cloudflare: A. Cấp API token giới hạn (mình tự cấu hình + kiểm tra) · B. Mình gửi hướng dẫn từng bước, bạn bấm (khuyên — không cần cấp quyền).
- Q2 — Giai đoạn 5 (nén lại ảnh gốc trong MinIO): làm luôn / để sau khi xem kết quả GĐ1+2 (khuyên để sau).
- Q3 — Font độ đậm 500: bỏ (nhẹ hơn ~25%, chữ "medium" hơi khác) / giữ (khuyên giữ, chỉ preload).