Lazy loading images (Tải lười biếng) là một mẫu thiết kế thường được sử dụng trong lập trình máy tính để trì hoãn việc khởi tạo một đối tượng cho đến thời điểm cần thiết. Lazy loading không những giúp tăng hiệu xuất và thời gian tải web hiệu quả mà còn góp phần giúp bạn giảm đi một phần chi phí vận hành bởi người dùng chỉ nhận được những gì họ cần thay vì tải lên tất cả.
Hình ảnh là thành phần rất quan trọng đối với bất kỳ website nào, nó có thể là banner, button, hình sản phẩm, logo… và thường giúp làm cho website sinh động hơn, sặc sỡ hơn. Nhưng đáng buồn nay nó cũng là thứ nặng nhất, chiếm kích thước lớn nhất trong quá trình bạn tải site về.
Trong bài viết này, mình sẽ nói về kĩ thuật tải Lazy Loading, giúp bạn tải nội dung(ở đây là hình ảnh) bằng cách ngăn chặn quá trình tải toàn bộ trang web, thay vào đó sẽ tải lên từng phần.
Phụ mục
1. Kĩ thuật Lazy Loading Images
Một hình ảnh có thể được tải lên website và hiển thị theo 2 cách: hoặc sử dụng thẻ <img> hoặc sử dụng thuộc tính background. Trước tiên, hãy nói về thẻ <img> trước, sau đó chúng tao sẽ nói về background sau.
1.1 Ý tưởng về phương pháp Lazy loading Image cho <img>
Về cơ bản, khi bạn dùng một thẻ <img>, trình duyệt sẽ sử dụng thuộc tính src=”…” để kích hoạt hình ảnh thông qua URL. Bất kể đó là hình ảnh thứ bao nhiêu, bên trong hay bên ngoài màn hình đầu tiên đi chăng nữa, cứ đụng img src=”” là trình duyệt sẽ tự động kích hoạt tải ảnh.
Bây giờ, để trì hoãn quá trình tải ảnh, chúng ta cần đặt URL của ảnh bên ngoài SRC, nhằm làm cho ảnh không được tải lên, ở đây chúng ta có thể sử dụng thuộc tính data-src. Như vậy, lúc này src không chứa ảnh và ảnh sẽ không được tải.
Bây giờ thì ảnh đã bị ngừng tải theo mặc định của trình duyệt. Để kích hoạt lại quá trình tải ảnh khi cần thiết, bạn cần xác định thời điểm nào ảnh nên được tải lên – là thời điểm mà ảnh ở chỗ độ xem cần thiết cho người dùng. Chúng ta có 2 cách để làm việc này:
1.2 Trigger image load sử dụng Javascript events
Trong kỹ thuật này chúng tạo sử dụng các event của trình duyệt như scroll, resize, và orientationChange. Trong đó, event scroll là một event dễ dùng và rõ ràng nhất để phân biệt người dùng rời khỏi màn hình đầu tiên. Các sự kiện resize(thay đổi kích thước), và orientationChange(xoay màn hình) đều quan trọng ngang nhau và phụ thuộc vào ý đồ thiết kế của bạn. Trong trường hợp số lượng ảnh có thay đổi, bạn cần kích hoạt cho từng event theo ý đồ của mình.
Khi một hình ảnh đang ở chế độ xem (viewport), chúng ta sẽ kích hoạt Lazy Loading bằng cách lấy giá trị URL từ thuộc tính data-src để đặt vào thuộc tính src, từ đó kích hoạt quá trình tải ảnh lên và hiển thị.
Chúng ta có thể dọn dẹp bằng cách loại bỏ trình kích hoạt Lazy Loading sau khi ảnh đã được tải lên để tránh kích hoạt lại lần nữa cũng như loại bỏ event listener không dùng đến nữa.
1.3 Sử dụng Intersection Observer API để trigger
Intersection Observer API là một API tương đối mới nên ít người sử dụng. Nó giúp bạn xác định vùng nào trên web đang ở chế độ xem và việc gì bạn muốn làm tiếp theo.
Tương tự như javascript ở trên, bạn cũng lấy dữ liệu từ thuộc tính data-src để truyền vào src và kích hoạt tải ảnh, nhưng lần này bạn sử dụng API thay vì dùng javascript.
Intersection Observer API là một giải pháp đơn giản hơn, giảm thiểu đáng kể thuật toán và mang lại hiệu suất tuyệt vời nhưng tiếc là ít người biết đến và sử dụng.
Nếu phải lựa chọn giữa 2 phương pháp: sử dụng Javascript và Intersection Observer API
thì mình khuyên bạn nên sử dụng Intersection Observer API vì tính đơn giản của nó. Tuy nhiên nó cũng có một vấn đề là Intersection Observer API không hỗ trợ nhiều trình duyệt nên bạn phải cân nhắc.
2. Native Lazy Loading
Trong phiên bản cập nhật của Chrome gần đây, Google đã chính thức hỗ trợ Native Lazy Loading, chính thúc là từ phiên bản Chrome 76. Vói phương pháp và hỗ trợ mới này, chúng ta chỉ cần thêm thuộc tính loading vào khi nhúng hình ảnh là chúng ta đã có Lazy Loading xịn xò.
Chỉ cần một chút kiến thức HTML cơ bản là làm được, ko cần phải là dev ghê gớm:
<iframe src="example.html" loading="lazy"></iframe> |
Thuộc tính loading hỗ trợ 3 giá trị sau:
- lazy: trì hoãn tải nội dung cho đến khi nó đạt một khoảng cách nhất định đến chế độ xem.
- eager: tải tất cả ảnh khi tải trang, bất kể nó nằm ở vị trí nào trên trang, có ở chế độ view hay không.
- auto: giá trị này là mặc định. Về cơ bản, nó cũng giống như bạn không nhúng thuộc tính loading
Tuy nhiên, không phải tất cả các trình duyệt đều hỗ trợ thuộc tính loading. Để sử dụng nó cho tất cả các trình duyệt bạn cần một nỗ lực lớn hơn. Dù sao thì thuộc tính này mình nghĩ cũng sớm được hỗ trợ thôi vì nó là rất cần thiết.
3. Lazy Loading CSS Background Images
Đứng sau thẻ <img> thì thuộc tính bacground cũng là rất thông dụng mỗi khi bạn cần tải một ảnh.
Để tải nền ảnh, về kĩ thuật có vẻ phức tạp hơn một chút so với tải ảnh với thẻ img. Trình duyệt sẽ xây dựng DOM((Document Object Model) sau đó là CSSOM (CSS Object Model) để quyết định xem CSS có được áp dụng cho node DOM đó hay không. Nghe rắc rối nhỉ !
Để áp dụng Lazy Loading cho thuộc tính background, bạn cần một mẹo nho nhỏ đển “trick” quyết định CSS có áp dụng cho node hay không. Về nguyên tắc, mã JS nếu có cũng sẽ giống nhau, vẫn áp dụng Intersection Observer API nếu bạn muốn.
Chúng ta sẽ có một khối DIV với ID bg-image và có thuộc tính background-image đầy đủ. Tuy nhiên chúng ta cũng sẽ có thêm một class .lazy thêm vào chính node này. Class lazy sẽ có thêm background-image có giá trị là none để override lên thuộc tính em>background-image của node.
Khi chuột scroll, hoặc sử dụng Intersection Observer API (hay các listener bất kỳ), khi xác định được hình ảnh đang ở chế độ xem, chúng ta tiến hành gỡ bỏ lớp .lazy và thuộc tính background-image của ID bg-image sẽ được áp dụng.
4. Trải nghiệm người dùng tốt hơn với Lazy Loading
Lazy Load mang lại một trải nghiệm tốt hơn và đem lại sự tiết kiệm băng thông đáng kể, đối với cấc website thương mại điện tử thì sử dụng lazy load quả thật là một lựa chọn trải nghiệm tuyệt vời.
Tuy nhiên, nhiều công ty vì lý do kỹ thuật (có thể là thiếu kiến thức, không có nền tảng kỹ thuật phù hợp, ngại thay đổi…), hay vì họ tin tưởng rằng lazy loading đi ngược lại với trải nghiệm người dùng(lazy load sẽ làm trang web chậm hơn do tải chậm, nội dung không được cung cấp một cách tức thì…)
Là một developer, làm thế nào để bạn giải quyết nỗi bận tâm đó ? Dưới đây là một vài gợi ý mà có thể bạn sẽ tìm kiếm được câu trả lời hoặc giải pháp cho vấn đề này.
4.1 Sử dụng placeholder đúng lúc, đúng chỗ
Placeholder không chỉ là thay thế text trong thẻ input như bạn nghĩ, nó còn có tác dụng “giữ chỗ” trrong lúc đợi ảnh được tải lên. Thông thường, chúng ta sẽ thấy các dev sử dụng màu sắc để giữ chỗ trong lúc đợi ảnh tải lên, Placeholder giữ chỗ có thể sử dụng màu sắc hoặc một ảnh nào đó.
Hãy xem một vài ví dụ dưới đây:
a) placeholder bằng màu sắc chủ đạo
Thay vì sử dụng một màu đơn sắc cho tất cả các ảnh (như cấch Facebook đang làm ở thời điểm này) bạn có thể sử dụng màu chủ đạo của ảnh được thế chỗ để giữ vị trí.
Kỹ thuật này đã được sử dụng khá lâu bởi Google và Pinterest cho phần hiển thị kết qảu tìm kiếm liên quan đến ảnh.
Có một thư viện cho bạn, đó là Manu.ninja
Mới nhìn trông có vẻ khó làm và lằng nhằng nhưng thực ra nó được thực hiện rất đơn giản nếu bạn hiểu rõ nguyên lý hoạt động. Đầu tiên bạn thu nhỏ ảnh xuống kích thước 1×1 pixel, sau đó thu nhỏ kích thước của placeholder với một tỉ lệ xấp xỉ. Vậy là bạn đã có màu chủ đạo.
Sử dụng ImageKit, bạn có thể xác định màu chủ đạo một cách dễ dàng, hãy nhìn ví dụ sau đây:
<!-- Original image at 400x300 --> <img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" alt="original image" /> <!-- Dominant colour image with same dimensions --> <img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-1,h-1:w-400,h-300" alt="dominant color placeholder" /> |
Placeholder chúng ta sử dụng chỉ có kích thước là 661 bytes so với ảnh gốc mà chúng ta sử dụng là 12700 bytes giúp cho nó nhỏ hơn đến 19 lần. Sau đây bạn sẽ muốn xem ví dụ minh họa khi nó hoạt động.
Nếu muốn, hãy xem thêm ví dụ minh họa ở đây.
b) Ảnh placeholder có chất lượng thấp hơn (LQIP)
LQIP – Low quality image placeholder: Mở rộng ý tưởng về placeholder, thay vì sử dụng màu sắc bạn có thể tạo ra một ảnh mờ hơn, chất lượng thấp hơn ảnh gốc để làm ảnh giữ chỗ trước khi ảnh gốc được tải. Nhờ đó ảnh sẽ nhẹ hơn rất nhiều lần. Đây là một ý tưởng cũng khá hay ho để tối ưu hóa tốc độ.
Kỹ thuật này cũng được ứng dụng bởi Facebook và Medium.com.
Hãy xem cách họ làm LQIP sử dụng ImageKit:
<!-- Original image at 400x300 --> <img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" alt="original image" /> <!-- Low quality image placeholder with same dimensions --> <img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300,bl-30,q-50" alt="dominant color placeholder" /> |
Ảnh LQIP là 1300 bytes, ít hơn hơn 10 lần so với ảnh gốc. Còn đây là hoạt động của nó:
Bạn có thể xem đoạn mã ví dụ cho kỹ thuật này ở đây.
4.2 Thêm bộ đệm trong lúc tải ảnh
Khi thảo luận về phương pháp tải ảnh, chúng tôi phát hiện ra rằng thời điểm ảnh đi vào chế độ xem (nghĩa là bắt đầu tải) khi phần trên của placeholder trùng với cạnh dưới của chế độ xem. Điều này mở ra một ý tưởng về phương pháp tải một phần ảnh tương ứng với chế độ xem.
Vấn đề
Người dùng thường cuộn chuột rất nhanh trong khi hình ảnh cần một chút thời gian để tải lên. Trong trường hơp thực tế, cho dù bạn có đường truyền tốt thì người dùng cũng phải mất thêm vài trăm millisecond để tải ảnh. Điều này tạo ra một trải nghiệm người dùng kém.
Bằng việc sử dụng Intersection Observers đã nói ở trên, chúng ta có thể giải quyết được việc đảm bảo ảnh được tải hoàn toàn trước khi vào chế độ xem. Đây cũng là một mẹo nhỏ mà bạn nên lưu tâm.
Giải pháp
Thay vì bạn tải ảnh khi chúng được kích hoạt ở điểm dưới cùng của chế độ xem, hãy xem xét tải ảnh khi chúng cách chế độ xem tầm 500px. Việc này cung cấp thêm thời gian để ảnh có thời gian nhiều hơn khi tải lên.
Với Intersection Observer API, bạn có thể sử dụng tham số “root” cùng với “rootMargin” (làm việc tốt cùng với CSS tiêu chuẩn) để tìm ra giới hạn cần thiết để tải ảnh. Thay vì kiểm tra cạnh dưới của chế độ xem và cạnh trên của ảnh bằng 0, chúng ta sẽ sử dụng một khoảng cách khác phù hợp hơn, ở ví dụ này là 500px.
Bạn có thể xem thêm code ở đây
Như video dưới đây, khi hình ảnh thứ 4 đang ở chế độ xem, hình ảnh thứ 5 sẽ được tải và khi bạn xem hình ảnh thứ 5, hình ảnh thứ 6 sẽ được tải lên.
4.3 Tránh nội dung thay đổi trong lúc tải lazy loading
Đây cũng là một điểm đáng lưu tâm nếu như bạn muốn cải thiện trải nghiệm người dùng tốt hơn.
Vấn đề
Khi không có hình ảnh, trình duyệt sẽ không thể định hình vùng chứa(container) phù hợp với ảnh đó và hệ quả là vùng chứa ảnh sẽ được thu nhỏ lại, sẽ bung ra khi có ảnh tương ứng. Điều này kéo các vùng chứa tiếp theo về phía trước làm xô lệch layout.
Khi bạn dùng Lazy Loading, kích thước thông thường của ảnh thay thế sẽ là 1×1 pixel (hoặc 0 nếu là background) – vì vậy container sẽ tiếp thay đổi kích thước khi ảnh được tải lên. Đây là một trải nghiệm cực kì khó chịu đối với người dùng.
Giải pháp
Định hình vùng chứa với chiều rộng(width) và chiều cao (height) xác định. Nhờ thứ này, trình duyệt có thể căn chỉnh chính xác vị trí và kích thước khung chứa mà không bị xô lệch. Khi nội dung được tải lên, nó sẽ được hiển thị chính xác trong khung chứa một cách hoàn hảo.
4.4 Không dùng Lazy Loading cho tất cả ảnh
Đây là một điều mà nhiều dev hay vướng phải – nguyên nhân chủ yếu là do lười biếng. Tuy rằng điều này có thể giúp tăng tốc độ tải trang lúc đầu nhưng vì Lazy Loading sẽ trì hoãn việc tải hình ảnh (nếu nó là những hình ảnh đầu tiên) thì đây thưc sự là một trải nghiệm người dùng chẳng tốt gì cả.
Dưới đây là một số nguyên tắc quan trọng:
- Bất kỳ hình hình ảnh nào được tải ở đầu trang web cũng không nên tải Lazy Loading – áp dụng cho bất kỳ hình ảnh, logo, banner bởi người dùng sẽ thấy chúng ngay từ màn hình đầu tiên. Ngoài ra, mỗi màn hình có một kích thước màn hình khác nhau, chúng sẽ có số lượng hình ảnh hiển thị khác nhau cho nên bạn cần phải tính toán loại tài nguyên nào nên được tải lên trước, hoặc sau.
- Bất kỳ hình ảnh nào “hơi” ngoài tầm của chế độ xem cũng không nên tải bằng Lazy Loading – điều này mình đã nói đến ở phần đầu liên quan đến vị trí giữa góc dưới chế độ xem và góc trên của ảnh.
- Nếu trang không quá dài, có thể chỉ cần 1 hoặc 2 lần cuộn chuột hoặc dưới 5 ảnh ở mỗi chế độ xem thì có thể không cần dùng đến Lazy Loading. Việc này là vì chẳng tiết kiệm được lợi ích nào đáng kể mà còn phải phát sinh thêm một đống mã thừa. Việc tải thêm mã Lazy Loading nhiều khi còn nhiều hơn nội dung tải ảnh.
5. Thư viện Javascript cho Lazy Loading
Thật may mắn vì có rất nhiều thư viện Javascript hỗ trợ Lazy Load cho chúng ta.
yall.js (Yet Another Lazy Loader)
Sử dụng Intersection Observer và hàm falls back của sự kiện để tải Lazy Load.
Nó hỗ trợ hầu hết các thẻ HTML nhưng hơi tiếc một chút là không làm việc với background-images.
lazysizes
Rất phổ biến và tiện dụng. Hỗ trợ hầu hết các định dạng ảnh với srcset và sizes attribute.
Hiệu suất tốt hơn nếu không có Intersection Observer.
WeltPixel Lazy Loading Enhanced
Một Magento 2 extension cho bạn nào xài Magento.
Magento Lazy Image Loader
Tương tự,đây cũng là một extension dành cho Magento
WordPress A3 Lazy Load
Một plugin cho WordPress.