Bạn có bao giờ thắc mắc vì sao một số trang web tải nhanh hơn hẳn ngay cả khi có nhiều hình ảnh hay animation phức tạp? Bí quyết nằm ở lazy loading – một kỹ thuật giúp trì hoãn việc tải các tài nguyên không cần thiết, chỉ tập trung vào những phần quan trọng mà người dùng cần ngay lập tức.
Cách tiếp cận này giúp tăng tốc độ tải trang, tiết kiệm băng thông, và tối ưu hóa trải nghiệm cho người dùng, đặc biệt trên những trang web nhiều hình ảnh hoặc dữ liệu phức tạp. Trong bài viết này, mình sẽ cùng bạn tìm hiểu lazy loading là gì, vì sao nó quan trọng và cách áp dụng nó trong các dự án thực tế.
1. Lazy Loading là gì?
Lazy Loading là kỹ thuật trì hoãn việc tải hoặc khởi tạo các tài nguyên cho đến khi chúng thực sự cần thiết. Thay vì tải toàn bộ nội dung khi trang web được mở, chỉ những phần hiển thị trên màn hình mới được tải, giúp giảm thời gian tải trang và tiết kiệm băng thông.
Khi bạn build ứng dụng web, lazy loading thường được áp dụng để trì hoãn việc tải các hình ảnh, video hoặc nội dung khác cho đến khi người dùng cuộn trang đến vị trí chứa chúng. Điều này giúp giảm thời gian tải trang ban đầu và tiết kiệm băng thông, đặc biệt hữu ích cho người dùng có kết nối internet chậm.
Ví dụ: trong một website chứa nhiều hình ảnh, bạn có thể sử dụng Lazy Loading để chỉ tải những hình ảnh đang nằm trong vùng nhìn của người dùng (viewport). Khi người dùng scroll xuống, các hình ảnh mới sẽ được tải dần.
2. Lợi ích của Lazy Loading
- Tăng tốc độ tải trang: thời gian tải trang là yếu tố rất quan trọng với doanh nghiệp, nhất là khi người dùng ngày càng thiếu kiên nhẫn. Lazy loading giúp bạn giảm đáng kể thời gian tải trang bằng cách chỉ tải những gì thực sự cần thiết.
- Cải thiện SEO: Google sẽ ưu tiên các trang web tải nhanh và thân thiện với người dùng. Lazy loading không chỉ giúp trang Web nhanh hơn mà còn nâng điểm Core Web Vitals – một trong những yếu tố quyết định thứ hạng SEO của bạn.
- Tiết kiệm băng thông: không phải người dùng nào cũng có đường truyền Internet mạnh. Lazy loading đảm bảo họ không phải tải những tài nguyên không cần thiết, tiết kiệm dữ liệu và mang lại trải nghiệm mượt mà hơn.
- Cải thiện trải nghiệm người dùng: lazy loading không chỉ giúp website của bạn nhanh hơn mà còn mang lại trải nghiệm tương tác tốt hơn. Một trang web mượt mà, không giật lag sẽ luôn để lại ấn tượng tốt trong lòng người dùng, mình chắc chắn là như thế.
- Giảm tải cho server: với lưu lượng truy cập lớn, việc tải toàn bộ nội dung ngay lập tức có thể khiến server quá tải. Lazy loading giúp giảm đáng kể số lượng yêu cầu HTTP tại thời điểm tải trang.
3. Các loại Lazy Loading phổ biến
Có nhiều cách để triển khai Lazy Loading, tùy thuộc vào loại tài nguyên và công nghệ sử dụng cho dự án của bạn. Dưới đây là một số loại Lazy Loading mình thấy phổ biến, gần như dự án nào cũng sẽ cần:
3.1 Lazy Loading cho hình ảnh
- Chỉ tải hình ảnh khi chúng sắp xuất hiện trong viewport của người dùng.
- Website có nhiều hình ảnh như: blog ảnh, e-com, social,...
- Sử dụng thuộc tính loading="lazy" trong HTML5 hoặc sử dụng JavaScript để thay đổi thuộc tính src khi cần.
<img src="200Lab-logo.jpg" alt="200Lab-logo-img" loading="lazy">
Đối với HTML và React bạn có thể dùng thẻ img như trên, nhưng đối với NextJS bạn có thể sử dụng component Image import Image from 'next/image'
đã hỗ trợ Lazy Loading mặc định.
3.2 Lazy Loading cho video, iframe
- Trì hoãn việc tải video và iframe cho đến khi chúng cần được hiển thị.
- Đối với các ứng dụng cần phải nhúng video YouTube, Google Maps, hoặc các nội dung của bên thứ ba.
- Sử dụng thuộc tính loading="lazy" cho
<iframe>
hoặc<video>
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
loading="lazy"
width="560"
height="315"
frameborder="0"
allowfullscreen
/>
Với NextJS bạn có thể tạo component và sử dụng dynamic import
function LazyIframe() {
return (
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
loading="lazy"
width="560"
height="315"
frameBorder="0"
allowFullScreen
></iframe>
);
}
import dynamic from 'next/dynamic';
const LazyIframe = dynamic(() => import('../components/LazyIframe'), {
ssr: false,
loading: () => <p>Đang tải video...</p>,
});
function Page() {
return (
<div>
<h1>Video</h1>
<LazyIframe />
</div>
);
}
export default Page;
3.3 Lazy Loading cho module, component (Code Splitting)
- Chia nhỏ ứng dụng thành các module và chỉ tải chúng khi cần thiết.
- Nên sử dụng đối với các ứng dụng web single page (SPA) với nhiều chức năng phức tạp.
- Sử dụng dynamic import trong JavaScript (import()), kết hợp với các bundler như Webpack.
Cá nhân mình thường sử dụngReact.lazy()
và Suspense
đối với React
import React, { Suspense } from 'react';
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>Ứng dụng của tôi</h1>
<Suspense fallback={<div>Đang tải...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
export default App;
còn đối với NextJS có thể sử dụng next/dynamic
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Đang tải...</p>,
});
function HomePage() {
return (
<div>
<h1>Trang chủ</h1>
<HeavyComponent />
</div>
);
}
export default HomePage;
3.4 Lazy Loading cho dữ liệu
- Trì hoãn việc gọi API hoặc truy vấn cơ sở dữ liệu cho đến khi dữ liệu cần thiết.
- Thường áp dụng với các ứng dụng cần tối ưu hóa số lượng request mạng, như bảng điều khiển với nhiều widget.
- Sử dụng các kỹ thuật như GraphQL với tính năng
@defer
hoặc quản lý trạng thái ứng dụng để chỉ gọi API khi cần.
3.5 Infinite Scrolling
- Tải thêm nội dung khi người dùng cuộn đến cuối trang.
- Bạn nên áp dụng với các ứng dụng như: mạng xã hội, trang tin tức, e-com.
- Sử dụng sự kiện cuộn , kiểm tra vị trí cuộn của người dùng để tải thêm dữ liệu mới.
import React, { useState, useEffect } from 'react';
function InfiniteScroll() {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const limit = 10;
const loadMore = () => {
fetch(`/api/posts?page=${page}&limit=${limit}`)
.then((res) => res.json())
.then((newItems) => {
setItems((prevItems) => [...prevItems, ...newItems]);
setPage(page + 1);
});
};
useEffect(() => {
loadMore();
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleScroll = () => {
if (
window.innerHeight + window.scrollY >= document.body.offsetHeight - 600
) {
loadMore();
}
};
return (
<div>
{posts.map((item) => (
<div key={post.id}>{post.content}</div>
))}
</div>
);
}
export default InfiniteScroll;
Ngoài ra, bạn có thể áp dụng useSWR
import { useState, useEffect } from 'react';
import useSWR from 'swr';
function InfiniteScroll() {
const [page, setPage] = useState(1);
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const fetcher = (url) => fetch(url).then((res) => res.json());
const { data, error } = useSWR(`/api/items?page=${page}&limit=10`, fetcher, {
revalidateOnFocus: false,
});
useEffect(() => {
if (data && data.items) {
setItems((prevItems) => [...prevItems, ...data.items]);
setIsLoading(false);
}
}, [data]);
useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 &&
!isLoading
) {
setIsLoading(true);
setPage((prevPage) => prevPage + 1);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isLoading]);
if (error) return <div>Lỗi khi tải dữ liệu.</div>;
return (
<div>
{items.map((item) => (
<div key={item.id} style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>
{item.content}
</div>
))}
{isLoading && <p>Đang tải thêm dữ liệu...</p>}
</div>
);
}
export default InfiniteScroll;
Nhưng sẽ có vấn đề khi người dùng scroll với tốc độ tên lửa, lúc đó API sẽ gọi liên tục. Để tránh điều này xảy ra, bạn nên thêm debounce cho sự kiện scroll.
npm install lodash.debounce
import debounce from 'lodash.debounce';
useEffect(() => {
const handleScroll = debounce(() => {
if (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 500 &&
!isLoadingMore
) {
setIsLoadingMore(true);
setPage((prevPage) => prevPage + 1);
}
}, 200);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isLoadingMore]);
3.6 Lazy Loading Fonts
- Trì hoãn việc tải phông chữ cho đến khi cần được sử dụng.
- Bạn nên áp dụng với các ứng dụng sử dụng nhiều phông chữ tùy chỉnh nhưng không cần thiết ngay lập tức, nhưng trên thực tế mình thấy gần như ứng dụng nào cũng sẽ sử dụng dù là chỉ 1 font.
- Sử dụng font-display: swap trong CSS
@font-face {
font-family: 'Roboto';
src: url('/fonts/Roboto.woff2') format('woff2');
font-display: swap;
}
Với dự án được xây dựng bằng NextJS, bạn có thể sử dụng next/front/google
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
function App() {
return (
<div className={inter.className}>
<p>200Lab - Up.</p>
</div>
);
}
export default App;
3.7 Lazy Loading Stylesheets
- Chỉ tải các file CSS khi cần thiết.
- Phần này thì mình thấy đa số các ứng dụng bây giờ có rất nhiều CSS riêng biệt để thu hút mắt người dùng, bạn nên đặc biệt chú ý tối ưu cho phần này.
- Sử dụng JavaScript để thêm thẻ
<link>
vào DOM khi cần.
<button onclick="loadCSS()">Tải CSS</button>
<script>
function loadCSS() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';
document.head.appendChild(link);
}
</script>
Trong React bạn có thể sử dụng useEffect()
để thêm CSS khi mà component được mount.
import React, { useEffect } from 'react';
function LazyStylesComponent() {
useEffect(() => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/styles/lazy-styles.css';
document.head.appendChild(link);
}, []);
return <div className="styled-component">Đây là CSS lazy</div>;
}
export default LazyStylesComponent;
Đối với NextJS bạn có thể sử dụng dynamic import cho CSS module
import dynamic from 'next/dynamic';
const styles = dynamic(() => import('../styles/lazy-styles.module.css'));
function Page() {
return (
<div className={styles.styledComponent}>
CSS lazy
</div>
);
}
export default Page;
4. Cách thức hoạt động của Lazy Loading
Lazy Loading hoạt động bằng cách trì hoãn việc tải các tài nguyên không cần thiết ngay lập tức. Thay vào đó, nó sẽ theo dõi vị trí của người dùng trên trang và chỉ tải các tài nguyên khi chúng sắp xuất hiện trong vùng nhìn thấy (viewport). Khi tài nguyên (ví dụ: hình ảnh, video, component) gần xuất hiện trong viewport, nó sẽ được tải về và hiển thị.
Có hai cách tiếp cận chính để triển khai Lazy Loading:
- Client-side Lazy Loading: sử dụng JavaScript trên browser để kiểm tra khi nào tài nguyên cần được tải.
- Server-side Rendering (SSR) với Lazy Loading: kết hợp SSR với Lazy Loading để tối ưu hóa cả phía server và client.
4.1 Client-side Lazy Loading
Trong Client-side Lazy Loading, tất cả logic Lazy Loading được thực hiện trên trình duyệt của người dùng bằng JavaScript. Trình duyệt sẽ theo dõi vị trí của các tài nguyên trên trang và quyết định khi nào cần tải chúng dựa trên vị trí cuộn của người dùng.
Có 2 kỹ thuật phổ biến là:
- Intersection Observer API: API của JavaScript cho phép bạn phát hiện khi một phần tử xuất hiện trong viewport.
- Event Listeners cho sự kiện cuộn: sự kiện scroll để kiểm tra vị trí của tài nguyên (hình ảnh, video,...) so với viewport.
4.1.1 Sử dụng Intersection Observer API (HTML và JavaScript)
<img data-src="large-image.jpg" alt="image" class="lazy-load">
document.addEventListener("DOMContentLoaded", function () {
const lazyImages = document.querySelectorAll("img.lazy-load");
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function (
entries,
observer
) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
let img = entry.target;
img.src = img.getAttribute("data-src");
img.classList.remove("lazy-load");
lazyImageObserver.unobserve(img);
}
});
});
lazyImages.forEach(function (img) {
lazyImageObserver.observe(img);
});
} else {
lazyImages.forEach(function (img) {
img.src = img.getAttribute("data-src");
img.classList.remove("lazy-load");
});
}
});
- data-src: lưu trữ đường dẫn thực sự của hình ảnh. Thuộc tính src ban đầu để trống hoặc có thể là một hình ảnh placeholder nhỏ.
- Intersection Observer: theo dõi khi hình ảnh xuất hiện trong viewport (entry.isIntersecting).
- Khi hình ảnh xuất hiện, cập nhật thuộc tính src với giá trị từ data-src và ngừng quan sát hình ảnh đó.
4.1.2 Sử dụng Intersection Observer API
import { useEffect, useRef } from 'react';
function LazyImage({ src, alt }) {
const imgRef = useRef();
useEffect(() => {
const img = imgRef.current;
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
([entry], observerInstance) => {
if (entry.isIntersecting) {
img.src = src;
observerInstance.unobserve(img);
}
},
{ threshold: 0.1 }
);
observer.observe(img);
return () => {
observer.unobserve(img);
};
} else {
img.src = src;
}
}, [src]);
return <img ref={imgRef} alt={alt} />;
}
export default LazyImage;
- useRef: tạo tham chiếu đến thẻ <img>.
- useEffect: thiết lập Intersection Observer khi component được mount.
- Intersection Observer: khi hình ảnh xuất hiện trong viewport, thuộc tính src sẽ được cập nhật để load hình ảnh.
Nhưng khi sử dụng 2 kỹ thuật này, sẽ có nhược điểm:
- Phụ thuộc vào JavaScript trên trình duyệt. nếu người dùng tắt JavaScript, Lazy Loading sẽ không hoạt động.
- Có thể ảnh hưởng đến SEO nếu nội dung quan trọng bị trì hoãn tải.
4.2 Server-side Rendering (SSR) với Lazy Loading
Trong SSR với Lazy Loading, quá trình render trang web diễn ra trên server, và HTML được gửi tới browser đã bao gồm nội dung cần thiết cho SEO và trải nghiệm người dùng ban đầu. Lazy Loading được kết hợp để trì hoãn việc tải các tài nguyên không cần thiết ngay lập tức trên phía client.
NextJS hỗ trợ SSR và cung cấp các công cụ để kết hợp Lazy Loading một cách hiệu quả. Ví dụ điển hình nhất là component Image
import Image from 'next/image';
function HomePage() {
return (
<div>
<h1>Trang chủ</h1>
<Image
src="/images/large-image.jpg"
alt="Hình ảnh lớn"
width={800}
height={600}
priority={false}
/>
</div>
);
}
export default HomePage;
- priority={false}: mặc định, các hình ảnh bên dưới viewport sẽ được Lazy Load.
- SSR với hình ảnh: Nextjs sẽ xử lý việc render HTML trên máy chủ với các placeholder cho hình ảnh, giúp cải thiện SEO và trải nghiệm người dùng.
Ưu điểm của việc sử dụng SSR với Lazy Loading:
- Cải thiện SEO vì nội dung quan trọng được render trên server.
- Tải trang nhanh hơn do giảm kích thước JavaScript ban đầu.
- Trải nghiệm người dùng tốt hơn, đặc biệt trên các thiết bị có cấu hình thấp.
Tuy nhiên vẫn có vài nhược điểm nhỏ, theo mình thấy không đáng kể:
- Cấu hình phức tạp hơn so với Client-side Lazy Loading.
- Có thể tăng tải cho server do quá trình render.
Đây là một vài lưu ý, mình muốn khuyên bạn khi áp dụng:
- Một số tính năng như Intersection Observer API không được hỗ trợ trên các trình duyệt cũ. Bạn nên kiểm tra và cung cấp giải pháp thay thế hoặc polyfill trong trường hợp cần.
- Đảm bảo rằng nội dung quan trọng cho SEO không bị trì hoãn. Các công cụ tìm kiếm có thể không chạy JavaScript hoặc không chờ tài nguyên được Lazy Load.
- Cân nhắc hiển thị Placeholder hoặc Skeleton khi tài nguyên đang được tải để cải thiện trải nghiệm người dùng.
Trong thực tế, thường sẽ kết hợp cả hai phương pháp để tận dụng ưu điểm của mỗi bên. Ví dụ:
- Sử dụng SSR để render nội dung quan trọng cho SEO và trải nghiệm người dùng ban đầu.
- Sử dụng Client-side Lazy Loading cho các tài nguyên không quan trọng, như hình ảnh bên dưới viewport hoặc các component ít quan trọng.
5. Kết luận
Qua bài viết, bạn đã hiểu Lazy Loading là gì, những lợi ích đáng giá mà nó mang lại, các loại Lazy Loading phổ biến, và cách thức nó vận hành. Quan trọng hơn cả, đây là một giải pháp mà bạn có thể áp dụng vào bất kỳ dự án nào – từ website, ứng dụng di động đến hệ thống phức tạp hơn – để đạt được hiệu suất vượt trội.
Nếu bạn muốn trang web hoặc ứng dụng của mình nhanh hơn, mượt mà hơn, thì đã đến lúc thử áp dụng Lazy Loading. Đôi khi, chỉ cần một thay đổi nhỏ, bạn có thể tạo ra sự khác biệt lớn trong trải nghiệm người dùng. Hãy bắt đầu ngay hôm nay và tận dụng tối đa sức mạnh của Lazy Loading!
Các bài viết liên quan:
Bài viết liên quan
Vercel là gì? Hướng dẫn deploy dự án Next.js bằng Vercel
Dec 07, 2024 • 14 min read
So sánh giữa HOCs, Render Props và Hooks.
Dec 05, 2024 • 8 min read
Render Props pattern là gì? Hướng dẫn sử dụng Render Props
Dec 03, 2024 • 8 min read
HOCs Pattern là gì? Hướng dẫn triển khai Hocs Pattern trong dự án React
Dec 02, 2024 • 7 min read
Hooks Pattern là gì? Hướng dẫn áp dụng Hooks Pattern trong dự án React
Nov 28, 2024 • 11 min read
Promise là gì? Hướng dẫn sử dụng Promise trong dự án React
Nov 27, 2024 • 7 min read