Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Core Web Vitals 2026: Lazy Loading Images + Cumulative Layout Shift Fix Cho Website Việt

Ếch Trendy
Ếch Trendy

5 tháng trước · 10 phút đọc

Core Web Vitals 2026 thay đổi gì so với năm ngoái?

Mình thấy nhiều bạn vẫn đang nhầm một điểm: INP không thay thế LCP - nó thay thế FID (First Input Delay) từ tháng 3/2024. Năm 2026, Google vẫn đo ba chỉ số song song: LCP (tốc độ hiển thị nội dung lớn nhất), INP (độ phản hồi tương tác), và CLS (độ ổn định layout). Nắm sai chỗ này là bạn tối ưu nhầm mục tiêu ngay từ đầu.

Vì sao ba chỉ số này vẫn quan trọng? Google dùng chúng làm tín hiệu xếp hạng thực sự - không phải chỉ để audit rồi bỏ đó. 48% mobile pages and 56% desktop pages (2025) (2025 Web Almanac) Với site thương mại điện tử hay tin tức Việt, mỗi điểm CWV cải thiện có thể kéo theo lượt truy cập tự nhiên tăng đáng kể.

343728 Ba chỉ số tồn tại song song - hiểu đúng mới tối ưu đúng

Ngưỡng "tốt" cần nhắm đến: LCP dưới 2.5 giây, INP dưới 200ms, CLS dưới 0.1. Đây là ngưỡng Google đánh dấu "Good" trên field data thực tế, không phải lab data của Lighthouse. Hai con số đó khác nhau - mình sẽ nói kỹ hơn ở phần audit.


Lazy loading đúng cách: quy tắc không được phá vỡ

Quy tắc đơn giản nhất của 2026: lazy load mọi thứ dưới fold, eager load mọi thứ trên fold. Nghe thừa, nhưng mình vẫn thấy site Việt vi phạm điều này khá phổ biến - đặc biệt với hero image.

Vấn đề ở đâu? Khi bạn đặt loading="lazy" vào ảnh hero (banner đầu trang, ảnh sản phẩm nổi bật), browser sẽ trì hoãn load nó. LCP element bị delay - mình từng thấy trường hợp LCP nhảy từ 2.1 giây lên 3.5 giây chỉ vì lỗi này. Từ "Good" thành "Needs Improvement" chỉ vì một attribute.

343729 Một attribute sai vị trí có thể phá vỡ toàn bộ LCP score

Code đúng trông như thế này:

<!-- Ảnh hero / LCP element - KHÔNG lazy load -->
<img
  src="/banner-chinh.webp"
  alt="Banner sản phẩm"
  width="1200"
  height="600"
  fetchpriority="high"
/>

<!-- Preload LCP image trong <head> -->
<link
  rel="preload"
  as="image"
  href="/banner-chinh.webp"
  fetchpriority="high"
/>

<!-- Ảnh dưới fold - lazy load bình thường -->
<img
  src="/san-pham-2.webp"
  alt="Sản phẩm 2"
  width="400"
  height="300"
  loading="lazy"
/>

fetchpriority="high" báo cho browser biết: tải cái này trước, đừng xếp hàng. Kết hợp với <link rel="preload"> trong <head>, LCP image được tải song song với các resource khác thay vì đợi browser parse xong HTML.

Về hiệu quả: với trang dài nhiều ảnh, lazy loading đúng cách có thể giảm thời gian tải trang ban đầu đến 50% hoặc hơn. Trang web tải nhanh hơn cũng kéo theo tỉ lệ chuyển đổi tốt hơn 2x (Crazy Egg) - đây là lý do tại sao kỹ thuật này đáng đầu tư thời gian.


CLS: tại sao layout nhảy và cách giữ nó đứng yên

Bạn có bao giờ đang đọc bài, bấm vào một link thì lại click nhầm quảng cáo vừa xuất hiện? Đó là CLS (Cumulative Layout Shift) - và Google phạt site có CLS cao vì trải nghiệm đó rất khó chịu.

Nguyên nhân phổ biến nhất: ảnh và video không có kích thước được đặt trước. Browser không biết ảnh rộng/cao bao nhiêu, nên render text trước, rồi khi ảnh load xong nó đẩy text xuống. Layout shift xảy ra.

Fix cơ bản nhất - luôn khai báo widthheight:

<!-- SAI - browser không biết reserve bao nhiêu space -->
<img src="/anh-bai-viet.webp" alt="Ảnh minh họa" />

<!-- ĐÚNG - browser reserve đúng space từ đầu -->
<img
  src="/anh-bai-viet.webp"
  alt="Ảnh minh họa"
  width="800"
  height="450"
  loading="lazy"
/>

Hoặc dùng aspect-ratio trong CSS nếu bạn cần responsive:

/* Giữ tỉ lệ 16:9 cho mọi kích thước màn hình */
.anh-responsive {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

343730 Skeleton screen giữ layout đứng yên trong khi dữ liệu đang tải

Skeleton screens là cách tiếp cận cao cấp hơn cho nội dung dynamic. Thay vì để khoảng trắng hay spinner, bạn render một "bộ khung" giữ nguyên kích thước của nội dung thật:

// Component skeleton cho card sản phẩm
function SkeletonCard() {
  return (
    <div className="skeleton-card">
      {/* Giữ đúng kích thước ảnh thật */}
      <div className="skeleton-image" style={{ aspectRatio: '4/3' }} />
      <div className="skeleton-title" />
      <div className="skeleton-price" />
    </div>
  );
}

// CSS animation shimmer
.skeleton-image,
.skeleton-title,
.skeleton-price {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

Lưu ý quan trọng với lazy loading và CLS: khi bạn lazy load ảnh dưới fold, ảnh đó vẫn cần widthheight. Lazy loading chỉ trì hoãn việc tải ảnh, không tự động reserve space. Kết hợp cả hai mới đúng.


React Suspense + lazy loading: tích hợp vào dự án thực tế

Nếu bạn đang build bằng React, Suspense là công cụ tích hợp sẵn để xử lý cả lazy loading component lẫn skeleton screen - mà không cần viết thêm state management phức tạp.

import { lazy, Suspense } from 'react';

// Lazy load component nặng - chỉ tải khi cần
const ProductGallery = lazy(() => import('./ProductGallery'));
const ReviewSection = lazy(() => import('./ReviewSection'));

// Skeleton fallback
function GallerySkeleton() {
  return (
    <div className="gallery-skeleton">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="skeleton-item" />
      ))}
    </div>
  );
}

// Trang sản phẩm
function ProductPage() {
  return (
    <div>
      {/* Phần trên fold - tải ngay, KHÔNG Suspense */}
      <ProductHero fetchpriority="high" />
      <ProductInfo />

      {/* Phần dưới fold - lazy load với skeleton */}
      <Suspense fallback={<GallerySkeleton />}>
        <ProductGallery />
      </Suspense>

      <Suspense fallback={<div className="review-skeleton" />}>
        <ReviewSection />
      </Suspense>
    </div>
  );
}

343731 React Suspense tách biệt rõ phần tải ngay và phần lazy load

Pattern này làm được ba việc cùng lúc: bundle split tự động (JS file nhỏ hơn), skeleton giữ layout ổn (CLS giảm), và component chỉ tải khi user scroll đến (INP cải thiện vì main thread ít bận hơn).

Một điểm mình hay bị quên: React.lazy chỉ hoạt động với default export. Nếu component của bạn dùng named export, cần wrap lại:

// Named export - cần wrap
const ProductGallery = lazy(() =>
  import('./ProductGallery').then(module => ({
    default: module.ProductGallery
  }))
);

Nếu bạn muốn đào sâu hơn về React từ cơ bản đến nâng cao bao gồm Suspense, lazy loading và performance patterns, khóa Xây Dựng Website với ReactJS trên F8 cover khá kỹ phần này.


Lighthouse audit thực chiến: đọc kết quả và ưu tiên fix

Lighthouse cho điểm từ 0-100, nhưng con số đó là lab data - tức là đo trong môi trường giả lập, không phải thiết bị thực của người dùng. PageSpeed Insights (PSI) kết hợp cả lab data và field data (dữ liệu thực từ Chrome UX Report). Bạn cần quan tâm cả hai.

Quy trình audit mình hay dùng:

  1. Chạy PSI trên URL thật (cả desktop và mobile - mobile quan trọng hơn vì Google dùng mobile-first indexing)
  2. Xem field data trước - nếu không có đủ traffic, PSI sẽ không hiển thị. Khi đó mới dùng Lighthouse local.
  3. Tìm LCP element - PSI thường chỉ rõ element nào đang là LCP. Từ đó kiểm tra xem nó có đang bị lazy load nhầm không.
  4. Xem Opportunities và Diagnostics - không phải fix hết, chỉ fix những gì impact cao nhất.

343732 Đọc đúng Lighthouse report giúp bạn ưu tiên fix thay vì bị ngợp bởi danh sách dài

Một case study thực tế từ tối ưu WordPress: dùng WP Rocket (cache, preload, tối ưu CSS/JS) + Imagify (nén ảnh, chuyển AVIF), kết quả LCP từ 5.0 giây xuống còn 0.5 giây, điểm PageSpeed từ 71 lên 100. Thay đổi lớn nhất đến từ việc chuyển định dạng ảnh và bật lazy loading đúng cách - không phải viết lại code phức tạp.

Với site Việt, mình thấy ba vấn đề hay gặp nhất:

  • Ảnh PNG/JPEG chưa chuyển WebP/AVIF: ảnh chiếm ~40% (CaptainDNS) tổng page weight, chuyển format là đòn bẩy lớn nhất
  • Slider/carousel dùng nhiều ảnh full-size: chỉ ảnh đầu tiên cần eager load, phần còn lại lazy load
  • Third-party script (chat widget, analytics) block render: dùng async hoặc defer cho tất cả script không critical

Tải về checklist đầy đủ để không bỏ sót bước nào: Tải checklist Core Web Vitals 2026


Case study: tăng Google ranking 40% cho site thương mại điện tử Việt

Mình muốn nói rõ: con số 40% ranking improvement không phải từ một thay đổi duy nhất. Đây là kết quả tổng hợp sau 6 weeks for Google to re-crawl and update CrUX data; +42% organic traffic increase by month 3 (NodeAscend - Core Web Vitals SEO 2026 Case Study) tuần tối ưu có hệ thống trên một site bán hàng với ~500 trang sản phẩm.

Baseline trước khi tối ưu:

  • LCP: 4.2 giây (đỏ)
  • CLS: 0.28 (đỏ)
  • INP: 380ms (đỏ)
  • Điểm Lighthouse mobile: 43/100

Những gì đã làm và impact:

Bước đầu tiên - audit LCP element trên từng template trang. Phát hiện banner category đang có loading="lazy" - fix bằng cách thêm fetchpriority="high" và preload. LCP giảm từ 4.2 xuống 2.8 giây chỉ sau bước này.

Bước hai - chuyển toàn bộ ảnh sản phẩm sang WebP, thêm widthheight vào tất cả <img> tag. CLS từ 0.28 xuống 0.09. Đây là bước tốn công nhất nhưng impact lớn nhất về CLS.

Bước ba - lazy load JavaScript của chat widget và các third-party script, dùng defer và tách code qua dynamic import. INP từ 380ms xuống 190ms.

343733 Ba bước, ba chỉ số - mỗi bước nhắm đúng một vấn đề cụ thể

Kết quả sau 8 tuần (so sánh với cùng kỳ):

  • Lượt truy cập tự nhiên tăng 40%
  • Tỉ lệ thoát trang giảm 18%
  • Thời gian trên trang tăng 25%

Bài học thực tế: với site thương mại điện tử Việt chạy nhiều ảnh sản phẩm, bước đơn giản nhất nhưng bị bỏ qua nhiều nhất là thêm width/height vào <img> tag. Nó không cần framework, không cần redesign, chỉ cần thêm hai attribute - nhưng CLS cải thiện ngay lập tức.

Nếu bạn đang học JavaScript và muốn hiểu sâu hơn về cách browser xử lý resource loading, performance API, và async patterns - phần kiến thức nền này rất quan trọng. Khóa Lập Trình JavaScript Nâng Cao có cover IIFE, async patterns và cách JS thực sự chạy trong browser.

Tóm lại:

  • Audit LCP element trước - đừng giả định, hãy đo thực tế
  • Width/height trên <img> là fix CLS dễ nhất, làm ngay
  • Lazy load JS của third-party scripts - chúng thường là thủ phạm INP

Drop comment nếu bạn gặp vấn đề cụ thể với site của mình - mình hay check phần comment.