文章結構
文章基本思路:“目的 => 方案 => 驗證 => 總結啓發”
一,圖片懶加載實現
二,預期效果
三,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功能,默認開啓
* <lazyload-list imgs="{{intros}}"></lazyload-list>
*/
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;
}
實現過程:
- 根據傳入的圖片鏈接數組渲染image標籤:
- 設置src爲空;
- 設置唯一的class(用於查找到唯一的標籤,添加監聽);
- 在DOM渲染完成後(ready事件),添加IntersectionObserver監聽:
- 接口兼容性判斷,向後兼容;
- 根據上面設置的唯一class找到DOM節點,給所有圖片DOM節點添加IntersectionObserver監聽;
- 用戶滾動(或者一進到頁面就看到了圖片)觸發添加IntersectionObserver回調,加載圖片
- 設置當前圖片鏈接,加載圖片;
- 刪除當前節點監聽;
具體接口請參考:小程序中的IntersectionObserver接口
補充-體驗相關:圖片應設置默認寬高(小程序本身有設置),圖片加載前後高度變化儘可能小一些,對頁面結構影響相對小,給用戶觀感會好很多。
二,預期效果
圖片懶加載主要節省了用戶沒看到的圖片的加載流量,節省流量在邏輯上是可以預期的。
但不同業務,不同場景用戶的瀏覽情況不一樣,例如當用戶想買這個商品時,一般會認真瀏覽所有的商品圖片,這個時候是會加載完所有的圖片的,當然用戶也沒有浪費流量。而當用戶只是從列表頁點進來看看簡單的介紹時,用戶往往不會看商品詳情圖片,或者說不會看完所有的,那這部分沒有顯示圖片的流量就能被節省下來。
邏輯推導出來的效果沒有問題,符合預期。下面我們來看看在線上產品中的效果如何:到底能爲用戶節省多少流量?
三,A/BTest驗證
對比驗證
核心在於控制變量法。
爲了知道使用圖片懶加載列表組件**“到底能爲用戶節省多少流量”,我們需要對比優化策略上線前後平均每個用戶在商品詳情頁花費的流量多少**。
簡化到方便統計:
- 平均每個用戶在商品詳情頁花費的流量
↓ - 平均每個用戶在商品詳情頁瀏覽的圖片數量(假設每張圖片的大小一致,實際上是30k~150k/張)
↓ - 平均每個用戶在商品詳情頁加載的圖片比例(抹平不同商品詳情頁圖片數量不一樣的差異)
↓ - 結論推導:如果圖片加載比例在應用了優化策略後下降很多,說明圖片懶加載優化能節省用戶大量流量,如果下降不多,證明該業務該場景下實際優化意義不大。
傳統方法
先統計原來一週內“平均每個用戶在商品詳情頁加載的圖片比例”,再統計圖片加載優化策略上線後接下來一週內加載比例,進行對比,共耗時兩週。(取完整一週作爲統計週期是爲了排除時間因素帶來的影響)
實際統計過程如下:
- 給所有image標籤綁定bindload事件,圖片加載完成後計數 + 1;
- 離開頁面前(onUnload,onHide / 或別的觸發頁面跳走的事件),計算加載比例 = 圖片加載數量 / 圖片總數;
- 上報該比例;
- 後臺統計:平均圖片加載比例 = 所有上報加載比例之和 / 上報次數
高效的A/BTest
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
將用戶隨機分成兩半; - 將該用戶類型記錄進cookie裏,避免下次進來再次被分組;
- 一般用戶開啓lazyload;
- 統計上報;
/**
* lazyload-list
* imgs: 圖片鏈接數組,
* lazyload: 是否開啓lazyload功能,默認開啓
* preloadCount: 預加載圖片數量
* <lazyload-list imgs="{{intros}}"></lazyload-list>
*/
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線上數據
100% => 58% ↓42%
以上僅是特定項目特定頁面的統計數據,僅供大家參考。
總結
從結果數據可以看到圖片懶加載是web流量優化的基礎,尤其是在長圖片列表有明顯的差異。應該將列表圖片懶加載優化看做雪碧圖壓縮這種基礎優化,有助於提升web應用的整體表現。
優化雖不復雜,但“確定目標,方案分析選擇,預期推導,A/BTest驗證,決策”這個方法能穩妥地解決問題。
啓發
也叫TodoList,或者社區說法,挖坑。
需求繁忙,期待學有餘力的朋友們共同探索。
- 採用方案二覆蓋低版本機型 - 開源社區以及有很多版本;
- 封裝純js邏輯,方便swiper等組件實現圖片懶加載;
- 學習現成的A/BTest系統:技術賦能產品做決策;