Demo视频
文章基本思路:“目的 => 方案 => 验证 => 总结启发”
一,图片懒加载实现
二,预期效果
三,A/BTest验证
四,总结
实现图片懒加载的目的是在不影响用户体验的前提下,尽可能节省用户流量甚至是电量。(可以参考Why lazy load images or video instead of just loading them?)
图片懒加载的本质是将某一图片的加载推迟到用户看到或即将看到该图片的时候。实现图片懒加载目前有三种方案:
方案 | 具体描述 | 缺点 |
---|---|---|
一. 小程序image组件的lazy-load属性 | 小程序 - image | ①不支持配置(加载时机,预加载数量) ②仅在page和scroll-view中生效(意味着很难通用) |
二,页面滚动监听 | 小程序页面监听页面滚动(onPageScroll),滚动时逐一比较图片节点的top值(可由wx.createSelectorQuery().select('yourselector’).boundingClientRect((ret)=>{}).exec() 获得)与滚动位置的关系 |
监听滑动 & 遍历所有节点会对滚动性能产生一定的影响(如果节点从上到下遍历,能做一些优化,也能达到较好的效果) |
三,IntersectionObserver | 采用IntersectionObserver API,交由浏览器内核底层判断图片是否与视窗相交,减少内核层与js层的跨进程通信,性能更好(小程序基础库1.9.3开始支持) | 不兼容低版本浏览器内核 |
补充:
①除了方案一,方案二三都同时支持小程序和H5,这里仅以小程序为例。
②如有别的方案欢迎分享补充_
基于上面的方案分析和当前用户系统占用率分布(IntersectionObserver API支持率大于80%),我决定采用IntersectionObserver API来对当前项目做渐进式增强,后续随着用户设备更新,将能实现100%的优化覆盖。
参考性能更优越的小程序图片懒加载方式,感谢分享!注意:原文章方案存在Android快速滚动到底部没有触发加载 的问题。
从前面的视频我们可以看到,优化的对象是商品详情图片列表,考虑到复用性,我把图片列表封装成lazyload-list
组件,源码如下:
/**
* lazyload-list
* imgs: 图片链接数组,
* lazyload: 是否开启lazyload功能,默认开启
*
*/
Component({
properties: {
imgs: {
type: Array,
value: [],
},
lazyload: {
type: Boolean,
value: true,
}
},
data: {
lazyImgs: [],
classNote: 'lazy-img-'
},
ready: function() {
// 接口兼容性判断,不支持新特性则回退到即时加载的方案
if (!this.createIntersectionObserver) {
// @TODO 可以改用基于滚动检测 & 高度判断的实现方案
this.setData({ lazyload: false });
} else {
var lazyImgs = [];
this.data.imgs.forEach(function(img) {
lazyImgs.push({
url: img,
loaded: false
})
});
this.setData({ lazyImgs: lazyImgs });
this.lazyloadImg();
}
},
methods: {
// 说明:原方案用同一个observer监听第一张图片加载,然后再监听下一张图片加载,
// 但在安卓系统下极快速滑动会出现第二张图片没有触发加载就跳到后面的图片的情况,导致后面的图片都加载不出来。
// 因此为了稳妥起见,给每一张图片设置单独的observer。
lazyloadImg: function () {
var that = this;
this.data.lazyImgs.forEach(function(img, i) {
var intersectionObserver = that.createIntersectionObserver();
var observeSelector = '.' + that.data.classNote + i; // css选择器
// bottom:300 指距离底部以下300px即会触发事件
intersectionObserver.relativeToViewport({bottom: 300}).observe(observeSelector, function(res) {
intersectionObserver.disconnect();
img.loaded = true;
that.setData({
['lazyImgs['+ i +']']: img
})
})
});
}
}
})
<view>
<view wx:for="{{lazyImgs}}" wx:key="{{item}}" class="{{classNote + index}}">
<image class="img" mode="widthFix" src="{{lazyload && !item.loaded ? '' : item.url}}"/>
view>
view>
.img {
width: 100%;
display: block;
}
实现过程:
具体接口请参考:小程序中的IntersectionObserver接口
补充-体验相关:图片应设置默认宽高(小程序本身有设置),图片加载前后高度变化尽可能小一些,对页面结构影响相对小,给用户观感会好很多。
图片懒加载主要节省了用户没看到的图片的加载流量,节省流量在逻辑上是可以预期的。
但不同业务,不同场景用户的浏览情况不一样,例如当用户想买这个商品时,一般会认真浏览所有的商品图片,这个时候是会加载完所有的图片的,当然用户也没有浪费流量。而当用户只是从列表页点进来看看简单的介绍时,用户往往不会看商品详情图片,或者说不会看完所有的,那这部分没有显示图片的流量就能被节省下来。
逻辑推导出来的效果没有问题,符合预期。下面我们来看看在线上产品中的效果如何:到底能为用户节省多少流量?
核心在于控制变量法。
为了知道使用图片懒加载列表组件**“到底能为用户节省多少流量”,我们需要对比优化策略上线前后平均每个用户在商品详情页花费的流量多少**。
简化到方便统计:
先统计原来一周内“平均每个用户在商品详情页加载的图片比例”,再统计图片加载优化策略上线后接下来一周内加载比例,进行对比,共耗时两周。(取完整一周作为统计周期是为了排除时间因素带来的影响)
实际统计过程如下:
A/BTest相信大家都理解,A/BTest的核心优势在于能在线上并行测试多个方案。例如上面为了获取前后两个图片加载比率,我们需要耗时两周,而如果采用A/BTest,随机将用户分成两半,一半执行原有的图片加载策略,一半执行优化后的图片加载策略,我们只需要耗费一周就能得到我们想要的结果。
两周 => 一周 是A/BTest给我们带来最直观的收益,毕竟时间先机对于互联网公司的重要性不用再多说,而A/BTest因其并行性带来测试环境更加相似的特点则能减少别的无关因素影响(例如这一周没有搞活动,下一周确搞了运营活动,统计数据可能会产生误差),A/BTest更多相关内容请参考数据分析领域的沉淀,我就不班门弄斧了。
** 真的需要A/BTest吗?**看到这里说不定有同学已经发现,上面这个优化对比,根本不需要测两个图片加载比例,因为优化前的图片加载比例就是100%(全加载),因此根本用不上A/BTest,光测优化后的图片加载比例就行。说得很对!这个优化比较简单,理论上也能直观推导出结果。但如果是要在这个图片方案上做调整,例如对比上面提到的方案二,我们就不能直观推导出结果了,此时A/BTest将能发挥重要作用!
统计过程和上面传统方法的类似,就不赘述了,区别仅在于上报的时候加上参数来区分两种图片加载方案,下面重点说说:
简单的A/BTest实现 - 随机选取用户开启图片加载优化:
Math.random() > 0.5
将用户随机分成两半;/**
* lazyload-list
* imgs: 图片链接数组,
* lazyload: 是否开启lazyload功能,默认开启
* preloadCount: 预加载图片数量
*
*/
Component({
properties: {
imgs: {
type: Array,
value: [],
},
lazyload: {
type: Boolean,
value: true,
},
preloadCount: {
type: Number,
value: 1
}
},
data: {
lazyImgs: [],
classNote: 'lazy-img-',
loadedCount: 0 // 图片加载数量统计
},
ready: function() {
// A/BTest: 随机选取用户
var key = 'lazyloadUserType';
var userType = wx.getStorageSync(key);
if (!userType) {
// 设置用户类型
var lazyloadOn = Math.random() > 0.5;
userType = lazyloadOn ? 'lazy' : 'normal';
wx.setStorageSync(key, userType);
}
// 根据用户类型启用不同加载策略
if (userType === 'lazy') {
// 接口兼容性判断,不支持新特性则回退到即时加载的方案
if (!this.createIntersectionObserver) {
// @TODO 可以改用基于滚动检测 & 高度判断的实现方案
this.setData({ lazyload: false });
} else {
var lazyImgs = [];
this.data.imgs.forEach(function(img) {
lazyImgs.push({
url: img,
loaded: false
})
});
this.setData({ lazyImgs: lazyImgs });
this.lazyloadImg();
}
} else {
this.setData({ lazyload: false });
}
},
detached: function() {
// 图片加载比例计算
var loadRate = (this.data.loadedCount/this.data.imgs.length).toFixed(4);
// console.log(loadRate);
// 数据上报
},
methods: {
// 说明:原方案用同一个observer监听第一张图片加载,然后再监听下一张图片加载,
// 但在安卓系统下极快速滑动会出现第二张图片没有触发加载就跳到后面的图片的情况,导致后面的图片都加载不出来。
// 因此为了稳妥起见,给每一张图片设置单独的observer。
lazyloadImg: function () {
var that = this;
this.data.lazyImgs.forEach(function(img, i) {
var intersectionObserver = that.createIntersectionObserver();
var observeSelector = '.' + that.data.classNote + i; // css选择器
// bottom:300 指距离底部以下300px即会触发事件
intersectionObserver.relativeToViewport({bottom: 300}).observe(observeSelector, function(res) {
intersectionObserver.disconnect();
img.loaded = true;
that.setData({
['lazyImgs['+ i +']']: img
})
})
});
},
onImgLoad: function (e) {
// 图片加载数量统计
this.data.loadedCount += 1;
}
}
})
经过一周的统计分析,使用图片懒加载列表组件后,图片加载比例:
// 2018-11-09~11-11线上数据
以上仅是特定项目特定页面的统计数据,仅供大家参考。
从结果数据可以看到图片懒加载是web流量优化的基础,尤其是在长图片列表有明显的差异。应该将列表图片懒加载优化看做雪碧图压缩这种基础优化,有助于提升web应用的整体表现。
优化虽不复杂,但“确定目标,方案分析选择,预期推导,A/BTest验证,决策”这个方法能稳妥地解决问题。
也叫TodoList,或者社区说法,挖坑。
需求繁忙,期待学有余力的朋友们共同探索。