Site icon saotuvi.com

Lazy Loading: Kỹ thuật Tối ưu Hiệu suất Website

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

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

HTML

<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

HTML

<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

JSX

function LazyIframe() {
  return (
    <iframe
      src="https://www.youtube.com/embed/dQw4w9WgXcQ"
      loading="lazy"
      width="560"
      height="315"
      frameBorder="0"
      allowFullScreen
    ></iframe>
  );
}

JSX

import dynamic from 'next/dynamic';

const LazyIframe = dynamic(() => import('../components/LazyIframe'), {
  ssr: false,
  loading: () => <p>Đang tải video...</p>,
});

function Page() {
  return (
    <p>
      <h1>Video</h1>
      <LazyIframe />
    </p>
  );
}

export default Page;

3.3 Lazy Loading cho module, component (Code Splitting)

Cá nhân mình thường sử dụngReact.lazy()Suspense đối với React

JSX

import React, { Suspense } from 'react';

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <p>
      <h1>Ứng dụng của tôi</h1>
      <Suspense fallback={<p>Đang tải...</p>}>
        <HeavyComponent />
      </Suspense>
    </p>
  );
}

export default App;

còn đối với NextJS có thể sử dụng next/dynamic

JSX

import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
  loading: () => <p>Đang tải...</p>,
});

function HomePage() {
  return (
    <p>
      <h1>Trang chủ</h1>
      <HeavyComponent />
    </p>
  );
}

export default HomePage;

3.4 Lazy Loading cho dữ liệu

3.5 Infinite Scrolling

JSX

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 (
    <p>
      {posts.map((item) => (
        <p key={post.id}>{post.content}</p>
      ))}
    </p>
  );
}

export default InfiniteScroll;

Ngoài ra, bạn có thể áp dụng useSWR

JSX

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 <p>Lỗi khi tải dữ liệu.</p>;

  return (
    <p>
      {items.map((item) => (
        <p key={item.id} style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>
          {item.content}
        </p>
      ))}
      {isLoading && <p>Đang tải thêm dữ liệu...</p>}
    </p>
  );
}

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.

Bash

npm install lodash.debounce

JSX

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

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

JSX

import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap' });

function App() {
  return (
    <p className={inter.className}>
      <p>200Lab - Up.</p>
    </p>
  );
}

export default App;

3.7 Lazy Loading Stylesheets

HTML

<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.

JSX

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 <p className="styled-component">Đây là CSS lazy</p>;
}

export default LazyStylesComponent;

Đối với NextJS bạn có thể sử dụng dynamic import cho CSS module

JSX

import dynamic from 'next/dynamic';

const styles = dynamic(() => import('../styles/lazy-styles.module.css'));

function Page() {
  return (
    <p className={styles.styledComponent}>
      CSS lazy
    </p>
  );
}

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:

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à:

4.1.1 Sử dụng Intersection Observer API (HTML và JavaScript)

HTML

<img data-src="large-image.jpg" alt="image" class="lazy-load">

JS

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");
    });
  }
});

4.1.2 Sử dụng Intersection Observer API

JSX

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;

Nhưng khi sử dụng 2 kỹ thuật này, sẽ có nhược điểm:

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

JSX

import Image from 'next/image';

function HomePage() {
  return (
    <p>
      <h1>Trang chủ</h1>
      <Image
        src="/images/large-image.jpg"
        alt="Hình ảnh lớn"
        width={800}
        height={600}
        priority={false}
      />
    </p>
  );
}

export default HomePage;

Ưu điểm của việc sử dụng SSR với Lazy Loading:

Tuy nhiên vẫn có vài nhược điểm nhỏ, theo mình thấy không đáng kể:

Đây là một vài lưu ý, mình muốn khuyên bạn khi áp 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ụ:

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:

Exit mobile version