浅谈图片分层加载与懒加载
# 技术介绍
# 一、 图片懒加载
按需加载,即用户滚动页面到一定位置(可视区域)时才触发图片的加载;
常用于图片较多的网页,可以延迟网络请求,页面更快load
# 效果
# 实现原理
<img>
节点不使用src属性,将图片地址放在其他自定义属性上- 当页面滚动到一定位置,将自定义属性上的图片地址赋值给src
主要有两种方案
# IntersectionObserver
一个用于判断 dom 节点是否处于视口的API
(引用网上图片)
简单代码:
var intersectionObserver = new IntersectionObserver(function(entries) {
entries.forEach(function (entry) {
//intersectionRatio:该元素的可见性比例
if (entry.intersectionRatio > 0) {
//对其dom节点(entry.target)进行下一步操作
}
});
},{
// 用于计算相交区域的根元素,默认document顶层文档的视口
root:null,
// 指定到root的距离,用于扩大或缩小交叉区域面积,一般用于提前/延迟懒加载
// 与margin一样跨域用4个值,允许负值
// 显示指定root时才可使用百分比值
rootMargin:"0px",
// 触发回调函数的临界值,用 0 ~ 1 的比率指定,也可以是一个数组。
// 其值是被观测元素可视面积 / 总面积
// 当可视比率经过这个值的时候,回调函数就会被调用。
thresholds:[0]
});
document.querySelectorAll('img').forEach(v=>intersectionObserver.observe(v))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
兼容性:较差,caniuse
# scroll+requestAnimationFrame+getBoundingClientRect
监听窗口滚动和大小变化事件,利用 requestAnimationFrame 进行节流
判断元素是否在视区中代码:
function inViewport(node) {
//文档滚动距离
var viewTop = getScrollY()
var viewBot = viewTop + windowHeight
//节点距离文档顶部的距离
var nodeTop = getNodeTop(node)
var nodeBot = nodeTop + getNodeOffsetHeight(wsnode)
var offset = (settings.lazyLoadStrategy.threshold / 100) * windowHeight
return (nodeBot >= viewTop - offset) && (nodeTop <= viewBot + offset)
}
2
3
4
5
6
7
8
9
10
11
12
兼容性:IE7+测试通过
# 二、 分层加载
先显示一张模糊的图片,后面再换成原图
# 效果
# 实现原理
<img id="img" src="small.jpg" data-original="big.jpg" />
let smallImage = document.getElementById('img')
let imgLarge = new Image();
imgLarge.src = smallImage.getAttribute("data-original")
imgLarge.onload = function(){
smallImage.src = imgLarge.src
}
2
3
4
5
6
# 3. 懒加载+分层加载
<img src="small.jpg">
然后按懒加载的做法,在图片节点进入视区时,进行src的替换
# 技术架构
# 时序图
sequenceDiagram
participant 浏览器
participant 代理服务器
participant 源站
浏览器 ->> 代理服务器:请求模糊图
Note right of 代理服务器:没有缓存,请求源站
代理服务器 ->> 源站:请求原始图像资源
Note right of 代理服务器:缓存图像
代理服务器 ->> 代理服务器:图像模糊化
代理服务器 -->> 浏览器:返回模糊图
Note right of 浏览器:dom解析完毕<br/>扫描文档
浏览器 ->> 代理服务器:请求图片质量标识wsq对应图像
代理服务器 ->> 代理服务器:根据wsq对原图做相应的压缩
代理服务器 -->>浏览器:返回wsq对应图片
2
3
4
5
6
7
8
9
10
11
12
13
14
# 总流程
graph TB
st(开始)-->init((初始化))
init-->obs[添加DOMContentLoaded,Load事件监听器]
obs-->DOMContentLoaded[DOMContentLoaded事件触发]
DOMContentLoaded-->cal[利用html文档下载速度来进行简单测速]
cal-->firstScreenPicSpeed[参照网速压缩比对照表得到首屏图片压缩比]
firstScreenPicSpeed-->firstScreen((扫描文档,获取图片节点))
firstScreen-->quicksort[按视距快排图片节点]
quicksort-->judgeCache[任取一模糊图的name,通过transferSize是否为0来判断本次加载是否为无缓存加载]
judgeCache-->firstScreenCal[获取视距处于首屏的图片节点]
firstScreenCal-->oripicload((进行原图加载))
oripicload-->load[Load事件触发]
load-->speedtest[采用测速算法测速,得到相应图像压缩比]
speedtest-->islazyLoad{是否懒加载}
islazyLoad--是-->lazyload[启动懒加载策略]
islazyLoad--否-->replaceOther[找到其他未进行原图加载的节点]
replaceOther-->oripicload1((进行原图加载))
lazyload-->islazycompleted{懒加载完毕?}
islazycompleted--是-->ed(结束)
islazycompleted--否-->scall{是否进行窗口滚动}
scall--是-->debounceCul[节流处理,获取满足视距条件的未进行原图加载的节点]
debounceCul-->oripicload2((进行原图加载))
oripicload2-->islazycompleted
scall--否-->mousetimeout{鼠标事件10s未触发}
mousetimeout--是-->replaceOther
mousetimeout--否-->scall
oripicload1-->ed
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
# 初始化具体流程
graph LR
init(初始化开始)-->evalElement(解析script标签中自定义属性)
evalElement-->getHost[获取可替换图片域名]
evalElement-->getHashHost[获取散列域名]
evalElement-->getLazy[获取懒加载策略]
evalElement-->getSpeedZip[获取网速压缩比参照表]
evalElement-->getTTL[获取图片缓存时间]
getHost-->initStop(初始化结束)
getHashHost-->initStop
getLazy-->initStop
getSpeedZip-->initStop
getTTL-->initStop
2
3
4
5
6
7
8
9
10
11
12
# 原图加载具体流程
graph TB
oripicstart(原图加载开始)-->judgeEle{判断节点类型}
judgeEle--picture节点-->getpic[获取其子节点中source节点和img节点中src和srcset属性中的图片url]
judgeEle--img节点-->getsrcset[获取节点的src和srcset属性中的图片url]
judgeEle--video节点-->getposter[获取节点的poster属性中的图片url]
judgeEle--其他节点-->getother[获取节点background-image的图片url]
getpic-->getfinalurl((url替换))
getsrcset-->getfinalurl1((url替换))
getposter-->getfinalurl1((url替换))
getother-->getfinalurl1((url替换))
getfinalurl1-->newImg[new Image的方式加载资源,其url为替换后的url,可能需要设置srcset属性]
newImg-->onload[Image的onload触发]
onload-->updateEle
getfinalurl-.-hashurl[将模糊图url地址通过一定算法映射到固定的散列域名,得到新的图片url-newUrl]
hashurl-->findstorage{localstorage中是否有newUrl且压缩比一样或更低的记录}
findstorage--是-->isExpired{是否过期}
findstorage--否-->carrycur
isExpired--否-->carryhigh[取未过期的压缩比最低的记录,newUrl后带上压缩比参数]
isExpired--是-->carrycur[图片url后带上当前的压缩比]
carryhigh-->replaceEnd(url替换完毕)
carrycur-->replaceEnd
getfinalurl-->updateEle[将节点属性的图片url进行替换]
updateEle-->updatelocal[更新localstorage]
updatelocal-->isnocache{无缓存加载或localstorage中没有命中缓存?}
isnocache--否-->oripicstop[原图加载完毕]
isnocache--是-->update[更新 url-压缩比-ttl 记录]
update-->oripicstop
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
# 扫描文档,获取图片节点具体流程
graph LR
init(初始化节点列表)-->scanOver{文档扫描完毕?}
scanOver--否-->scan[扫描文档]
scan-->getPicEle[获取picture节点]
getPicEle-->getchild[获取子节点的src和srcset中所有图片的url]
getchild-->ishost{节点图片url的域名存在可替换图片域名?}
ishost--否-->skip[跳过该节点]
ishost--是-->addList[节点列表加入该节点]
scan-->getImgEle[获取父节点非picture的img节点]
getImgEle-->getsrc[获取节点的src或srcset属性]
getsrc-->ishost
scan-->getstyleEle[获取style中含有background-image属性且值为图片url的节点]
getstyleEle-->ishost
scan-->getcss[扫描css文档,获取含有background-image属性且值为图片url的css规则]
getcss-->getcssDom[匹配css规则得到相应节点]
getcssDom-->ishost
scan-->getvideo[获取含poster属性且值为图片url的video节点]
getvideo-->ishost
skip-->scanOver
addList-->scanOver
scanOver--是-->scanEnd(节点获取完毕)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 问题与解决
# 1. 响应式图片处理
<img>
的 srcset 属性和css的image-set()<picture>
source、img子节点
(1) 拿到srcset值,并获取srcset中的图片地址列表
(2) 将图片地址变成带质量标识的地址
(3) 将处理过的图片地址列表替换为原来的srcset值
# 2. 带宽估算
利用 WebPerformanceApi 获取已加载资源的网络请求时间耗费
# 算法流程
- 数据结构体
{
name: v.name,//标识而已
start: v.responseStart,
end: v.responseEnd,
size: v.transferSize,
//KB/S 新算法中不用该参数
speed: v.transferSize / (v.responseEnd - v.responseStart || 1)
}
2
3
4
5
6
7
8
- 过滤、按start值升序排序
剔除小数据,保留满足以下条件数据--》小数据会导致出现过大的网速计算结果,剔除小数据后对总体结果影响小,
v.transferSize > 100 && (v.responseEnd - v.responseStart) > 10 && v.responseStart < loadTime
分组,将响应时间无连续的分组
计算每组的每条响应的速度(具体计算见如下),并计算整组均值、方差、最大值
- 3.1 将每条响应的start,end放入numArr
- 3.2 numArr去重,升序排序
- 3.3 将每条响应的时间区间按numArr值分割,当其他响应有重复的区间,该条响应的该时间区间会乘以重复的数量(包括自己)
- 3.4 将每段时间区间相加,形成该响应的实际响应时间,用size处于该时间则为该响应的实际速度
- 3.5 计算每条响应的速度
- 根据size做加权平均,得到每组带宽估算值。
- 比较每组数据的结果,取最大值
# 3. 多版本缓存命中策略
Q: 带网络参数,不会命中原图已有本地缓存
详细描述:本地已缓存高质量图片a.jpg?q=80
,本地通过网络计算需要去请求低清图片a.jpg?q=50
,这样就浪费了原来的原图缓存了。
解法:引入localStorage的缓存控制策略
不同比例图片load后,将url-TTL-q
放到 localstorage 中。
每次网络计算完后,先查询缓存中有没有更高清的且未过期的图片,有的话选更高清的进行请求,不对localstorage做处理 ;
否则请求相应网络状态的图片,onload后保存或更新localstorage。
注1:每种清晰度图片的TTL可配置,通过script节点自定义属性设置。
注意:强刷页面或者disable cache
,请求图片得到响应后,cache-control
本地缓存时间会重新计算。
相应的我们localstorage的TTL也要进行修改
这边我们主要就是判断图片是否为无缓存请求,当为无缓存请求,img.onload后需要对localstorage进行更新。
至于怎么判断是否为无缓存,就用performance.getEntriesByName('当前加载图片的url')
结果是否满足某些规则来判断
# transferSize 比 encodedBodySize 小的情况:
It is possible for transferSize value to be lower than encodedBodySize: when a cached response is successfully revalidated the transferSize reports the size of the response HTTP headers incurred during the revalidation, and encodedBodySize reports the size of the previously retrieved payload body.
缓存生效,transferSize 为响应HTTP头的大小,而encodedBodySize 为先前检索到的有效内容主体的大小。
200 from cache. 且 transferSize 一般为0
# encodedBodySize 为0 的情况:
The encodedBodySize may be zero depending on the response code - e.g. HTTP 204 (No Content), 3XX, etc.
204,3XX 。
# 4. 动态图片节点扫描
function wraptSet(obj, attr, interceptor, callback, useParent) {
var desc = Object.getOwnPropertyDescriptor(obj, attr);
var original = desc.set;
desc.set = function (value) {
try {
console.log("inject to attr:", obj, attr)
var new_value = interceptor(value);
} catch (err) {
console.warn(err);
} finally {
if (callback) {
callback(this, useParent)
} else {
//说明是元素的图片地址属性设置
this.setAttribute("wsload", "true")
}
return original.call(this, new_value);
}
}
Object.defineProperty(obj, attr, desc);
}
function wrapInvoke(obj, method, callback, useParent) {
var original = obj[method];
obj[method] = function () {
try {
console.log("inject to method:", obj, method)
if (callback) {
callback(this, useParent)
}
return original.apply(this, arguments);
} catch (err) {
console.warn(err);
}
}
}
var callback = function (node, useParent) {
setTimeout(function () {
nodeList = nodeList.concat(Util.queryImgNodeList(useParent ? node.parentNode : node))
LazyLoad.check()
}, 5);
}
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
39
40
41
进行处理的属性和方法
NodeWriter.wraptSet(window.HTMLImageElement.prototype, 'src', wrapSetSrc);
window.HTMLSourceElement && NodeWriter.wraptSet(window.HTMLSourceElement.prototype, 'src', wrapSetSrc);
// ie8 设置srcset会报错:属性不能同时具有取值函数和值
if (!((gBrowser.name === "IE" || gBrowser.name === 'MSIE') && gBrowser.version === "8")) {
NodeWriter.wraptSet(window.HTMLImageElement.prototype, 'srcset', wrapSetSrcSet);
NodeWriter.wraptSet(window.HTMLSourceElement.prototype, 'srcset', wrapSetSrcSet);
}
window.HTMLVideoElement && NodeWriter.wraptSet(window.HTMLVideoElement.prototype, 'poster', wrapSetSrc);
var EleProto = ((gBrowser.name === "IE" || gBrowser.name === 'MSIE') && window.HTMLElement) ? window.HTMLElement.prototype : window.Element.prototype
NodeWriter.wrapInvoke(EleProto, 'insertAdjacentHTML', callback, true);
NodeWriter.wraptSet(EleProto, 'innerHTML', wrapSetOrigin, callback);
NodeWriter.wraptSet(EleProto, 'outerHTML', wrapSetOrigin, callback, true);
NodeWriter.wraptSet(EleProto, 'className', wrapSetOrigin, callback);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 总结
本文介绍了一个带宽估算模型,基于该模型实现分层加载与懒加载功能,有效减少用户流量消耗,提高页面加载速度。