首先要告訴大家的是,截止到上個章節,我們需要大家絞盡腦汁去理解的“硬核”操作基本告一段落了。從本節開始,我們會一起去實現一些必知必會、同時難度不大的常用優化手段。
這部分內容不難,但**很關鍵**。尤其是近期有校招或跳槽需求的同學,還請務必對這部分內容多加留心,說不定下一次的面試題里就有它們的身影。
## Lazy-Load 初相見
Lazy-Load,翻譯過來是“懶加載”。它是針對圖片加載時機的優化:在一些圖片量比較大的網站(比如電商網站首頁,或者團購網站、小游戲首頁等),如果我們嘗試在用戶打開頁面的時候,就把所有的圖片資源加載完畢,那么很可能會造成白屏、卡頓等現象,因為圖片真的太多了,一口氣處理這么多任務,瀏覽器做不到啊!
但我們再想,用戶真的需要這么多圖片嗎?不對,用戶點開頁面的瞬間,呈現給他的只有屏幕的一部分(我們稱之為首屏)。只要我們可以在頁面打開的時候把首屏的圖片資源加載出來,用戶就會認為頁面是沒問題的。至于下面的圖片,我們完全可以等用戶下拉的瞬間再即時去請求、即時呈現給他。這樣一來,性能的壓力小了,用戶的體驗卻沒有變差——這個延遲加載的過程,就是 Lazy-Load。
現在我們打開掘金首頁:

大家留意一欄文章右側可能會出現的圖片,這里咱們給個特寫:

大家現在以盡可能快的速度,瘋狂向下拉動頁面。發現什么?是不是發現我們圖示的這個圖片的位置,會出現閃動——有時候我們明明已經拉到目標位置了,文字也呈現完畢了,圖片卻慢半拍才顯示出來。這是因為,掘金首頁也采用了懶加載策略。當我們的頁面并未滾動至包含圖片的 div 元素所在的位置時,它的樣式是這樣的:

我們把代碼提出來看一下:
```
<div data-v-b2db8566=""
data-v-009ea7bb=""
data-v-6b46a625=""
data-src="https://user-gold-cdn.xitu.io/2018/9/27/16619f449ee24252?imageView2/1/w/120/h/120/q/85/format/webp/interlace/1"
class="lazy thumb thumb"
style="background-image: none; background-size: cover;">
</div>
```
我們注意到 style 內聯樣式中,背景圖片設置為了 none。也就是說這個 div 是沒有內容的,它只起到一個**占位**的作用。
這個“占位”的概念,在這個例子里或許體現得不夠直觀。最直觀的應該是淘寶首頁的 HTML Preview 效果:

我們看到,這個還沒來得及被圖片填充完全的網頁,是用大大小小的空 div 元素來占位的。掘金首頁也是如此。
一旦我們通過滾動使得這個 div 出現在了可見范圍內,那么 div 元素的內容就會發生變化,呈現如下的內容:

我們給 style 一個特寫:
```
style="background-image: url("https://user-gold-cdn.xitu.io/2018/9/27/16619f449ee24252?imageView2/1/w/120/h/120/q/85/format/webp/interlace/1"); background-size: cover;"
```
可以看出,style 內聯樣式中的背景圖片屬性從 none 變成了一個在線圖片的 URL。也就是說,出現在可視區域的瞬間,div 元素的內容被即時地修改掉了——它被寫入了有效的圖片 URL,于是圖片才得以呈現。這就是懶加載的實現思路。
## 一起寫一個 Lazy-Load 吧!
基于上面的實現思路,我們完全可以手動實現一個屬于自己的 Lazy-Load。
(**此處敲黑板劃重點,Lazy-Load 的思路及實現方式為大廠面試常考題,還望諸位同學引起重視**)
首先新建一個空項目,目錄結構如下:

大家可以往 images 文件夾里塞入各種各樣自己喜歡的圖片。
我們在 index.html 中,為這些圖片預置 img 標簽:
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Lazy-Load</title>
<style>
.img {
width: 200px;
height:200px;
background-color: gray;
}
.pic {
// 必要的img樣式
}
</style>
</head>
<body>
<div class="container">
<div class="img">
// 注意我們并沒有為它引入真實的src
<img class="pic" alt="加載中" data-src="./images/1.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/2.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/3.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/4.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/5.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/6.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/7.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/8.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/9.png">
</div>
<div class="img">
<img class="pic" alt="加載中" data-src="./images/10.png">
</div>
</div>
</body>
</html>
```
在懶加載的實現中,有兩個關鍵的數值:一個是**當前可視區域的高度**,另一個是**元素距離可視區域頂部的高度**。
**當前可視區域的高度**, 在和現代瀏覽器及 IE9 以上的瀏覽器中,可以用 window.innerHeight 屬性獲取。在低版本 IE 的標準模式中,可以用 document.documentElement.clientHeight 獲取,這里我們兼容兩種情況:
```
const viewHeight = window.innerHeight || document.documentElement.clientHeight
```
而**元素距離可視區域頂部的高度**,我們這里選用 getBoundingClientRect() 方法來獲取返回元素的大小及其相對于視口的位置。對此 MDN 給出了非常清晰的解釋:
> 該方法的返回值是一個 DOMRect 對象,這個對象是由該元素的 getClientRects() 方法返回的一組矩形的集合, 即:是與該元素相關的 CSS 邊框集合 。
> DOMRect 對象包含了一組用于描述邊框的只讀屬性——left、top、right 和 bottom,單位為像素。除了 width 和 height 外的屬性都是相對于視口的左上角位置而言的。
其中需要引起我們注意的就是 left、top、right 和 bottom,它們對應到元素上是這樣的:

可以看出,top 屬性代表了元素距離可視區域頂部的高度,正好可以為我們所用!
Lazy-Load 方法開工啦!
```
<script>
// 獲取所有的圖片標簽
const imgs = document.getElementsByTagName('img')
// 獲取可視區域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// num用于統計當前顯示到了哪一張圖片,避免每次都從第一張圖片開始檢查是否露出
let num = 0
function lazyload(){
for(let i=num; i<imgs.length; i++) {
// 用可視區域高度減去元素頂部距離可視區域頂部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top
// 如果可視區域高度大于等于元素頂部距離可視區域頂部的高度,說明元素露出
if(distance >= 0 ){
// 給元素寫入真實的src,展示圖片
imgs[i].src = imgs[i].getAttribute('data-src')
// 前i張圖片已經加載完畢,下次從第i+1張開始檢查是否露出
num = i + 1
}
}
}
// 監聽Scroll事件
window.addEventListener('scroll', lazyload, false);
</script>
```
## 小結
本節我們實現出了一個最基本的懶加載功能。但是大家要注意一點:這個 scroll 事件,是一個**危險**的事件——它太容易被觸發了。試想,用戶在訪問網頁的時候,是不是可以無限次地去觸發滾動?尤其是一個頁面死活加載不出來的時候,瘋狂調戲鼠標滾輪(或者瀏覽器滾動條)的用戶可不在少數啊!
再回頭看看我們上面寫的代碼。按照我們的邏輯,用戶的每一次滾動都將觸發我們的監聽函數。函數執行是吃性能的,頻繁地響應某個事件將造成大量不必要的頁面計算。因此,我們需要針對那些有可能被頻繁觸發的事件作進一步地優化。這里就引出了我們下一節的兩位主角——throttle 與 debounce。
- 開篇:知識體系與小冊格局
- 網絡篇 1:webpack 性能調優與 Gzip 原理
- 網絡篇 2:圖片優化——質量與性能的博弈
- 存儲篇 1:瀏覽器緩存機制介紹與緩存策略剖析
- 存儲篇 2:本地存儲——從 Cookie 到 Web Storage、IndexDB
- 彩蛋篇:CDN 的緩存與回源機制解析
- 渲染篇 1:服務端渲染的探索與實踐
- 渲染篇 2:知己知彼——解鎖瀏覽器背后的運行機制
- 渲染篇 3:對癥下藥——DOM 優化原理與基本實踐
- 渲染篇 4:千方百計——Event Loop 與異步更新策略
- 渲染篇 5:最后一擊——回流(Reflow)與重繪(Repaint)
- 應用篇 1:優化首屏體驗——Lazy-Load 初探
- 應用篇 2:事件的節流(throttle)與防抖(debounce)
- 性能監測篇:Performance、LightHouse 與性能 API
- 前方的路:希望成為你的起點