长图文加载优化
最近在开发一个长图文预览项目,主要用在手机端浏览(主要在微信端)。这项目其实就是一个手机网页,把数据中的文本和图片等元素渲染出来即可。这样的项目很常见,包括微信内公众号的文章etc.这个项目很简单,但非常头疼的一个问题是对图片的懒加载处理。(下面讨论的加载策略暂且都是针对图片)
前置条件:假定项目数据是一个数组,数组元素都是图片,并且指定了图片在屏幕中的left和top。
我们最开始想到的处理方式是:优先考虑首屏体验。
取得数据后,首屏先呈现Loading状态。通过屏幕的高度H和图片的top,得到首屏的图片,并对其每一个图片的onload和onerror事件绑定回调。当回调全都执行完成之后便将首屏Loading状态移除,呈现首屏的图片。大概的实现是:
firstScreenPromises = firstScreenImgs.map((img) => {
return new Promise((resolve, reject) => {
let image = new Image()
image.src = img.imgSrc
image.onload = image.onerror = resolve
})
})
Promise.all(firstScreenPromises).then(data => {
// 首屏开始显示
})
接下来首屏后面的图片就全部扔给浏览器去加载了。
上面首屏显示优化自然ok,不过对后面屏幕图片的显示策略自然是不太好的。于是考虑分屏加载,一屏一屏加载图片。
// 分屏
screenBox = {}
H = screen.height
for (let i = 0; i < imgs.length; i += 1) {
screenNum = Math.floor(imgs.top / H)
screenBox[screenNum] = screenBox[screenNum] || []
screenBox[screenNum].push(imgs[i])
}
indexs = Object.keys(screenBox)
----------上边代码块 (X) 继续给下一块代码使用——------------
loadNext()
// 分屏按序加载
function loadNext() {
if (!indexs.length)
return
screenBox[indexs[0]].map(img=>{
// 同首屏firstScreenPromises
}).then(data=>{
// load完后回调该函数继续load下一屏
indexs.shift()
loadNext()
})
}
上面优化点在于按序分屏加载图片,这适合用户慢慢往下看的情况,但是会有两个弊端。
一是:如果用户突然猛翻到页面较后的位置,此时如果还在加载前面某屏的图片,那用户需要等待。
二是:如果页面图片元素非常多,屏幕数很多,会消耗许多流量,也许用户不想看到最后咧。
于是考虑控制预加载的屏幕数,用户看屏幕所时处的屏幕数为N,预加载N+1,N+2....N+M,最多预加载M屏。同时监听屏幕滚动,通过滚动的高度算出用户所处的屏幕数。
....上边代码块(X).....
loadNext(0) // 初始化load首屏
window.onscroll = ()=>{
getViewScreenIndex = function() {
// 滚动监听求出用户视野所处的屏幕数(若处在a,a+1,取a)
return ...
}
loadNext(getViewScreenIndex())
}
// 加载第N屏,加载完继续加载到第N+M屏
function loadNext(N) {
hasLoad = 0
function load() {
if (hasLoad == M) {
// 已经加载到N+M屏,停止预加载
return
}
hasLoad++
if (indexs.indexOf(N) > -1) {
screenBox[indexs[0]].map(img=>{
// 同首屏firstScreenPromises
}).then(data=>{
// load完后回调该函数继续load下一屏
indexs.splice(indexs.indexOf(N), 1)
load()
})
} else {
load()
}
}
}
最后一个优化点:假设正在load第1屏(接下来会预加载第2,3屏),此时屏幕滚动到了第4屏(将会预加载第5,6屏)。此时是否还有必要继续去加载第2,3屏?我觉得是没必要的,用户更有可能会继续往后翻。所以此时我会取消掉2,3屏的预加载(当然,如果此时正在预加载第2屏,那只会取消掉第3屏的加载)。
这一块的代码,加上一些细节处理,可以到我的github lazyloader看看。
总结:目前能考虑到的上边策略为的是提升用户体验(预加载),同时不会去消耗太多流量(限制预加载的数目)。但我相信还会有更加优化的策略,希望能得到高人的指点,那就真的灰常感激啦!
同时,这里边还会遇到一些兼容上的坑。比如:此处我所用到的滚动监听是window.onscroll。这个监听事件在不同设备上的表现非常不一样,会使得这里的加载策略不一定能使所有的设备都体验不错。
android的(不确定是不是所有)window.onscroll会在手指按着屏幕拖动时触发,以及屏幕滚动停止的时候触发;而ios的则是只在屏幕停止滚动的时候才会触发。这两者,在松开手后屏幕滚动时都不会触发onscroll事件。
目前还没想到比较好的兼容策略,希望有人能提供好的资料和想法借鉴借鉴,感激涕零。