logo头像
Snippet 博客主题

LazyLoad 图片懒加载

最近在对 react-daily 项目首页进行优化

V9XfQF.png

V9XinJ.png

在观察网络资源加载时发现首页打开需要加载大量图片,在网络较差情况下可能会造成白屏、卡顿等现象,而用户刚打开页面时并不需要那么多图片,只需要先加载首屏——即用户所能看到的图片,至于下面剩余的图片可以等用户下拉的瞬间再即时去请求,这样一来,性能的压力小了,用户的体验却没有变差,这个延迟加载的过程,就是 Lazy-Load,图片懒加载。

react-lazyload 库

一开始选择使用 react-lazyload 库,但发现使用后没有效果,页面打开只加载了首屏图片,往下拉时并没有加载相应的图片。

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

import React from 'react';
import ReactDOM from 'react-dom';
import LazyLoad from 'react-lazyload';
import MyComponent from './MyComponent';

const App = () => {
return (
<div className="list">
<LazyLoad height={200}>
<img src="tiger.jpg" /> /* Lazy loading images is supported out of box,
no extra config needed, set `height` for better
experience
*/
</LazyLoad>
<LazyLoad height={200} once >
/* Once this component is loaded, LazyLoad will
not care about it anymore, set this to `true`
if you're concerned about improvingperformance */
<MyComponent />
</LazyLoad>
<LazyLoad height={200} offset={100}>
/* This component will be loaded when it's top
edge is 100px from viewport. It's useful to
make user ignorant about lazy load effect. */
<MyComponent />
</LazyLoad>
<LazyLoad>
<MyComponent />
</LazyLoad>
</div>
);
};

ReactDOM.render(<App />, document.body);

查看源码

react-lazyload 有一个props为 overflow,默认为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (this.props.overflow) { // overflow 为true,向上查找滚动容器
const parent = scrollParent(ReactDom.findDOMNode(this));
if (parent && typeof parent.getAttribute === 'function') {
const listenerCount = 1 + (+parent.getAttribute(LISTEN_FLAG));
if (listenerCount === 1) {
parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent);// finalLazyLoadHandler 及passiveEvent 见下面
}
parent.setAttribute(LISTEN_FLAG, listenerCount);
}
} else if (listeners.length === 0 || needResetFinalLazyLoadHandler) { // 否则直接绑定window
const { scroll, resize } = this.props;

if (scroll) {
on(window, 'scroll', finalLazyLoadHandler, passiveEvent);
}

if (resize) {
on(window, 'resize', finalLazyLoadHandler, passiveEvent);
}
}

通过源码可以看到,这里当 overflow 为true时,调用 scrollParent 获取滚动容器,否者直接将滚动事件绑定在 window。

scrollParent 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/** * @fileOverview Find scroll parent */

export default (node) => {
if (!node) {
return document.documentElement;
}

const excludeStaticParent = node.style.position === 'absolute';
const overflowRegex = /(scroll|auto)/;
let parent = node;

while (parent) {
if (!parent.parentNode) {
return node.ownerDocument || document.documentElement;
}

const style = window.getComputedStyle(parent); //获取节点的所有样式
const position = style.position;
const overflow = style.overflow;
const overflowX = style['overflow-x'];
const overflowY = style['overflow-y'];

if (position === 'static' && excludeStaticParent) {
parent = parent.parentNode;
continue;
}

if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) {
return parent;
}

parent = parent.parentNode;
}

return node.ownerDocument || node.documentElement || document.documentElement;
};

这段代码比较简单,可以看到,scrollParent 默认是迭代向上查找 parentNode 样式的 overflow ,直到找到第一个 overflow 为 auto 或 scroll 的节点。然后返回该节点,作为滚动容器。

看到这里,基本可以知道首页列表懒加载无效的原因了,react-lazyload 仅支持 overflow 的滚动方式,而首页列表由于使用了 better-scroll 库,是以 transform 的滚动方式,所以懒加载没有效果。

verlok/lazyload

LazyLoad 是一个快速的,轻量级的,灵活的图片懒加载库,本质是基于 img 标签的 srcset 属性。

简单使用

HTML

1
2
3
<img alt="..." 
data-src="../img/44721746JJ_15_a.jpg"
width="220" height="280">

Javascript

1
var myLazyLoad = new LazyLoad();

基本原理

浏览器解析 html 的时候,在遇到 img 标签以及发现 src 属性的时候,浏览器就会去发请求拿图片去了。这里就是切入点,根据这种现象,做下面几件事:

  1. 把列表中所有的图片的 img 标签的 src 设为空
  2. 把真实的图片路径存成一个 dom 属性,打个比方:
  3. 写一个检测列表某一项是否是可见状态
  4. 全局滚动事件做一个监听,检测当前列表的项是否是可见的,如果可见则给 img 标签上存着真实图片路径赋值给 src 属性

IntersectionObserver

react-lazyload 库使用 getBoundingClientRect() 方法检测元素是否可见,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题。

而 lazyload 检测可见使用的是 IntersectionObserver API,可以自动”观察”元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。

IntersectionObserver 兼容性不好,不支持 IntersectionObserver 的浏览器,直接一次性显示图片。

在 React 中使用 lazyload

  1. 安装 vanilla-lazyload
1
yarn add vanilla-lazyload
  1. 封装 Lazyimage 组件

lazy-image.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from "react";
import LazyLoad from "vanilla-lazyload";
import lazyloadConfig from "./config/lazyload";

// Only initialize it one time for the entire application
if (!document.lazyLoadInstance) {
document.lazyLoadInstance = new LazyLoad(lazyloadConfig);
}

export class LazyImage extends React.Component {
// Update lazyLoad after first rendering of every image
componentDidMount() {
document.lazyLoadInstance.update();
}

// Update lazyLoad after rerendering of every image
componentDidUpdate() {
document.lazyLoadInstance.update();
}

// Just render the image with data-src
render() {
const { alt, src, srcset, sizes, width, height } = this.props;
return (
<img
alt={alt}
className="lazy"
data-src={src}
data-srcset={srcset}
data-sizes={sizes}
width={width}
height={height}
/>
);
}
}

export default LazyImage;

config/lazyload.js

1
2
3
export default {
elements_selector: ".lazy"
};

  1. 使用 Lazyimage 组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from "react";
import ReactDOM from "react-dom";
import LazyImage from "./lazy-image";

function App() {
return (
<div className="App">
<h1>ReactJS and vanilla-lazyload</h1>
<div class="images-container">
<LazyImage
sizes="200px"
srcset="https://placehold.it/200x300?text=Image2 200w, https://placehold.it/400x600?text=Image2 400w"
src="https://placehold.it/200x300?text=Image2"
alt="200x300"
width="200"
height="300"
/>
<LazyImage
sizes="200px"
srcset="https://placehold.it/200x300?text=Image3 200w, https://placehold.it/400x600?text=Image3 400w"
src="https://placehold.it/200x300?text=Image3"
alt="200x300"
width="200"
height="300"
/>
</div>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

参考链接

React 图片懒加载库源码解析
verlok/lazyload
lazyload Usage with React
移动Web滚动性能优化: Passive event listeners
IntersectionObserver API 使用教程
react-lazy-load粗读