Lazy load là gì: Cách triển khai lazy load ảnh và video trên website

Vài lời của người dịch: Bài viết này chủ yếu dành cho những ai muốn tìm hiểu sâu hơn về lazy load, nó đòi hỏi bạn phải có hiểu biết nhất định về HTML, JavaScript, CSS. Với những ai đơn thuần chỉ muốn áp dụng lazy load lên trang WordPress, có thể tìm hiểu các plugin sẵn có chất lượng như Flying Images, a3 Lazy Load, vân vân. Nhìn chung tôi ủng hộ tính năng lazy load, nhưng như một cách tự phản biện, tôi chủ động tìm hiểu các điểm yếu của nó, chẳng hạn ở đâyở đâycả đây nữa.

Lưu ý: Hiện bạn đã có thể sử dụng lazy load cấp độ trình duyệt (native lazy loading)! Bạn có thể tham khảo link vừa dẫn để biết cách sử dụng thuộc tính loading và tận dụng thư viện của bên thứ ba đóng vai trò như một dự phòng (fallback) cho các trình duyệt vẫn chưa hỗ trợ thuộc tính này.

Thành phần ảnhvideo trên website thường chiếm một lượng dữ liệu tải về lớn. Không may là các bên liên quan đến dự án có thể không thích cắt giảm bất cứ tài nguyên media (đa phương tiện) nào từ các ứng dụng đã có của họ. Tình huống bế tắc như vậy gây bực bội, đặc biệt khi tất cả mọi người đều muốn cải thiện hiệu suất và tốc độ, nhưng lại không đồng thuận về cách để đạt được điều đó. May thay, lazy load là giải pháp giúp bạn có được dung lượng trang cần phải tải lần đầu (initial page payload)* thấp và thời gian tải lần đầu ngắn hơn, nhưng không bắt bạn phải cắt bỏ nội dung.

(*) initial page payload: dung lượng trang tải lần đầu. Từ khóa ở đây là initial/lần đầu. Các trang không lazy load thì trang sẽ tải một lượt tất cả các tài nguyên trên trang, còn với trang áp dụng lazy load, nội dung của trang sẽ không tải một lượt tất cả, mà nó sẽ được chia làm nhiều lần, các nội dung lazy load sẽ tải sau khi đạt điều kiện kích hoạt (trigger).

Lazy load là gì?

Lazy load là kỹ thuật thực hiện trì hoãn (defer) tải các tài nguyên không quan trọng (non-critical resoureces) vào thời điểm tải trang (page load time). Thay vì tải ngay lập tức, các tài nguyên không quan trọng này chỉ tải vào thời điểm cần thiết (moment of need). Khi đề cập đến ảnh, thì “không quan trọng” thường đồng nghĩa với “ngoài màn hình / off-screen”. Nếu bạn sử dụng Lighthouse và kiểm tra một số cơ hội cải thiện, bạn có thể thấy một vài hướng dẫn trong địa hạt này ở dạng kiểm tra các ảnh ngoài màn hình:

kiểm tra ảnh ngoài màn hình

Hình 1. Một kiểm tra hiệu suất của Lighthouse xác định các ảnh ngoài màn hình, những cái là ứng cử viên tiềm năng áp dụng lazy load.

Bạn có khả năng đã thấy lazy load trong thực tế rồi, và nó diễn ra như thế này:

  • Bạn truy cập một trang, và bắt đầu cuộn chuột trong quá trình đọc nội dung.
  • Đến điểm nào đó, bạn cuộn chuột đến một ảnh chờ sẵn/giữ chỗ (placeholder image) bên trong viewport (khung nhìn trình duyệt)**.
  • Ảnh chờ sẵn này đột nhiên được thay thế bằng ảnh cuối cùng (ảnh thực mà bạn muốn người dùng xem).

(**): Trong bài viết này, người dịch sử dụng song song viewport và khung nhìn trình duyệt, chúng có nghĩa tương đương.

Một ví dụ về lazy load ảnh mà bạn có thể tìm thấy dễ dàng chính là các nền tảng xuất bản phổ biến (popular publishing platform), chẳng hạn như Medium, nó chỉ tải một ảnh chiếm chỗ nhẹ nhàng (lightweight) vào thời điểm tải trang, và thay thế chúng bằng các ảnh thực (cần lazy load) khi chúng được cuộn chuột đến và nằm trong (hoặc gần) phần viewport của người dùng. (Ví dụ demo na ná medium: https://code.speed.family/lazysizes-demo-LQIP1.html)

ví dụ về lazy load ảnh

Hình 2. Một ví dụ về lazy load ảnh trong thực tế. Một ảnh chiếm chỗ được tải ở bên trái vào thời điểm tải trang, và khi được cuộn đến viewport, ảnh cuối cùng (ảnh thực) được tải vào thời điểm phù hợp để thay thế ảnh chiếm chỗ.

Nếu bạn không quen thuộc với lazy load, bạn có thể tự hỏi lợi ích của kỹ thuật này là gì. Hãy đọc tiếp để biết nhé!

Tại sao lại cần lazy load ảnh hoặc video mà không tải chúng luôn cho rồi?

Bởi vì có khả năng là bạn đang tải về các thành phần trên trang mà người dùng có thể không bao giờ nhìn đến. Điều này là vấn đề vì hai lý do sau:

  • Nó là sự lãng phí dữ liệu. Trên các kiểu kết nối băng thông không giới hạn (unmetered), điều tồi tệ nhất có thể vẫn chưa xảy ra đâu (dù bạn có thể sử dụng lượng băng thông quý giá này để ưu tiên tải các tài nguyên khác mà người dùng thực sự sẽ nhìn thấy). Trên các gói giới hạn dữ liệu, tải các thành phần người dùng không bao giờ nhìn đến có thể làm lãng phí tiền của họ.
  • Nó làm lãng phí thời gian xử lý, pin và các tài nguyên hệ thống khác. Sau khi một tài nguyên đa phương tiện (media) được tải về, trình duyệt phải giải mã (decode) và kết xuất (render) nội dung của nó trong khung nhìn trình duyệt.

Khi chúng ta lazy load ảnh và video, chúng ta làm giảm thời gian tải cần thiết để tải trang lúc ban đầu (initial page) thông qua giảm dung lượng tải trang lúc ban đầu, cũng như giảm sử dụng tài nguyên hệ thống, tất cả đều ảnh hưởng tích cực đến hiệu suất. Trong hướng dẫn này, chúng ta sẽ bàn về một số kỹ thuật và các chỉ dẫn để thực hiện lazy load ảnh và video cũng như một danh sách ngắn các thư viện phổ biến thường được dùng.

Lazy load ảnh

Cơ chế lazy load ảnh đơn giản về mặt lý thuyết, nhưng đi vào chi tiết thì lại khá khó nhằn (finicky). Thêm vào đó có một số trường hợp riêng biệt có thể có cả lợi ích từ lazy load. Nào, chúng ta cùng bắt đầu tìm hiểu cách lazy load các ảnh nội tuyến (inline) trong HTML.

Ảnh nội tuyến

Các ứng cử viên phổ biến nhất để lazy load là các ảnh sử dụng phần tử <img>. Khi chúng ta lazy load các phần tử <img>, chúng ta sử dụng JavaScript để kiểm tra xem chúng có đang ở trong viewport hay không. Nếu chúng có, thì thuộc tính src của chúng (và đôi khi là srcset) sẽ được điền URL của bức ảnh cần thiết.

Sử dụng Intersection Observer

Nếu bạn từng viết mã lazy load trước đây, bạn có thể đã thử hoàn thành nhiệm vụ này bằng cách sử dụng các trình xử lý sự kiện (event handlers) như scroll hoặc resize. Dù cách tiếp cận này cho phép tương thích với nhiều trình duyệt nhất, thì các trình duyệt hiện đại bây giờ cung cấp cách thức hiệu quả hơn trong việc kiểm tra phần tử đang hiển thị ra ngoài thông qua API Intersection Observer.

Lưu ý: Nhược điểm của Intersection Observer là nó không được hỗ trợ trong tất cả các trình duyệt. Nếu vấn đề tương thích với lượng lớn trình duyệt là điều quan trọng hơn, hãy nhớ đọc phần kế tiếp, nó sẽ chỉ cho bạn cách lazy load ảnh sử dụng trình xử lý sự kiện scrollresize, tuy rằng so với API thì nó kém hiệu quả hơn.

Intersection observer dễ dùng và dễ đọc hơn mã dựa trên nhiều trình xử lý sự kiện khác nhau, bởi vì người lập trình chỉ cần đăng ký một observer để quan sát các phần tử thay vì phải viết mã tẻ nhạt dùng để nhận diện phần tử nào đang hiển thị. Tất cả những gì còn lại phải làm với người lập trình là quyết định xem nên thực hiện điều gì khi phần tử đập vào mắt người dùng. Giờ cùng giả định đây là mẫu mã đánh dấu cơ bản cho các phần tử lazy load <img> của chúng ta:

<img class="lazy" src="anh-giu-cho.jpg" data-src="anh-de-lazy-load-1x.jpg" data-srcset="anh-de-lazy-load-2x.jpg 2x, anh-de-lazy-load-1x.jpg 1x" alt="Tôi là ảnh!">

Có ba thành phần liên quan của mã đánh dấu này mà chúng ta cần tập trung vào:

  1. Thuộc tính class, sẽ được JavaScript dùng để lựa chọn phần tử (element).
  2. Thuộc tính src, đóng vai trò ảnh giữ chỗ, và sẽ xuất hiện khi trang tải lần đầu tiên. Ảnh giữ chỗ này tất nhiên có kích cỡ rất nhỏ.
  3. Các thuộc tính data-srcdata-srcset là nơi chứa URL ảnh thực mà bạn muốn hiển thị cho người dùng khi phần từ nằm trong khung nhìn trình duyệt.

Giờ chúng ta sẽ học cách sử dụng intersection observer trong JavaScript để lazy load ảnh bằng mã đánh dẫu mẫu dưới đây:

document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Bạn có thể thêm các dự phòng ở phần này để tăng tính tương thích
  }
});

Dựa vào sự kiện DOMContentLoaded của tài liệu, đoạn mã trên truy vấn DOM để lấy tất cả các phần tử <img> có class là lazy. Nếu intersection observer khả dụng, chúng ta sẽ tạo một observer mới để chạy callback khi phần tử img.lazy đi vào khung nhìn trình duyệt. Kiểm tra ví dụ trên CodePen để thấy cách đoạn mã này hoạt động trong thực tế.

Ví dụ tôi thực hiện dựa vào mã mẫu ở trên: https://code.speed.family/hand-code-lazyload1.html

Lưu ý: Đoạn mã ở trên sử dụng phương thức intersection observer có tên isIntersecting, đây là phương thức không có trong triển khai của intersection observer của Edge phiên bản 15. Do đó, đoạn mã lazy load ở trên (và các đoạn mã tương tự khác) sẽ lỗi trên Edge.

Sử dụng trình xử lý sự kiện (cách có tính tương thích cao nhất)

Dù bạn phải sử dụng intersection observer để triển khai lazy load, thì khả năng tương thích với trình duyệt của ứng dụng vẫn là điều quan trọng. Bạn có thể triển khai dự phòng cho intersection observer (và đây là cách dễ nhất), ngoài ra bạn cũng có thể dự phòng cho đoạn mã bằng cách sử dụng các trình xử lý sự kiện như là scroll, resize, và có thể là orientationchange kèm sự phối hợp với getBoundingClientRect để xác định xem liệu một phần tử có ở trong khung nhìn trình duyệt hay không.

Giả sử với cùng mẫu đánh dấu từ trước, đoạn mã JavaScript sau đây sẽ cung cấp tính năng lazy load:

document.addEventListener("DOMContentLoaded", function() {
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  let active = false;

  const lazyLoad = function() {
    if (active === false) {
      active = true;

      setTimeout(function() {
        lazyImages.forEach(function(lazyImage) {
          if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
            lazyImage.src = lazyImage.dataset.src;
            lazyImage.srcset = lazyImage.dataset.srcset;
            lazyImage.classList.remove("lazy");

            lazyImages = lazyImages.filter(function(image) {
              return image !== lazyImage;
            });

            if (lazyImages.length === 0) {
              document.removeEventListener("scroll", lazyLoad);
              window.removeEventListener("resize", lazyLoad);
              window.removeEventListener("orientationchange", lazyLoad);
            }
          }
        });

        active = false;
      }, 200);
    }
  };

  document.addEventListener("scroll", lazyLoad);
  window.addEventListener("resize", lazyLoad);
  window.addEventListener("orientationchange", lazyLoad);
});

Đoạn mã trên sử dụng getBoundingClientRect trong trình xử lý sự kiện scroll để kiểm tra xem có bất kỳ phần tử img.lazy nào nằm trong khung nhìn trình duyệt hay không. setTimeout được gọi để trì hoãn xử lý, và biến active chứa thông tin trạng thái quá trình xử lý- cái được sử dụng để điều chỉnh các lời gọi liên quan đến chức năng. Vì ảnh được lazy load, chúng sẽ bị loại bỏ khỏi mảng phần tử. Khi mảng phần tử đạt đến length, đoạn mã của trình xử lý sự kiện scroll bị loại bỏ. Bạn có thể xem ví dụ này trên CodePen để thấy đoạn mã trong thực tế.

Ví dụ về đoạn mã sử dụng trình xử lý sự kiện: https://code.speed.family/hand-code-lazyload2.html

Trong khi đoạn mã này hoạt động được trên hầu hết các trình duyệt, nó có khả năng có các vấn đề về hiệu suất khi lời gọi setTimeout lặp lại có thể gây lãng phí, ngay cả khi đoạn mã bên trong chúng đã được điều chỉnh. Trong ví dụ này, một kiểm tra được chạy lặp đi lặp lại với tần suất 200 mili giây khi tài liệu được cuộn hoặc kích cỡ cửa sổ thay đổi bất kể chuyện có ảnh trong viewport hay không. Thêm vào đó, công việc theo dõi để biết còn có bao nhiêu phần tử còn lại cần lazy load thật tẻ nhạt, và việc bỏ trình xử lý sự kiện scroll được để dành lại cho nhà lập trình.

Kết luận đơn giản: Sử dụng intersection observer bất cứ khi nào có thể, và thực hiện dự phòng bằng trình xử lý sự kiện nếu tính tương thích rộng nhất là yêu cầu quan trọng của ứng dụng.

Ảnh trong CSS

Dù thẻ <img> là cách sử dụng ảnh phổ biến nhất trên các trang web, thì ảnh cũng có thể được gọi (invoked) qua thuộc tính CSS là background-image (và các thuộc tính khác nữa). Không giống với phần tử <img> sẽ được tải bất kể nó có đi vào khung nhìn trình duyệt hay không, hành vi tải ảnh trong CSS được thực hiện với nhiều suy đoán, tính toán (speculation) hơn. Khi tài liệu và mô hình đối tượng CSS (the document and CSS object models) cũng như cây kết xuất (render tree) được xây dựng, trình duyệt sẽ kiểm tra cách CSS được áp dụng vào tài liệu trước khi yêu cầu các tài nguyên bên ngoài. Nếu trình duyệt phát hiện một quy tắc CSS liên quan đến tài nguyên bên ngoài không được áp dụng vào tài liệu như cách nó đang được xây dựng thì trình duyệt sẽ không gửi yêu cầu đến tài nguyên đó.

Hành vi tính toán này có thể được sử dụng để trì hoãn các ảnh trong CSS bằng cách sử dụng JavaScript để phát hiện khi một phần tử nằm trong khung nhìn trình duyệt, và sau đó áp một class vào phần tử mà class đó được áp dụng style gọi ảnh nền (background image). Cái này sẽ giúp ảnh được tải vào thời điểm cần thiết thay vì tải ngay lúc ban đầu. Ví dụ, hãy lấy một phần tử có chứa một ảnh background lớn ở vị trí thu hút:

<div class="lazy-background">
  <h1>Đây là tiêu đề thu hút sự chú ý của bạn!</h1>
  <p>Đây là phần văn bản thu hút để thuyết phục bạn mua hàng!</p>
  <a href="/buy-a-thing">mua hàng!</a>
</div>

Phần tử div.lazy-background thông thường sẽ chứa ảnh background thu hút được gọi bởi một số mã CSS. Trong ví dụ về lazy load này, chúng ta có thể cô lập thuộc tính background-image của phần tử div.lazy-background thông qua class visible – cái mà chúng ta chỉ thêm vào phần tử khi nó ở trong khung nhìn trình duyệt:

.lazy-background {
  background-image: url("hero-placeholder.jpg"); /* đây chỉ là ảnh giữ chỗ mà thôi */
}

.lazy-background.visible {
  background-image: url("hero.jpg"); /* còn đây mới là ảnh thật */
}

Bắt đầu từ đây, chúng ta sẽ sử dụng JavaScript để kiểm tra xem phần tử có ở trong viewport hay không (bằng intersection observer!), và thêm class visible vào phần tử div.lazy-background vào thời điểm đó, cái mà sẽ giúp tải ảnh về:

document.addEventListener("DOMContentLoaded", function() {
  var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background"));

  if ("IntersectionObserver" in window) {
    let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          lazyBackgroundObserver.unobserve(entry.target);
        }
      });
    });

    lazyBackgrounds.forEach(function(lazyBackground) {
      lazyBackgroundObserver.observe(lazyBackground);
    });
  }
});

Như đã nói từ trước, bạn chắc chắn sẽ muốn cung cấp một dự phòng cho phương thức intersection observer bởi vì không phải tất cả trình duyệt đều hỗ trợ nó ở thời điểm hiện tại. Kiểm tra demo trên CodePen để xem cách đoạn mã này hoạt động trong thực tế như thế nào.

Bạn có thể tham khảo mẫu trang mà tôi tạo theo hướng dẫn trên: https://code.speed.family/lazyload-bgr-image.html

Lazy load video

Cũng giống với phần tử ảnh, chúng ta cũng có thể sử dụng lazy load cho video. Khi chúng ta tải video trong bối cảnh thông thường, nó sẽ sử dụng phần tử <video> (mặc dù cũng có một phương thức thay thế sử dụng <img> để áp dụng trên một số nền tảng bị hạn chế). Mặc dù cách sử dụng lazy load <video> còn tùy vào từng trường hợp cụ thể. Chúng ta sẽ bàn về một số kịch bản cần các giải pháp khác nhau.

Với video không bật tự động (autoplay)

Với các video được bật bởi thao tác chủ động ban đầu của người dùng (ví dụ video không bật tự động), thì chỉ thị thuộc tính preload trên phần tử <video> có thể rất cần thiết:

<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

Ở đây, chúng ta sử dụng thuộc tính preload với giá trị none để ngăn trình duyệt preload mọi dữ liệu của video. Để giữ không gian, chúng ta sử dụng thuộc tính poster trong phần tử <video>. Cần phải làm như thế là vì các hành vi mặc định trong việc tải video có thể thay đổi nhiều giữa các trình duyệt khác nhau:

  • Trong Chrome, hành vi mặc định của preloadauto, nhưng trong Chrome 64, mặc định của nó giờ là metadata. Thậm chí, trên Chrome phiên bản máy bàn, một phần video có thể preload bằng cách sử dụng header Content-Range. Firefox, Edge và Internet Explorer 11 cũng có các hành vi tương tự.
  • Giống Chrome trên phiên bản máy bàn, phiên bản 11 trên máy bàn của Safari sẽ preload một phần video. Trong phiên bản 11.2 (hiện là phiên bản kỹ thuật xem trước của Safari) chỉ siêu dữ liệu (metadata) của video là được preload. Với Safari trên iOS, video không bao giờ được preload.
  • Khi chế độ Data Saver được bật, thuộc tính mặc định của preloadnone.

Do hành vi mặc định của trình duyệt liên quan đến preload không thống nhất giữa các trình duyệt khác nhau, tuyên bố rõ ràng sẽ là cách làm tốt nhất. Trong trường hợp này khi người dùng bắt đầu bật video, sử dụng preload=none là cách dễ nhất để trì hoãn tải video trên tất cả các nền tảng. Thuộc tính preload không phải là cách duy nhất để trì hoãn tải video. Bạn có thể tham khảo thêm bài viết Fast Playback with Video Preload để có được một số ý tưởng và cái nhìn sâu hơn vào cách làm việc với video playback trong JavaScript.

Thật không may, nó không hữu ích trong trường hợp chúng ta muốn sử dụng video thay cho ảnh động GIF, cái chúng ta sẽ bàn đến ngay sau đây.

Với video đóng vai trò thay thế ảnh động GIF

Dù ảnh động GIF được ưa thích và sử dụng rỗng rãi, chúng yếu hơn khi so sánh với video trong một số khía cạnh, đặc biệt là kích cỡ file đầu ra. Ảnh động GIF có thể có thể có kích cỡ dữ liệu lên đến vài magabytes. Video với chất lượng hình tương tự có xu hướng nhỏ hơn nhiều về mặt dung lượng.

Sử dụng phần tử <video> để thay thế ảnh động GIF không đơn giản như phần tử <img>. Ảnh động GIF có ba hành vi sau cần được video thay thế kế thừa:

  1. Chúng bật tự động khi tải về xong.
  2. Chúng bật đi bật lại liên tục (mặc dù không phải lúc nào cũng như vậy).
  3. Chúng không gắn kèm âm thanh.

Để đạt được kết quả tương tự, phần tử <video> cần trông giống như thế này:

<video autoplay muted loop playsinline>
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

Các thuộc tính autoplay/tự động bật, muted/tắt tiếng, loop/lặp lại đã tự giải thích ý nghĩa của chúng. playsinline là cần thiết để bật tự động trong iOS. Giờ chúng ta có thể phục vụ video thay thế ảnh GIF hoạt động được trên các nền tảng. Nhưng làm thế nào để lazy load nó? Chrome sẽ lazy load video cho bạn, nhưng bạn không thể tự tin cho rằng tất cả các trình duyệt đều cung cấp hành vi tối ưu này. Phụ thuộc đối tượng người đọc và các yêu cầu ứng dụng, bạn có thể cần can thiệp theo cách thủ công. Để bắt đầu, hãy chỉnh sửa mã đánh dấu <video> của bạn cho phù hợp:

<video autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>

Bạn có thể lưu ý thuộc tính poster được thêm vào, cái cho phép bạn chỉ định không gian giữ chỗ cho phần tử <video> cho đến khi video được lazy load. Như ví dụ về <img> trước đó, chúng ta giấu URL video trong thuộc tính data-src trong từng phần tử <source>. Từ đó chúng ta sử dụng một số mã JavaScript tương tự với ví dụ trước đó để lazy load ảnh dựa trên intersection observer:

document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

Khi chúng ta lazy load phần tử <video>, chúng ta cần đi qua tất cả các phần tử con <source> và chuyển thuộc tính data-src của nó thành thuộc tính src. Một khi chúng ta làm điều đó, chúng ta cần kích hoạt tải video bằng cách gọi phương thức load của phần tử, sau đó tài nguyên đa phương tiện sẽ tự động phát tương ứng với thuộc tính autoplay.

Sử dụng phương thức này, chúng ta có được giải pháp video mô phỏng hành vi của ảnh động GIF, nhưng không phát sinh việc sử dụng nhiều dữ liệu như cách ảnh động GIF thực hiện, và chúng ta có được lazy load cho nội dung.

Các thư viện lazy load

Nếu bạn không muốn quan tâm đến cách lazy load hoạt động dưới chế độ nền (hood) và chỉ thích quất luôn thư viện cho đơn giản (chẳng có gì phải xấu hổ trong chuyện này cả!), thế thì có cả tá lựa chọn cho bạn. Nhiều thư viện sử dụng mẫu mã đánh dấu tương tự như các mã được trình bày ở trên. Bên dưới là các thư viện lazy load có thể rất hữu dụng cho bạn:

  • lazysizes là thư viện lazy load với đầy đủ tính năng có khả năng giúp bạn lazy load ảnh và iframe. Mẫu của nó sử dụng khá giống với mã trong các ví dụ bên trên, trong đó nó tự động liên kết đến class lazyload trong các phần tử <img>, và yêu cầu bạn chỉ định URL cụ thể trong thuộc tính data-src và/hoặc data-srcset, URL sẽ được chuyển tương ứng vào trong thuộc tính src hoặc srcset. Lazysizes sử dụng intersection observer (bạn có thể vá víu thêm), và có thể mở rộng bằng một số plugin để thực hiện nhiệm vụ lazy load video. Bạn có thể tham khảo hướng dẫn sử dụng lazysizes.js ở đây.
  • lozad.js là lựa chọn siêu nhẹ dành cho bạn, nó chỉ sử dụng intersection observer. Nhờ vậy mà nó rất hiệu quả, nhưng sẽ cần vá víu (polyfilled) thêm trước khi bạn muốn triển khai lozad.js trên các trình duyệt đời cũ.
  • blazy là tùy chọn khác cũng rất nhẹ nhàng (nó chỉ có dung lượng 1,4 KB). Giống như lazysizes, nó không cần bất kỳ tiện ích của bên thứ ba nào, blazy tương thích với IE7+. Tuy nhiên, thật không may, blazy lại không sử dụng intersection observer.
  • yall.js là thư viện tôi viết (tác giả gốc của bài tiếng Anh), nó sử dụng IntersectionObserver và dự phòng bằng các trình xử lý sự kiện. Yall.js tương thích với IE11 và các trình duyệt lớn khác.
  • Nếu bạn muốn tìm kiếm thư viện lazy load chuyên cho React, bạn có thể cân nhắc sử dụng react-lazyload. Dù nó không sử dụng intersection observer, nó vẫn cung cấp phương thức tương tự trong việc lazy load ảnh cho những ai quen thuộc phát triển ứng dụng bằng React.

Các thư viện lazy load kể trên đều có tài liệu hướng dẫn chi tiết, với nhiều mẫu mã đánh dấu cho phép bạn sử dụng nhiều kiểu lazy load khác nhau. Nếu bạn không thành thạo mã thì bạn chỉ cần chọn thư viện rồi triển khai thôi. Điều đó sẽ giúp tiết kiệm rất nhiều công sức.

Rắc rối nào có thể xảy ra

Lazy load ảnh và video có các ảnh hưởng tích cực lên hiệu suất và tốc độ, tuy nhiên không nên coi nó là nhiệm vụ giản đơn. Nếu bạn triển khai sai, nó có thể để lại hậu quả khôn lường. Do đó bạn cần để ý đến các vấn đề quan trọng dưới đây.

Ghi nhớ đường biên fold

Ý tưởng lazy load mọi tài nguyên đa phương tiện trên trang bằng JavaScript rất hấp dẫn, nhưng bạn cần chống lại sự cám dỗ này. Bất cứ thứ gì nằm trên màn hình đầu tiên không cần lazy load. Các tài nguyên này phải được xem như là các tài sản quan trọng, và vì thế cần phải được tải theo cách thông thường.

Vấn đề trọng tâm trong việc tải tài nguyên đa phương tiện quan trọng theo cách thông thường chứ không dùng lazy load là vì việc lazy load làm trì hoãn việc tải các tài nguyên đó cho đến khi DOM có khả năng tương tác- khi tập lệnh tải xong và bắt đầu được thực thi. Với các ảnh nằm bên dưới đường biên fold, điều này không có vấn đề gì, nhưng với các tài nguyên quan trọng nằm trên đường biên fold nó sẽ tải nhanh hơn khi bạn sử dụng phần tử <img> tiêu chuẩn.

Tất nhiên vị trí của đường biên fold không rõ ràng trong thời đại này, khi website được hiển thị trên nhiều màn hình với các kích cỡ khác nhau. Những nội dung mà nằm trên màn hình đầu tiên của laptop có thể lại nằm dưới màn hình đầu tiên trên thiết bị di động. Không có giải pháp hoàn hảo nào để giải quyết vấn đề này trong mọi tình huống. Bạn cần tiến hành kiểm kê các tài nguyên quan trọng nằm trên trang, và tải những ảnh này theo cách thông thường.

Thêm vào đó, bạn có thể không muốn quá nghiêm ngặt về đường biên fold cũng như ngưỡng kích hoạt lazy load. Mục tiêu của bạn có thể được cải thiện khi bạn thành lập một vùng đệm nằm bên dưới đường biên fold, nhờ thế ảnh có khả năng bắt đầu tải sớm trước khi người dùng cuộn đến chúng trong viewport. Ví dụ, intersection observer API cho phép bạn chỉ định thuộc tính rootMargin trong một đối tượng tùy chọn khi bạn tạo một IntersectionObserver mới. Điều này đem đến cho các phần tử một bộ đệm, cái sẽ kích hoạt hành vi lazy load trước khi phần tử đi vào khung nhìn trình duyệt (qua đó sẽ giúp người dùng thỏa mãn hơn nhờ tránh được hiện tượng trễ):

let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
  // Lazy loading image code goes here
}, {
  rootMargin: "0px 0px 256px 0px"
});

Nếu bạn thấy giá trị của rootMargin trông giống giá trị mà bạn chỉ định cho margin (lề) trong CSS thì đúng rồi đấy ạ! Trong trường hợp này, chúng ta mở rộng lề đáy của phần tử observing (khung nhìn trình duyệt theo mặc định, nhưng điều này có thể thay đổi) lên 256 pixel. Điều này có nghĩa là chức năng gọi sẽ được thực thi khi phần tử ảnh cách 256 pixel so với viewport, nói cách khác, ảnh sẽ bắt đầu tải trước khi người dùng thực sự thấy nó.

Để đạt được hiệu ứng tương tự khi sử dụng mã xử lý sự kiện scroll, bạn đơn giản chỉ cần điều chỉnh kiểm tra getBoundingClientRect để nó bao gồm bộ đệm, và bạn sẽ có được hiệu ứng tương tự trong trình duyệt không hỗ trợ intersection observer.

Bố cục thay đổi và các không gian giữ chỗ

Lazy load tài nguyên đa phương tiện có thể là nguyên nhân gây thay đổi bố cục nếu không gian giữ chỗ không được sử dụng. Những thay đổi này có thể làm phân tán mức độ tập trung, định hướng của người dùng và kích hoạt vận hành cấu trúc DOM tốn kém cũng như tiêu thụ tài nguyên hệ thống và góp phần gay ra hiện tượng jank. Ít nhất bạn nên cân nhắc sử dụng khối màu giữ chỗ đồng nhất có cùng độ phân giải với ảnh nhắm đến, hoặc các kỹ thuật như LQIP hoặc SQIP để gợi ý các phần tử đa phương tiện trước khi nó được tải.

Với các thẻ <img>, src ban đầu phải chỉ đến không gian giữ chỗ cho đến khi thuộc tính đó được cập nhật với URL ảnh cuối cùng. Sử dụng thuộc tính poster trong phần tử <video> để trỏ đến ảnh giữ chỗ. Ngoài ra, sử dụng thuộc tính widthheight trên cả thẻ <video> lẫn thẻ <img>. Điều này đảm bảo việc chuyển tiếp từ ảnh chiếm chỗ tới ảnh thực sự không làm thay đổi kích cỡ kết xuất của phần tử được dùng làm nơi bao chứa tài nguyên đa phương tiện.

Độ trễ trong nhiệm vụ giải mã hình ảnh

Tải các ảnh có dung lượng lớn trong JavaScript và đặt chúng vào trong DOM có thể làm luồng chính bị bận rộn, đây là nguyên nhân làm giao diện người dùng không thể phản hồi trong thời gian ngắn khi mà trình duyệt phải giải mã hình ảnh. Giải mã hình ảnh không đồng bộ (asynchronously) bằng phương thức decode trước khi chèn chúng vào DOM có thể cắt giảm kiểu jank này (độ trễ), nhưng cần nhận thức rõ là: hiện chúng không khả dụng trên mọi trình duyệt, và nó làm phức tạp thêm logic của lazy load. Nếu bạn muốn sử dụng nó, bạn sẽ cần kiểm tra lại nó. Bên dưới chúng tôi cho bạn thấy cách sử dụng Image.decode() đi kèm dự phòng:

var newImage = new Image();
newImage.src = "my-awesome-image.jpg";

if ("decode" in newImage) {
  // Fancy decoding logic
  newImage.decode().then(function() {
    imageContainer.appendChild(newImage);
  });
} else {
  // Regular image load
  imageContainer.appendChild(newImage);
}

Bạn có thể ngâm cứu link CodePen này để xem đoạn mã tương tự cho ví dụ trên trong thực tế. Nếu hầu hết các ảnh của bạn khá nhỏ, điều này sẽ chẳng có ích gì nhiều, nhưng nó chắc chắn có thể giúp bạn cắt giảm hiện tượng jank khi bạn lazy load các ảnh kích cỡ lớn và chèn chúng vào trong DOM.

Khi các tài nguyên không chịu tải về

Đôi khi các tài nguyên đa phương tiện không thể tải về vì một lý do nào đấy hoặc lỗi bất chợt. Điều đó có thể xảy ra khi nào? Nó còn tùy, nhưng đây là một tình huống giả định cho bạn: Bạn có tài liệu HTML có chính sách cache trong một thời gian ngắn (ví dụ 5 phút), và người dùng ghé thăm website hoặc người dùng rời tab cũ mở trong một thời gian dài (ví dụ vài tiếng) rồi sau đó họ quay trở lại để đọc tiếp nội dung của bạn. Tại một số điểm trong quá trình này, sẽ xuất hiện tái triển khai. Trong quá trình triển khai này, tên một tài nguyên ảnh thay đổi dựa trên phiên bản hash, hoặc bị loại bỏ hoàn toàn. Khi đến thời điểm người dùng cần lazy load ảnh, tài nguyên không còn có sẵn nữa, và thế là xảy ra lỗi.

Trong khi giả định trên cần phải thừa nhận là ít khi xảy ra, nó có thể vẫn buộc bạn phải có kế hoặc dự phòng nếu lazy load gặp lỗi. Với các ảnh, giải pháp có thể giống như bên dưới đây:

var newImage = new Image();
newImage.src = "my-awesome-image.jpg";

newImage.onerror = function(){
  // Chỉ thị cần phải làm gì khi có lỗi
};
newImage.onload = function(){
  // Tải ảnh
};

Điều mà bạn muốn làm trong sự kiện lỗi còn tùy thuộc vào ứng dụng của bạn. Ví dụ, bạn có thể thay thế ảnh giữ chỗ bằng một nút bấm để người dùng cố gắng tải ảnh một lần nữa, hoặc đơn giản hơn thì hiển thị một thông báo lỗi trong khu vực chứa ảnh chiếm chỗ.

Các kịch bản khác cũng có thể phát sinh. Cho dù bạn có làm gì đi nữa, ý tưởng tốt là thông báo cho người dung khi lỗi xuất hiện, và có thể cung cấp cho họ hành động nào đấy để khắc phục nếu có điều gì đó không ổn.

Tính sẵn có của JavaScript

Bạn không thể cứ giả định rằng JavaScript lúc nào cũng sẵn có được. Nếu bạn thực hiện lazy load ảnh, hãy cân nhắc đến việc cung cấp mã đánh dấu <noscript> để nó có thể hiển thị được ảnh trong trường hợp JavaScript không sẵn có (unavailable). Ví dụ bên dưới trình bày cách đơn giản nhất để thực hiện dự phòng thông qua sử dụng phần tử <noscript> để phục vụ ảnh nếu JavaScripts bị tắt (dù thông thường, theo mặc định thì nó luôn bật):

<!-- Hình ảnh thực tế được tải lười thông qua JavaScript -->
<img class="lazy" src="anh-giu-cho.jpg" data-src="anh-can-lazy-load.jpg" alt="Tôi là ảnh!">
<!-- Hiển thị ảnh trong trường hợp JavaScript bị tắt -->
<noscript>
  <img src="anh-can-lazy-load.jpg" alt="Tôi là ảnh!">
</noscript>

Nếu JavaScript bị tắt, người dùng sẽ thấy cả ảnh chiếm chỗ và ảnh được bao chứa trong phần tử <noscript>. Để xử lý vấn đề này, chúng ta đặt một class no-js trong thẻ <html> giống như thế này:

<html class="no-js">

Sau đó chúng ta đặt một dòng nội tuyến trong <head>, nó cần phải đứng trước bất cứ style sheets nào được yêu cầu thông qua thẻ <link> để loại bỏ class no-js của phần tử <html> nếu JavaScript được bật.

<script>document.documentElement.classList.remove("no-js");</script>

Cuối cùng, chúng ta có thể sử dụng vài dòng CSS đơn giản để ẩn các phần tử có class là lazy khi JavaScript không sẵn có, giống như thế này:

.no-js .lazy {
  display: none;
}

Điều này sẽ không ngăn ảnh chiếm chỗ tải về, nhưng đem lại kết quả như mong muốn. Những ai tắt JavaScript sẽ không phải thấy các ảnh giữ chỗ không có ý nghĩa trong nội dung.

Ví dụ sử dụng dự phòng <noscript>: https://code.speed.family/lazysizes-noscript.html

Kết luận

Khi được sử dụng cẩn trọng, lazy load ảnh và video có thể giúp bạn giảm đáng kể thời gian tải lần đầu, cũng như dữ liệu trang tải lần đầu. Người dùng không phải gánh chịu các hoạt động mạng và chi phí xử lý các tải nguyên đa phương tiện không cần thiết mà họ có thể không bao giờ thấy, nhưng họ có thể vẫn thấy được các tài nguyên đó nếu họ muốn.

Khi các kỹ thuật cải tiến hiệu suất ngày càng phát triển, lazy load là phương thức tăng tốc không gây tranh cãi. Nếu bạn có nhiều ảnh nội tuyến trong trang, đây là cách hoàn hảo để cắt giảm dữ liệu tải về không cần thiết. Người dùng của bạn và các bên liên quan thuộc dự án sẽ rất cảm ơn vì điều đó!

(Dịch từ bài viết Lazy Loading Images and Video, tác giả: Jeremy Wagner, trang web[.]dev)

Leave a Comment