Triển khai Lazy Loading Image trong React

Posted on September 27th, 2018

Đối với hầu hết các trang web thì ảnh là thứ không thể thiếu. Tuy nhiên, nếu ảnh có kích thước và dung lượng lớn, mà ta lại load ảnh theo cách thông thường thì sẽ dẫn đến ảnh load bị chậm, làm phá vỡ cấu trúc giao diện. Điều này làm giảm trải nghiệm của người dùng. Để khắc phục tình trạng này, người ta thường sẽ sử dụng phương pháp Lazy Loading Image. Vậy Lazy Loading Image là gì và cách triển khai Lazy Loading Image trong React như thế nào?

Mời bạn theo dõi bài viết!

Tư tưởng chính của phương pháp Lazy Loading Image

Về cơ bản, phương pháp Lazy Loading Image gồm các bước sau:

  • Bước 1: Sử dụng ảnh đơn sắc, hoặc ảnh đã làm mờ đóng vai trò là place holder (giữ chỗ). Vì những ảnh này có kích thước rất nhỏ (cỡ 200 byte là đủ) nên sẽ được load rất nhanh. Do đó, giao diện chương trình sẽ không bị vỡ.
  • Bước 2: Kiểm tra các phần tử DOM liên quan đến hình ảnh xem nó có ở trên viewport (phạm vi màn hình) hay không. Nếu có thì sẽ load ảnh gốc một cách bất đồng bộ. Sau khi load ảnh xong thì mới set lại ảnh gốc đó cho phần tử DOM.
  • Bước 3:Trong quá trình scroll màn hình, thực hiện lại Bước 2 cho đến khi tất cả các ảnh gốc được load hết thì thôi.

Tư tưởng chính là vậy. Bây giờ, mình sẽ bắt đầu triển khai Lazy Loading Image trong React.

Triển khai Lazy Loading Image trong React

Chuẩn bị ảnh Place Holder

Bạn có thể sử dụng bất kì công cụ chỉnh sửa ảnh nào để tạo ảnh này. Còn mình thì sử dụng Dynamic Dummy Image Generator cho nhanh. Bạn có thể vào trang đó để xem cách sử dụng hoặc đơn giản sử dụng đường link này: https://dummyimage.com/674x384/fff/ffffff.png. Ảnh tải về sẽ có dung lượng cỡ 1 KB.

Tiếp theo, bạn có thể vào trang tinypng.com để giảm dung lượng ảnh lại. Ảnh sau optimize sẽ có dung lượng cỡ 126 B thôi. Như vậy là đủ nhỏ rồi phải không bạn?

Xem code trên Github Xem Demo

Code toàn bộ component Lazy Loading Image

lazy-image.js

import React from 'react';
import "./lazy-image.css";

function elementInViewport(el) {
  const rect = el.getBoundingClientRect();

  return (
    rect.top >= 0
    && rect.left >= 0
    && rect.top <= (
      window.innerHeight || document.documentElement.clientHeight
    )
  )
}

export default class LazyImage extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      loaded: false
    }

    this.handleScroll = this.handleScroll.bind(this);
  }

  componentDidMount() {
    this.handleScroll();

    window.addEventListener('scroll', this.handleScroll);
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll);
  }

  handleScroll() {
    if (!this.state.loaded && elementInViewport(this.imgElm)) {
      // Load real image
      const imgLoader = new Image();
      imgLoader.src = this.props.src;
      imgLoader.onload = () => {
        const ratioWH = imgLoader.width / imgLoader.height;

        this.imgElm.setAttribute(
          `src`,
          `${this.props.src}`
        );

        this.props.keepRatio && this.imgElm.setAttribute(
          `height`,
          `${this.props.width / ratioWH}`
        )

        this.imgElm.classList.add(`${this.props.effect}`);

        this.setState({
          loaded: true
        });
      }
    }
  }

  render() {
    const width = this.props.width || '100%';
    const height = this.props.height || '100%';

    return (
      <img
        src={this.props.placeHolder}
        width={width}
        height={height}
        ref={imgElm => this.imgElm = imgElm}
        className="lazy-image"
        alt={this.props.alt}
      />
    )
  }
}

lazy-image.css

.lazy-image {
  opacity: 0;
  transition: opacity 0.1s ease-in-out;
  -webkit-transition: opacity 0.1s ease-in-out;
  -moz-transition: opacity 0.1s ease-in-out;
}

.lazy-image.opacity {
  opacity: 1;
}

Giải thích cách triển khai Lazy Loading Image trong React

Hàm khởi tạo

Khởi tạo biến state loaded. Biến này dùng để lưu trạng thái ảnh đã được load hay chưa. Ban đầu ảnh chưa được load nên giá trị này là false.

this.state = {
  loaded: false
}

Bind function handleScroll với this. Function này dùng để xử lý event khi scroll màn hình.

this.handleScroll = this.handleScroll.bind(this);

Hàm render

Hàm này tương ứng với Bước 1 mà mình đã nói phía trên. Trong hàm này, mình thực hiện các công việc sau đây.

Tính giá trị của widthheight. Nếu người dùng không truyền 2 thuộc tính này vào thì mặc định giá trị là 100%

const width = this.props.width || '100%';
const height = this.props.height || '100%';

Component này khi render lên HTML sẽ đóng vai trò là một thẻ img với các thuộc tính là:

  • src={this.props.placeHolder} : ban đầu gán src của ảnh là ảnh Place Holder mà mình đã tải ở phần trước.
  • width={width}: xác định chiều rộng ảnh
  • height={height}: xác định chiều cao ảnh
  • ref={imgElm => this.imgElm = imgElm}: định nghĩa biến this.imgElm chính là reference đến phần tử DOM
  • className="lazy-image": định nghĩa tên class, dùng để tuỳ biến style trong file lazy-image.css. Mình đang để mặc định opacity: 0 - nghĩa là ảnh ban đầu sẽ bị ẩn đi. Khi nào ảnh gốc được load xong mình sẽ gán opacity: 1 để tạo hiệu ứng xuất hiện. Bạn có thể tuỳ biến tuỳ theo ý thích.
  • alt={this.props.alt}: định nghĩa thuộc tính alt dùng để hiển thị khi không tải được ảnh

Hàm componentDidMount

Hàm componentDidMount dùng để xử lý khi component đã được mount (đơn giản là nó được render lên HTML). Trong hàm này, mình làm 2 việc:

  • Kiểm tra xem component có đang nằm trong viewport hay không (nếu có thì mình sẽ load ảnh gốc lên) bằng cách gọi hàm this.handleScroll() - tương ứng với Bước 2.
  • Đăng ký sự kiện scroll của màn hình Window - dùng để xử lý Bước 3.
window.addEventListener('scroll', this.handleScroll);

Hàm componentWillUnmount

Hàm componentWillUnmount dùng để xử lý trước khi component bị huỷ. Lúc này, mình phải huỷ sự kiện đã đăng ký ở phần trước, để tránh bị leak memory.

window.removeEventListener('scroll', this.handleScroll);

Hàm handleScroll

Như mình đã nói ở trên, hàm này dùng để xử lý khi scroll màn hình. Trong hàm này, mình sẽ phải kiểm tra xem ảnh gốc đã được load hay chưa - thông qua biến this.state.loaded. Đồng thời kiểm tra xem phần tử DOM có đang nằm trong viewport hay không - thông qua hàm elementInViewport.

function elementInViewport(el) {
  const rect = el.getBoundingClientRect();

  return (
    rect.top >= 0
    && rect.left >= 0
    && rect.top <= (window.innerHeight || document.documentElement.clientHeight)
  )
}

Rõ ràng, nếu ảnh đã được load hoặc nó không nằm trên viewport thì mình không cần thiết phải gọi phần xử lý load ảnh làm gì cả. Ngược lại, mình sẽ phải load ảnh gốc lên thông qua đối tượng Image.

Đầu tiên, khởi tạo đối tượng Image:

const imgLoader = new Image();

Tiếp theo, gán thuộc tính src với đường dẫn ảnh gốc cần tải:

imgLoader.src = this.props.src;

Định nghĩa hàm onload - hàm này được gọi khi ảnh đã được load hoàn toàn. Việc load ảnh là bất đồng bộ nên sẽ không ảnh hưởng đến hoạt động của trang web.

Trong hàm onload mình thực hiện các công việc đó là:

Tính tỉ lệ chiều rộng / chiều cao của ảnh thực tế const ratioWH = imgLoader.width / imgLoader.height. Mục đích là để điều chỉnh lại tỉ lệ chiều rộng / chiều cao nếu cần thiết.

Gán src với đường dẫn ảnh gốc cần tải.

this.imgElm.setAttribute(
  `src`,
  `${this.props.src}`
);

Nếu người dùng muốn giữ nguyên tỉ lệ ảnh gốc thì mình sẽ điều chỉnh lại chiều cao của ảnh

this.props.keepRatio && this.imgElm.setAttribute(
  `height`,
  `${this.props.width / ratioWH}`
)

Về cơ bản, khi đến bước này thì ảnh đã sẵn sàng để hiển thị. Nhưng người dùng vẫn chưa nhìn thấy ảnh, vì opacity: 0. Do đó, mình sẽ thêm vào thuộc tính class của ảnh giá trị this.props.effect. Mặc định, mình mới định nghĩa 1 effect là opacity.

this.imgElm.classList.add(`${this.props.effect}`);

Định nghĩa trong css:

.lazy-image.opacity {
  opacity: 1;
}

Đến đây thì ảnh hoàn toàn được hiện lên, công việc cuối cùng là thay đổi state this.state.loaded. Mục đích là khi mình scroll màn hình, phần xử lý này sẽ không được gọi nữa - đảm bảo chỉ gọi phần load ảnh đúng 1 lần.

this.setState({
  loaded: true
});

Như vậy, mình đã định nghĩa xong component LazyImage. Bây giờ, chỉ cần áp dụng nó thôi.

Cách sử dụng component LazyImage

Mình có làm một ví dụ trên Github. Bạn có thể tham khảo source code tại đây. Hoặc xem demo trực tiếp tại đây.

Ví dụ demo mình sử dụng create-react-app. Bạn có thể tham khảo thêm tại bài viết Tạo và deploy ứng dụng React lên Github Pages để biết cách tạo project và deploy lên Github Pages.

Trong App.js mình đã sử dụng component LazyImage như sau:

<LazyImage
  placeHolder={placeHolder}
  src={props.src}
  width={`100%`}
  height={`auto`}
  effect={"opacity"}
  alt={props.alt}
/>

Về chi tiết, bạn có thể tham khảo thêm trong project demo của mình. Có gì khó hiểu, bạn có thể để lại trong phần bình luận phía dưới, mình sẽ cố gắng giải đáp.

Lời kết

Trên đây là cách mà mình đã triển khai Lazy Loading Image trong React. Hy vọng bạn có thể hiểu được tư tưởng chính của phương pháp này và tự implement theo cách của riêng mình. Còn phần ví dụ của mình chỉ dùng để tham khảo nên có thể sẽ có issue. Mong bạn thông cảm.

Tiếp theo, mình dự định sẽ tìm hiểu và chia sẻ tiếp về Lazy Loading Video, Lazy Loading JavaScript,... Rất mong bạn sẽ theo dõi blog của mình thường xuyên. Và nếu bạn thấy bài viết này hay thì có thể cho mình đánh giá ở phần dưới.

Xem code trên Github Xem Demo

Xin chào và hẹn gặp lại bạn trong bài viết tiếp theo, thân ái!


★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé: