18-08-2019 • ☕️ ☕️ 5 phút trướcReact js
Cụ thể là tôi đang nói về images. Hình ảnh có thể tiêu tốn rất nhiều băng thông (con số này lên tới 70% đối với một số website). Bạn phải trả phí để gửi chúng đi, đồng thời người dùng của bạn cũng bị tính phí để xem chúng. Trên thực tế, cả bạn và người dùng đều phải trả tiền cho cả những hình ảnh không bao giờ được nhìn thấy, bởi vì khách truy cập nhiều khi còn không bao giờ cuộn xuống đủ xa để xem chúng.
Hình ảnh không chỉ ảnh hưởng đến hiệu suất thực tế của website mà còn ảnh hưởng đến cả hiệu suất trực quan nữa. Hiệu suất trực quan (perceived performance) tạo cho website "cảm giác" nhanh hơn ngay cả khi website đó đã đang load với tốc độ rất nhanh rồi.
<br />
Về cơ bản, phương pháp Lazy Loading Image gồm các bước sau:
Nghe cũng EZ nhỡ. Giờ thì mình đi vào code nào.
import React from "react"; 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); } componentWillUnmount() { window.removeEventListener("scroll", this.handleScroll); } handleImageLoaded = () => { this.handleScroll(); window.addEventListener("scroll", this.handleScroll); }; handleImageErrored = () => { console.log("error load image"); }; 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).toString()); 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" onLoad={this.handleImageLoaded} onError={this.handleImageErrored} alt={this.props.alt} /> ); } }
Một tí transition cho nó đỡ tức mắt :)
.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; }
<LazyImage placeHolder={item} src="https://miro.medium.com/max/1400/1*K0a7xINk0RM5gfXGSN68cw.png" width={"100%"} height={"auto"} effect={"opacity"} alt={"test lazy image"} keepRatio={true} />
Khởi tạo biến state loaded. Biến này dùng để lưu trạng thái ảnh đã được load ảnh gốc hay chưa. Ban đầu ảnh chưa được load nên giá trị này là false.
this.state = { loaded: false }
Nếu người dùng không truyền css width và height vào thì mặc định nó là '100%' hoặc bạn có thể dùng defaultProps để định nghĩa giá trị của 2 biến đó.
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" onLoad={this.handleImageLoaded} onError={this.handleImageErrored} alt={this.props.alt} /> ); }
Sau khi ảnh placeHolder load xong thì sẽ thực thi function handleImageLoaded trong này cho hàm this.handleScroll(); chạy để bắt những cái ảnh hiện đang nằm trong viewport khi chưa scroll. Lệnh tiếp theo là lắng nghe sự kiện scroll của window.
handleImageLoaded = () => { this.handleScroll(); window.addEventListener("scroll", this.handleScroll); };
function elementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) ); }
Trong function handleScroll ta thực hiện các bước sau:
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).toString()); this.imgElm.classList.add(`${this.props.effect}`); this.setState({ loaded: true }); }; } }