前端性能優化之圖片懶加載

本文在github作了收錄 github.com/Michael-lzg…html

demo源碼地址 github.com/Michael-lzg…vue

在類電商類項目,每每存在大量的圖片,如 banner 廣告圖,菜單導航圖,美團等商家列表頭圖等。圖片衆多以及圖片體積過大每每會影響頁面加載速度,形成不良的用戶體驗,因此進行圖片懶加載優化勢在必行。webpack

爲何要進行圖片懶加載

咱們先來看一下頁面啓動時加載的圖片信息。git

如圖所示,這個頁面啓動時加載了幾十張圖片(甚至更多),而這些圖片請求幾乎是併發的,在 Chrome 瀏覽器,最多支持的併發請求次數是有限的,其餘的請求會推入到隊列中等待或者停滯不前,直到上輪請求完成後新的請求才會發出。因此至關一部分圖片資源請求是須要排隊等待時間的。github

在上面能夠看出,有部分圖片達到幾百 kB,設置 2M(這鍋必須運營背,非得上傳高清大圖不可?),直接致使了加載時間過長。web

針對以上狀況,進行圖片懶加載有如下優勢:vue-cli

  1. 減小資源的加載,頁面啓動只加載首屏的圖片,這樣能明顯減小了服務器的壓力和流量,也可以減少瀏覽器的負擔。
  2. 防止併發加載的資源過多而阻塞 js 的加載,影響整個網站的啓動。
  3. 能提高用戶的體驗,不妨設想下,用戶打開頁面的時候,若是頁面上全部的圖片都須要加載,因爲圖片數目較大,等待時間很長這就嚴重影響用戶體驗。

圖片懶加載的原理

圖片懶加載的原理主要是判斷當前圖片是否到了可視區域這一核心邏輯實現的npm

  1. 拿到全部的圖片 dome 。
  2. 遍歷每一個圖片判斷當前圖片是否到了可視區範圍內。
  3. 若是到了就設置圖片的 src 屬性。
  4. 綁定 window 的 scroll 事件,對其進行事件監聽。

咱們先來看下頁面結構數組

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Lazyload</title>
    <style> img {
        display: block;
        margin-bottom: 50px;
        height: 200px;
        width: 400px;
      } </style>
  </head>
  <body>
    <img src="./img/default.png" data-src="./img/1.jpg" />
    <img src="./img/default.png" data-src="./img/2.jpg" />
    <img src="./img/default.png" data-src="./img/3.jpg" />
    <img src="./img/default.png" data-src="./img/4.jpg" />
    <img src="./img/default.png" data-src="./img/5.jpg" />
    <img src="./img/default.png" data-src="./img/6.jpg" />
    <img src="./img/default.png" data-src="./img/7.jpg" />
    <img src="./img/default.png" data-src="./img/8.jpg" />
    <img src="./img/default.png" data-src="./img/9.jpg" />
    <img src="./img/default.png" data-src="./img/10.jpg" />
  </body>
</html>

先獲取全部圖片的 dom,經過 document.body.clientHeight 獲取可視區高度,再使用 element.getBoundingClientRect() API 直接獲得元素相對瀏覽的 top 值, 遍歷每一個圖片判斷當前圖片是否到了可視區範圍內。代碼以下:瀏覽器

function lazyload() {
  let viewHeight = document.body.clientHeight //獲取可視區高度
  let imgs = document.querySelectorAll('img[data-src]')
  imgs.forEach((item, index) => {
    if (item.dataset.src === '') return

    // 用於得到頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置
    let rect = item.getBoundingClientRect()
    if (rect.bottom >= 0 && rect.top < viewHeight) {
      item.src = item.dataset.src
      item.removeAttribute('data-src')
    }
  })
}

最後給 window 綁定 onscroll 事件

window.addEventListener('scroll', lazyload)

主要就完成了一個圖片懶加載的操做了。可是這樣存在較大的性能問題,由於 scroll 事件會在很短的時間內觸發不少次,嚴重影響頁面性能,爲了提升網頁性能,咱們須要一個節流函數來控制函數的屢次觸發,在一段時間內(如 200ms)只執行一次回調。

下面實現一個節流函數

function throttle(fn, delay) {
  let timer
  let prevTime
  return function (...args) {
    const currTime = Date.now()
    const context = this
    if (!prevTime) prevTime = currTime
    clearTimeout(timer)

    if (currTime - prevTime > delay) {
      prevTime = currTime
      fn.apply(context, args)
      clearTimeout(timer)
      return
    }

    timer = setTimeout(function () {
      prevTime = Date.now()
      timer = null
      fn.apply(context, args)
    }, delay)
  }
}

而後修改一下 srcoll 事件

window.addEventListener('scroll', throttle(lazyload, 200))

IntersectionObserver

經過上面例子的實現,咱們要實現懶加載都須要去監聽 scroll 事件,儘管咱們能夠經過函數節流的方式來阻止高頻率的執行函數,可是咱們仍是須要去計算 scrollTopoffsetHeight 等屬性,有沒有簡單的不須要計算這些屬性的方式呢,答案就是 IntersectionObserver

IntersectionObserver 是一個新的 API,能夠自動"觀察"元素是否可見,Chrome 51+ 已經支持。因爲可見(visible)的本質是,目標元素與視口產生一個交叉區,因此這個 API 叫作"交叉觀察器"。咱們來看一下它的用法:

var io = new IntersectionObserver(callback, option)

// 開始觀察
io.observe(document.getElementById('example'))

// 中止觀察
io.unobserve(element)

// 關閉觀察器
io.disconnect()

IntersectionObserver 是瀏覽器原生提供的構造函數,接受兩個參數:callback 是可見性變化時的回調函數,option 是配置對象(該參數可選)。

目標元素的可見性變化時,就會調用觀察器的回調函數 callback。callback 通常會觸發兩次。一次是目標元素剛剛進入視口(開始可見),另外一次是徹底離開視口(開始不可見)。

var io = new IntersectionObserver((entries) => {
  console.log(entries)
})

callback 函數的參數(entries)是一個數組,每一個成員都是一個 IntersectionObserverEntry 對象。舉例來講,若是同時有兩個被觀察的對象的可見性發生變化,entries 數組就會有兩個成員。

  • time:可見性發生變化的時間,是一個高精度時間戳,單位爲毫秒
  • target:被觀察的目標元素,是一個 DOM 節點對象
  • isIntersecting: 目標是否可見
  • rootBounds:根元素的矩形區域的信息,getBoundingClientRect()方法的返回值,若是沒有根元素(即直接相對於視口滾動),則返回 null
  • boundingClientRect:目標元素的矩形區域的信息
  • intersectionRect:目標元素與視口(或根元素)的交叉區域的信息
  • intersectionRatio:目標元素的可見比例,即 intersectionRectboundingClientRect 的比例,徹底可見時爲 1,徹底不可見時小於等於 0

下面咱們用 IntersectionObserver 實現圖片懶加載

const imgs = document.querySelectorAll('img[data-src]')
const config = {
  rootMargin: '0px',
  threshold: 0,
}
let observer = new IntersectionObserver((entries, self) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      let img = entry.target
      let src = img.dataset.src
      if (src) {
        img.src = src
        img.removeAttribute('data-src')
      }
      // 解除觀察
      self.unobserve(entry.target)
    }
  })
}, config)

imgs.forEach((image) => {
  observer.observe(image)
})

懶加載指令

Vue 中除了平時經常使用的 v-showv-bindv-for 等指令外,還能夠自定義指令。Vue 指令定義函數提供了幾個鉤子函數(可選):

  • bind: 只調用一次,指令第一次綁定到元素時調用,能夠定義一個在綁定時執行一次的初始化動做。
  • inserted: 被綁定元素插入父節點時調用(父節點存在便可調用,沒必要存在於 document 中)。
  • update: 被綁定元素所在的模板更新時調用,而不論綁定值是否變化。經過比較更新先後的綁定值。
  • componentUpdated: 被綁定元素所在模板完成一次更新週期時調用。
  • unbind: 只調用一次, 指令與元素解綁時調用。

實現一個懶加載指令的思路

  1. 判斷瀏覽器是否支持 IntersectionObserver API,若是支持就使用 IntersectionObserver 實現懶加載,不然則使用 srcoll 事件監聽 + 節流的方法實現。
  2. 經過 Vue.directive 註冊一個 v-lazy 的指令,暴露一個 install() 函數,供 Vue 調用。
  3. main.js 裏 use(指令) 便可調用。
  4. 將組件內 <img> 標籤的 src 換成 v-lazy 便可實現圖片懶加載。

代碼以下

新建 LazyLoad.js 文件

const LazyLoad = {
  // install方法
  install(Vue, options) {
    const defaultSrc = options.default
    Vue.directive('lazy', {
      bind(el, binding) {
        LazyLoad.init(el, binding.value, defaultSrc)
      },
      inserted(el) {
        if (IntersectionObserver) {
          LazyLoad.observe(el)
        } else {
          LazyLoad.listenerScroll(el)
        }
      },
    })
  },
  // 初始化
  init(el, val, def) {
    el.setAttribute('data-src', val)
    el.setAttribute('src', def)
  },
  // 利用IntersectionObserver監聽el
  observe(el) {
    var io = new IntersectionObserver((entries) => {
      const realSrc = el.dataset.src
      if (entries[0].isIntersecting) {
        if (realSrc) {
          el.src = realSrc
          el.removeAttribute('data-src')
        }
      }
    })
    io.observe(el)
  },
  // 監聽scroll事件
  listenerScroll(el) {
    const handler = LazyLoad.throttle(LazyLoad.load, 300)
    LazyLoad.load(el)
    window.addEventListener('scroll', () => {
      handler(el)
    })
  },
  // 加載真實圖片
  load(el) {
    const windowHeight = document.documentElement.clientHeight
    const elTop = el.getBoundingClientRect().top
    const elBtm = el.getBoundingClientRect().bottom
    const realSrc = el.dataset.src
    if (elTop - windowHeight < 0 && elBtm > 0) {
      if (realSrc) {
        el.src = realSrc
        el.removeAttribute('data-src')
      }
    }
  },
  // 節流
  throttle(fn, delay) {
    let timer
    let prevTime
    return function (...args) {
      const currTime = Date.now()
      const context = this
      if (!prevTime) prevTime = currTime
      clearTimeout(timer)

      if (currTime - prevTime > delay) {
        prevTime = currTime
        fn.apply(context, args)
        clearTimeout(timer)
        return
      }

      timer = setTimeout(function () {
        prevTime = Date.now()
        timer = null
        fn.apply(context, args)
      }, delay)
    }
  },
}

export default LazyLoad

main.js 裏 use 指令

import LazyLoad from './LazyLoad.js'

Vue.use(LazyLoad, {
  default: 'xxx.png',
})

將組件內 <img> 標籤的 src 換成 v-lazy

<img v-lazy="xxx.jpg" />

這樣就能完成一個 vue 懶加載的指令了。

小結

  1. 爲提升網站加載性能,圖片懶加載是必要的。
  2. 圖片懶加載是實現原理是判斷當前圖片是否到了可視區域進行加載,可經過監聽 scroll 事件和 IntersectionObserver 實現相應的功能。
  3. 可經過 Vue.directive 編寫圖片懶加載指令。

推薦文章

w你必須知道的webpack插件原理分析
webpack的異步加載原理及分包策略
總結18個webpack插件,總會有你想要的!
搭建一個 vue-cli4+webpack 移動端框架(開箱即用)
從零構建到優化一個相似vue-cli的腳手架
封裝一個toast和dialog組件併發布到npm
從零開始構建一個webpack項目
總結幾個webpack打包優化的方法
總結vue知識體系之高級應用篇
總結vue知識體系之實用技巧
總結vue知識體系之基礎入門篇
總結移動端H5開發經常使用技巧(乾貨滿滿哦!)

相關文章
相關標籤/搜索