- Nguyên tắc vàng trước khi lazy load
- Markup chuẩn: tách URL thật sang data-*
- Script Intersection Observer đa năng
- Style nhỏ để chuyển cảnh mượt
- Tích hợp WordPress: enqueue script và tự động “chuẩn hoá” ảnh trong nội dung
- Lazy background-image trong block/shortcode
- Lazy iframe YouTube kèm tương tác
- Best practices tinh chỉnh IO
- Kiểm thử và đo lường
- Kết luận
Nguyên tắc vàng trước khi lazy load
- Không lazy ảnh hero/LCP (trên màn hình đầu tiên). Hãy đặt
fetchpriority="high",decoding="async", và preload font/critical CSS. - Lazy các phần tử ngoài viewport: ảnh bài viết dài, gallery, iframe nhúng (YouTube, Maps), video poster, background-image.
- Prefetch hợp lý: dùng
rootMarginđể tải sớm trước khi người dùng cuộn tới (ví dụ300px). - Có fallback nếu trình duyệt không hỗ trợ IO, và luôn thêm
<noscript>cho SEO.
Markup chuẩn: tách URL thật sang data-*
<!-- Ảnh tiêu chuẩn -->
<img
class="lazy"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='9' viewBox='0 0 16 9'%3E%3C/svg%3E"
data-src="https://cdn.example.com/img/post-1234-800.jpg"
data-srcset="https://cdn.example.com/img/post-1234-400.jpg 400w, https://cdn.example.com/img/post-1234-800.jpg 800w, https://cdn.example.com/img/post-1234-1200.jpg 1200w"
data-sizes="(max-width: 768px) 100vw, 768px"
alt="Mô tả ảnh"
width="800" height="450"
decoding="async"
/>
<noscript>
<img src="https://cdn.example.com/img/post-1234-800.jpg" alt="Mô tả ảnh" width="800" height="450" />
</noscript>
<!-- Iframe YouTube -->
<div class="lazy-embed" data-embed="youtube" data-src="https://www.youtube.com/embed/VIDEO_ID?rel=0">
<button class="lazy-embed-play" aria-label="Phát video">Phát video</button>
</div>
<!-- Phần tử có background-image -->
<div class="lazy-bg" data-bg="https://cdn.example.com/img/hero-1600.jpg"></div>
Script Intersection Observer đa năng
Script này hỗ trợ ảnh, srcset, iframe và background-image. Bạn có thể đặt trong file JS riêng và enqueue trong WordPress.
/**
* Lazy Loader sử dụng Intersection Observer
* - Ảnh: data-src, data-srcset, data-sizes
* - Iframe: data-src
* - Background: data-bg
*/
(function() {
var supportsIO = 'IntersectionObserver' in window;
var throttleRAF;
function revealImage(el) {
if (el.dataset.src) el.src = el.dataset.src;
if (el.dataset.srcset) el.srcset = el.dataset.srcset;
if (el.dataset.sizes) el.sizes = el.dataset.sizes;
el.removeAttribute('data-src');
el.removeAttribute('data-srcset');
el.removeAttribute('data-sizes');
el.addEventListener('load', function() {
el.classList.add('is-loaded');
}, { once: true });
}
function revealIframe(el) {
if (el.dataset.src) el.src = el.dataset.src;
el.removeAttribute('data-src');
el.classList.add('is-loaded');
}
function revealBg(el) {
var url = el.dataset.bg;
if (!url) return;
var img = new Image();
img.onload = function() {
el.style.backgroundImage = 'url("' + url + '")';
el.classList.add('is-loaded');
};
img.src = url;
el.removeAttribute('data-bg');
}
function onIntersect(entries, observer) {
entries.forEach(function(entry) {
if (!entry.isIntersecting) return;
var el = entry.target;
if (el.tagName === 'IMG') {
revealImage(el);
} else if (el.classList.contains('lazy-bg')) {
revealBg(el);
} else if (el.tagName === 'IFRAME') {
revealIframe(el);
} else if (el.classList.contains('lazy-embed')) {
hydrateEmbed(el);
}
observer.unobserve(el);
});
}
function hydrateEmbed(wrapper) {
// Ví dụ YouTube: tạo iframe khi người dùng bấm, hoặc auto khi vào viewport
var type = wrapper.dataset.embed;
var src = wrapper.dataset.src;
var button = wrapper.querySelector('.lazy-embed-play');
function buildIframe() {
var iframe = document.createElement('iframe');
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('loading', 'lazy');
iframe.setAttribute('title', 'Video');
iframe.src = src;
wrapper.innerHTML = '';
wrapper.appendChild(iframe);
wrapper.classList.add('is-loaded');
}
if (button) {
button.addEventListener('click', buildIframe, { once: true });
} else {
buildIframe();
}
}
function fallbackLazy() {
// Fallback cho trình duyệt không có IO: degrade gracefully
var lazyImgs = document.querySelectorAll('img.lazy[data-src], img[data-srcset], iframe[data-src], .lazy-bg[data-bg], .lazy-embed[data-src]');
var loadAll = function() {
if (throttleRAF) cancelAnimationFrame(throttleRAF);
throttleRAF = requestAnimationFrame(function() {
var vh = window.innerHeight, vw = window.innerWidth;
lazyImgs.forEach(function(el) {
var rect = el.getBoundingClientRect();
if (rect.bottom >= -300 && rect.top <= vh + 300 && rect.right >= -300 && rect.left <= vw + 300) {
if (el.tagName === 'IMG') revealImage(el);
else if (el.classList.contains('lazy-bg')) revealBg(el);
else if (el.tagName === 'IFRAME') revealIframe(el);
else if (el.classList.contains('lazy-embed')) hydrateEmbed(el);
}
});
});
};
['scroll','resize','orientationchange','load'].forEach(function(evt){
window.addEventListener(evt, loadAll, { passive: true });
});
loadAll();
}
function init() {
// Đừng lazy ảnh LCP: selector dưới bỏ qua ảnh có data-lcp hoặc .no-lazy
var candidates = document.querySelectorAll('img.lazy:not(.no-lazy):not([data-lcp]), img[data-srcset], iframe[data-src], .lazy-bg[data-bg], .lazy-embed[data-src]');
if (supportsIO) {
var io = new IntersectionObserver(onIntersect, {
root: null,
rootMargin: '300px 0px', // Prefetch trước 300px
threshold: 0.01
});
candidates.forEach(function(el){ io.observe(el); });
} else {
fallbackLazy();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Style nhỏ để chuyển cảnh mượt
img.lazy { filter: blur(8px); transition: filter .3s ease; }
img.lazy.is-loaded { filter: blur(0); }
.lazy-bg { background-size: cover; background-position: center; min-height: 240px; }
.lazy-bg.is-loaded { transition: opacity .3s ease; opacity: 1; }
Tích hợp WordPress: enqueue script và tự động “chuẩn hoá” ảnh trong nội dung
Đoạn dưới enqueue file JS và filter nội dung để chuyển img sang lazy markup (đơn giản hoá, có thể mở rộng cho srcset).
<?php
// functions.php
add_action('wp_enqueue_scripts', function() {
wp_enqueue_script('io-lazy', get_template_directory_uri() . '/assets/js/io-lazy.js', [], '1.0', true);
});
// Biến đổi <img> trong the_content thành lazy
add_filter('the_content', function($content) {
if (is_admin() || wp_doing_ajax()) return $content;
// Thay src="..." thành placeholder + data-src
$content = preg_replace_callback('#<img([^>]+)src=([\'"])([^\'"]+)\\2([^>]*)>#i', function($m){
$attrsBefore = $m[1];
$quote = $m[2];
$src = $m[3];
$attrsAfter = $m[4];
// Bỏ qua ảnh có class no-lazy hoặc ảnh trong phần đầu trang
if (strpos($attrsBefore . $attrsAfter, 'no-lazy') !== false) {
return '<img' . $attrsBefore . 'src=' . $quote . esc_url($src) . $quote . $attrsAfter . '>';
}
$placeholder = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E";
// Thêm class lazy nếu chưa có
if (strpos($attrsBefore . $attrsAfter, 'class=') === false) {
$attrsBefore .= ' class="lazy"';
} else {
$attrsBefore = preg_replace('/class=([\'"])(.*?)\\1/i', 'class=$1$2 lazy$1', $attrsBefore, 1);
}
// Đưa src thật vào data-src
$new = '<img' . $attrsBefore . ' src=' . $quote . esc_url($placeholder) . $quote . ' data-src=' . $quote . esc_url($src) . $quote . $attrsAfter . '>';
// Bổ sung noscript cho SEO/crawler
$new .= '<noscript><img' . $m[1] . 'src=' . $quote . esc_url($src) . $quote . $m[4] . '></noscript>';
return $new;
}, $content);
return $content;
}, 12);
Lazy background-image trong block/shortcode
Với các section hero/gợi ý dùng background, bạn có thể dùng lớp lazy-bg và thuộc tính data-bg, script IO ở trên sẽ tự tải:
<section class="teaser lazy-bg" data-bg="https://cdn.example.com/cover-1600.jpg">
<div class="container">Nội dung...</div>
</section>
Lazy iframe YouTube kèm tương tác
Để tối ưu thêm, chỉ tạo iframe khi người dùng bấm “Phát”:
<div class="lazy-embed" data-embed="youtube" data-src="https://www.youtube.com/embed/VIDEO_ID?rel=0">
<button class="lazy-embed-play">Phát video</button>
</div>
Best practices tinh chỉnh IO
- rootMargin: tăng prefetch nếu ảnh nặng, ví dụ
600pxcho gallery. - threshold: chỉ cần chạm một chút là đủ (
0.01) để khởi tải. - Batch: để browser tự tối ưu, observer chung cho nhiều phần tử là đủ, không cần một observer/element.
- SEO: luôn có
<noscript>và điền đủalt,width,height. - Đừng lazy hero: đặt
class="no-lazy",fetchpriority="high", có thể thêmpreloadnếu ảnh thật sự quan trọng.
Kiểm thử và đo lường
- Dùng Lighthouse/CrUX để xem LCP, CLS, TBT; lazy load đúng sẽ cải thiện TBT và giảm băng thông.
- Kiểm tra thiết bị chậm/thấp mạng: tăng
rootMarginđể tải sớm hơn. - Đếm số request tiết kiệm: so sánh trước–sau khi triển khai.
Kết luận
Intersection Observer giúp bạn đưa lazy load lên “level kiểm soát”: chỉ tải khi cần, tải sớm vừa đủ trước khi người dùng nhìn thấy, và giữ nguyên trải nghiệm SEO. Với vài đoạn JS/PHP gọn gàng như trên, bạn có thể áp dụng đồng loạt cho ảnh, iframe và background-image trong WordPress mà không chạm vào nội dung gốc của tác giả.
Bình luận