当今的 web 页面
回顾一下浏览器从0开始加载网页的过程
现在访问一个 web 页面一般会经过这样的简易步骤:
- 用户在浏览器中输入 web 页面对应的域名
- 域名经过 DNS 服务器解析成对应的 IP 地址(如果是合法的话)
- 客户端与服务端通过三次握手建立 TCP 连接
- 浏览器(客户端)向服务器发起 HTTP 请求一个静态的 HTML 页面(e.g index.html,detail.html, home.html ....)
- 加载 css (不阻塞) 和 js (一般情况下阻塞) 文件
- 遇到一些需要加载资源的标签,比如
img
、video
、audio
这些标签时,它们会再次向服务器发送 HTTP 请求获取这些标签所需要的资源文件。 - ...
- 在最后时刻,通过四次挥手断开 TCP 连接
单服务器处理用户访问可能存在的问题
在浏览器从0开始加载网页的过程的第6步,如果当前页面对应的业务存在许多的资源,比如:
- 电商网站(淘宝,京东)
- 视频网站(B站、油管、抖音)
- 新闻网站(今日头条、网易新闻)
- ...
如果在单个服务器上存在大量的访问并且同时加载页面,必定会增加单个服务器的负担导致服务响应较慢甚至超时。
简单地实现服务器的分流
于是针对上述情况,现在很多网页都是这样处理的:
- 把所有的静态 HTML 页面放在自己的服务器上面
- 把脚本文件(css、js)放在带宽较高的托管资源服务器上面(e.g 七牛云、阿里云 ...)
- 把资源文件(图片、音频、视频)或单个、或切片的放在带宽较高的托管资源服务器上面(e.g 七牛云、阿里云 ...)
以百度举例:
- 请求 HTML
- 请求脚本文件
- 请求资源文件
那既然是这样,为什么还要图片优化呢?
服务器分流只是在服务器端的流量分配上面优化了服务器的带宽,但是它并不能解决用户在客户端(e.g 电脑,手机)上面可能还会存在的问题:
- 网络问题
- 网络巨卡,资源加载不出来,导致白屏
- 网络很卡,资源能加载但是加载缓慢,存在卡顿
- 网络小卡,资源能加载一部分,但是另外一部分白屏或者卡屏
- 用户体验问题
解决客户端可能存在问题的方式:
- 图片的懒加载
- 图片的预加载
- 显示前的骨架屏
- 雪碧图
本文着重讲述如何使用图片懒加载技术完成多图片网页的加载显示。
首先,什么是图片懒加载?
懒加载,顾名思义:相较于普通加载,它可以:
- 先统一加载一张 loading 图片(只加载一张图片,而不是全部一次性加载)
- 在图片滚动到可视区域范围内,把真正需要的图片地址替换掉之前加载的 loading 图片地址, 并且显示出来。
总体来说,懒加载以总体多加载一张 loading 图片的代价,让其他的图片能够按照特定的条件和时机进行加载,给予了用户一定的缓冲时间。对于一些多图的网站来讲,懒加载是一种不错的选择方案。
懒加载的整体思路:
懒加载的整体步骤如下:
- 列表渲染所有的图片,把图片对应的
<img>
标签的src
属性设置为 loading 图片的地址
html
<div class="img-list J_ImgList">
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/4.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" alt="" />
</div>
</div>
- 在图片对应的
<img>
标签设置一个自定义的属性data-src
, 它存放的值为真正需要加载的图片地址的值。
html
<div class="img-list J_ImgList">
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/1.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/2.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/3.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/4.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/5.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/6.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/7.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/8.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/9.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/10.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/11.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/12.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/13.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/14.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/15.jpg" alt="" />
</div>
<div class="img-list-item">
<img src="imgs/loading.png" data-src="imgs/16.jpg" alt="" />
</div>
</div>
- 写一下样式
css
html,
body {
margin: 0;
font-size: 15px;
color: #3d3d3d;
}
img {
display: block;
width: 100%;
height: 100%;
}
.img-list {
width: 100%;
}
.img-list::after {
content: "";
display: table;
clear: both;
}
.img-list .img-list-item {
width: 25%;
float: left;
height: 500px;
padding: 5px;
box-sizing: border-box;
}
- 写一个 IIFE 演示模块
javascript
;(function () {
var init = function () {
bindEvent();
}
function bindEvent() {}
init();
})();
- 获取这些图片元素,绑定事件处理函数
handleScroll
。
javascript
;(function (doc) {
var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
var init = function () {
bindEvent();
}
function bindEvent() {
window.addEventListener('scroll', handleScroll, false);
}
function handleScroll() {
// do something
}
init();
})(document);
- 封装一个函数
lazyLoadImg()
判断这些图片是否处于可视范围之内,如果处于可视范围之内并且存在data-src
属性,使用data-src
替换掉原来的src
的值,删除data-src
。
javascript
/**
* @function lazyLoadImg
* @description 懒加载图片元素
* @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
* @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
* @param {number} clientHeight 容器的整体高度
* @param {number} scrollTop 当前容器已滚动的高度
* @return {void} 没有返回值
*/
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
for (var i = 0; i < oImgItems.length; i++) {
var oImgItem = oImgItems[i];
var oImg = oImgItem.getElementsByTagName('img')[0];
var offsetTop = oImgItem.offsetTop;
if (clientHeight + scrollTop >= offsetTop) {
if (!!oImg.getAttribute('data-src')) {
oImg.setAttribute('src', oImg.getAttribute('data-src'));
oImg.removeAttribute('data-src');
}
}
}
}
- 设置
lazyLoadImg()
在 init 时执行; 在handleScroll
的时候,也执行一下lazyLoadImg()
javascript
;(function (doc) {
var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
var init = function () {
lazyLoadImg(
oImgItems,
document.documentElement.clientHeight,
document.documentElement.scrollTop || document.body.scrollTop
);
bindEvent();
}
function bindEvent() {
window.addEventListener('scroll', utils.throttle(handleScroll), false);
}
function handleScroll(e) {
var clientHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
lazyLoadImg(oImgItems, clientHeight, scrollTop);
}
/**
* @function lazyLoadImg
* @description 懒加载图片元素
* @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
* @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
* @param {number} clientHeight 容器的整体高度
* @param {number} scrollTop 当前容器已滚动的高度
* @return {void} 没有返回值
*/
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
for (var i = 0; i < oImgItems.length; i++) {
var oImgItem = oImgItems[i];
var oImg = oImgItem.getElementsByTagName('img')[0];
var offsetTop = oImgItem.offsetTop;
if (clientHeight + scrollTop >= offsetTop) {
if (!!oImg.getAttribute('data-src')) {
oImg.setAttribute('src', oImg.getAttribute('data-src'));
oImg.removeAttribute('data-src');
}
}
}
}
init();
})(document);
- 在 init 时执行时, 需要将页面滚动到顶部。
javascript
;(function (doc) {
var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
var init = function () {
window.scroll(0, 0);
lazyLoadImg(
oImgItems,
document.documentElement.clientHeight,
document.documentElement.scrollTop || document.body.scrollTop
);
bindEvent();
}
function bindEvent() {
window.addEventListener('scroll', utils.throttle(handleScroll), false);
}
function handleScroll(e) {
var clientHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
lazyLoadImg(oImgItems, clientHeight, scrollTop);
}
/**
* @function lazyLoadImg
* @description 懒加载图片元素
* @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
* @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
* @param {number} clientHeight 容器的整体高度
* @param {number} scrollTop 当前容器已滚动的高度
* @return {void} 没有返回值
*/
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
for (var i = 0; i < oImgItems.length; i++) {
var oImgItem = oImgItems[i];
var oImg = oImgItem.getElementsByTagName('img')[0];
var offsetTop = oImgItem.offsetTop;
if (clientHeight + scrollTop >= offsetTop) {
if (!!oImg.getAttribute('data-src')) {
oImg.setAttribute('src', oImg.getAttribute('data-src'));
oImg.removeAttribute('data-src');
}
}
}
}
init();
})(document);
代码优化:
优化点一:
滚动触发太频繁,使用节流函数控制单位时间内触发滚动的频率:
javascript
;(function (doc) {
var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
var init = function () {
bindEvent();
}
function bindEvent() {
window.scroll(0, 0);
lazyLoadImg(
oImgItems,
document.documentElement.clientHeight,
document.documentElement.scrollTop || document.body.scrollTop
);
window.addEventListener('scroll', throttle(handleScroll, 300), false);
}
function handleScroll(e) {
var clientHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
lazyLoadImg(oImgItems, clientHeight, scrollTop);
}
/**
* @function lazyLoadImg
* @description 懒加载图片元素
* @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
* @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
* @param {number} clientHeight 容器的整体高度
* @param {number} scrollTop 当前容器已滚动的高度
* @return {void} 没有返回值
*/
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
for (var i = 0; i < oImgItems.length; i++) {
var oImgItem = oImgItems[i];
var oImg = oImgItem.getElementsByTagName('img')[0];
var offsetTop = oImgItem.offsetTop;
if (clientHeight + scrollTop >= offsetTop) {
if (!!oImg.getAttribute('data-src')) {
oImg.setAttribute('src', oImg.getAttribute('data-src'));
oImg.removeAttribute('data-src');
}
}
}
}
init();
})(document);
javascript
/**
* @function throttle
* @description 节流函数
* @param {Function} fn 需要被节流执行的函数
* @param {number | undefined} delay 需要被节流的时长 (默认是 300 ms)
* @return {Function} 返回一个被节流的执行函数
*/
function throttle(fn, delay) {
var _delay = typeof delay === 'number' && delay > 0 ? delay : 300;
var timer = null;
var startTime = Date.now();
return function () {
var args = arguments;
var ctx = this;
var endTime = Date.now();
if (timer) {
clearTimeout(timer);
timer = null;
}
if (endTime - startTime >= _delay) {
startTime = endTime;
} else {
timer = setTimeout(function () {
fn.apply(ctx, args);
startTime = Date.now();
clearTimeout(timer);
timer = null;
}, endTime - startTime);
}
}
}
优化点二:
如果图片滚动到最底部,图片列表的懒加载就会失效。
这里需要在 init 的时候就把窗口滚动到最顶部,并且其他的程序延迟执行。
javascript
;(function (doc) {
var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
var init = function () {
var t = setTimeout(function() {
window.scrollTo(0, 0);
lazyLoadImg(
oImgItems,
document.documentElement.clientHeight,
document.documentElement.scrollTop || document.body.scrollTop
);
bindEvent();
clearTimeout(t);
t = null;
}, 300);
}
function bindEvent() {
window.addEventListener('scroll', utils.throttle(handleScroll), false);
}
function handleScroll(e) {
console.log('handleScroll', e);
var clientHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
lazyLoadImg(oImgItems, clientHeight, scrollTop);
}
/**
* @function lazyLoadImg
* @description 懒加载图片元素
* @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
* @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
* @param {number} clientHeight 容器的整体高度
* @param {number} scrollTop 当前容器已滚动的高度
* @return {void} 没有返回值
*/
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
for (var i = 0; i < oImgItems.length; i++) {
var oImgItem = oImgItems[i];
var oImg = oImgItem.getElementsByTagName('img')[0];
var offsetTop = oImgItem.offsetTop;
if (clientHeight + scrollTop >= offsetTop) {
if (!!oImg.getAttribute('data-src')) {
oImg.setAttribute('src', oImg.getAttribute('data-src'));
oImg.removeAttribute('data-src');
}
}
}
}
init();
})(document);
优化点三:
lazyLoadImg
需要作为一个纯函数,把它提取到工具外面,然后将工具导入到模块中使用
javascript
var utils = (function () {
/**
* @function lazyLoadImg
* @description 懒加载图片元素
* @description 容器的整体高度 + 当前容器已滚动的高度 > imgItem的偏移高度 ? 懒加载图片 : 不作处理
* @param {HTMLCollectionOf<HTMLElement>} oImgItems 图片元素集合
* @param {number} clientHeight 容器的整体高度
* @param {number} scrollTop 当前容器已滚动的高度
* @return {void} 没有返回值
*/
function lazyLoadImg(oImgItems, clientHeight, scrollTop) {
for (var i = 0; i < oImgItems.length; i++) {
var oImgItem = oImgItems[i];
var oImg = oImgItem.getElementsByTagName('img')[0];
var offsetTop = oImgItem.offsetTop;
if (clientHeight + scrollTop >= offsetTop) {
if (!!oImg.getAttribute('data-src')) {
oImg.setAttribute('src', oImg.getAttribute('data-src'));
oImg.removeAttribute('data-src');
}
}
}
}
/**
* @function throttle
* @description 节流函数
* @param {Function} fn 需要被节流执行的函数
* @param {number | undefined} delay 需要被节流的时长 (默认是 300 ms)
* @return {Function} 返回一个被节流的执行函数
*/
function throttle(fn, delay) {
var _delay = typeof delay === 'number' && delay > 0 ? delay : 300;
var timer = null;
var startTime = Date.now();
return function () {
var args = arguments;
var ctx = this;
var endTime = Date.now();
if (timer) {
clearTimeout(timer);
timer = null;
}
if (endTime - startTime >= _delay) {
startTime = endTime;
} else {
timer = setTimeout(function () {
fn.apply(ctx, args);
startTime = Date.now();
clearTimeout(timer);
timer = null;
}, endTime - startTime);
}
}
}
return {
lazyLoadImg: lazyLoadImg,
throttle: throttle,
};
})();
javascript
;(function (doc, utils) {
var oImgWrap = doc.getElementsByClassName('J_ImgList')[0];
var oImgItems = oImgWrap.getElementsByClassName('img-list-item');
var init = function () {
var t = setTimeout(function() {
window.scrollTo(0, 0);
utils.lazyLoadImg(
oImgItems,
document.documentElement.clientHeight,
document.documentElement.scrollTop || document.body.scrollTop
);
bindEvent();
clearTimeout(t);
t = null;
}, 300);
}
function bindEvent() {
window.addEventListener('scroll', utils.throttle(handleScroll), false);
}
function handleScroll(e) {
console.log('handleScroll', e);
var clientHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
utils.lazyLoadImg(oImgItems, clientHeight, scrollTop);
}
init();
})(document, utils);