圖片優化

小火柴的藍色理想發表於2018-06-18

前面的話

  本文將詳細介紹前端專案中的圖片相關的優化方案

 

圖片格式

  目前在前端的開發中常用的圖片格式有jpg、png、gif,png8、png24、png32、svg和webp

【gif】

  gif是無損的,具有檔案小、支援動畫及透明的優點。但gif無法支援半透明,且僅支援8bit的索引色,即在整個圖片中,只能存在256種不同的顏色

  但實際上,gif是一種逐漸被拋棄的圖片格式。png格式的出現就是為了替代它

  由於gif支援動畫的這個“一招鮮”的本領,在網路中仍然佔有一席之地,主要用於一些小圖示

【jpg】

  jpg又稱為jpeg,是有損的,但採用了直接色,保證了色彩的豐富性。jpg圖片支援透明和半透明,所有空白區域填充白色

  jpg格式主要用於高清圖、攝影圖等大圖

【png8】

  png8是無損的,是png的索引色版本

  前面提到過,png是gif格式的替代者,在相同圖片效果下,png8具有更小的檔案體積,且支援透明度的調節

  但png8不支援半透明,也不支援動畫

png

【png24】

  png24是無損的,是png的直接色版本。 

  png24支援透明,也支援半透明,但png有檔案體積較大的缺點

  png24的目標是替換jpg。但一般而言,png24的檔案大小是jpg的5倍之多,但顯示效果卻只有一點點提升

【png32】

  png32是在png24的基礎上,新增了8位的alpha通道資訊,可以支援透明和半透明,且支援圖層,輔助線等複雜資料的儲存

  使用ps匯出的透明的png24圖片,實際上是閹割版的png32,因為只有32位的png圖片才支援透明,閹割版是說匯出的圖片不支援圖層

【SVG】

  svg是無損的向量圖。svg與上面這些圖片格式最大的不同是,上面的圖片格式都是點陣圖,而svg是向量圖,具有無論如何縮放都不會失真的優點

  svg格式非常適用於繪製logo、圖示等  

  但由於低版本瀏覽器支援不足,應用不廣泛

【webp】

  WebP 格式是 Google 於2010年釋出的一種支援有失真壓縮和無失真壓縮的圖片檔案格式,派生自影像編碼格式 VP8。它具有較優的影像資料壓縮演算法,能帶來更小的圖片體積,而且擁有肉眼識別無差異的影像質量,同時具備了無損和有損的壓縮模式、Alpha 透明以及動畫的特性,在 JPEG 和 PNG 上的轉化效果都非常優秀、穩定和統一。目前,知名網站 Youtube 、Facebook、Ebay 等均有使用 WebP格式。

  WebP 集合了多種圖片檔案格式的特點,JPEG 適合壓縮照片和其他細節豐富的圖片,GIF 可以顯示動態圖片,PNG 支援透明影像,圖片色彩非常豐富,而 WebP 則兼具上述優點,且較於它們還有更出色的地方。

  無失真壓縮後的 WebP 比 PNG 檔案少了 45% 的檔案大小,即使 PNG 檔案經過其他壓縮工具壓縮後,WebP 還是可以減少 28% 的檔案大小。此外,與 JPEG 相比,在質量相同的情況下,WebP 格式影像的體積要比 JPEG 格式影像小 40%,而 WebP 在壓縮方面比 JPEG 格式更優越

  但目前為止,webp只能在安卓系統下使用

 

PS儲存

  一般地,在對設計圖進行修改前,首先要保留一份psd源文字,然後再在其副本上進行修改

  通過photoshop將設計圖切成需要的素材時,涉及到圖片格式的設定問題,應注意以下幾點:

  1、當圖片色彩豐富且無透明要求時,建議儲存為jpg格式並選擇合適的品質,一般為60-80

  2、當圖片色彩不太豐富時無論有無透明要求,儲存為PNG8格式(特點是隻有256種顏色,檔案本身比較小),儲存時選擇無仿色,無雜邊

  3、當圖片有半透明要求時,儲存PNG24格式(對圖片不進行壓縮,所有相對比較大)

 

懶載入

  圖片延遲載入也稱為懶載入,延遲載入圖片或符合某些條件時才載入某些圖片,通常用於圖片比較多的網頁。可以減少請求數或者延遲請求數,優化效能

【呈現形式】

  一般而言,有以下三種呈現形式

  1、延時載入,使用setTimeout或setInterval進行載入延遲,如果使用者在載入前就離開,自然就不會進行載入

  2、條件載入,符合某些條件或者觸發了某些條件才開始非同步載入

  3、可視區域載入,僅僅載入使用者可以看到的區域,這個主要監控滾動條實現,一般距離使用者看到的底邊很近的時候開始載入,這樣能保證使用者下拉時圖片正好接上,不會有太長時間的停頓

【基本步驟】

  1、待載入的圖片預設載入一張佔點陣圖

  2、使用data-src屬性儲存真正地址

  3、當觸發某些條件時,自動改變該區域的圖片的src屬性為真實的地址

【可視區域載入】

  可視區域載入,是圖片懶載入最常用的一種形式,涉及到的細節如下所示:

  1、判斷可視區域

  圖片頂部距離頁面頂部的高度小於頁面高度

  2、儲存圖片路徑

  待載入的圖片預設載入一張佔點陣圖,使用data-src屬性儲存真正的地址

  3、判斷載入時機

  監聽頁面的scroll事件,收集當前進入頁面的圖片元素,給src賦值為真正的地址,給已載入的圖片新增標記

  4、滾動效能提升

  使用函式節流優化滾動效能

  程式碼如下所示:

const oList = document.getElementById(`list`)
const viewHeight = oList.clientHeight
const eles = document.querySelectorAll(`img[data-src]`)
const lazyLoad = () => {
  Array.prototype.forEach.call(eles, item => {
    const rect = item.getBoundingClientRect()
    if (rect.top <= viewHeight && !item.isLoaded) {
      item.isLoaded = true
      const oImg = new Image()
      oImg.onload = () => { item.src = oImg.src }
      oImg.src = item.getAttribute(`data-src`)
    }
  })
}
const throttle = (fn, wait=100) =>{
  return function() {
    if(fn.timer) return
    fn.timer = setTimeout(() => {
      fn.apply(this, arguments)
      fn.timer = null
    }, wait)
  }
}
lazyLoad()
oList.addEventListener(`scroll`, throttle(lazyLoad))

  效果如下

 

懶載入進階

  上面程式碼的問題在於,每次呼叫getBoundingClientRect()方法時,都會觸發迴流,嚴重地影響效能

  可以使用Intersection Observer這一API來解決問題,可以非同步觀察目標元素與祖先元素或頂層檔案的交集變化

  建立一個 IntersectionObserver物件並傳入相應引數和回撥用函式,該回撥函式將會在target 元素和root的交集大小超過threshold規定的大小時候被執行

var options = {
    root: document.querySelector(`#scrollArea`), 
    rootMargin: `0px`, 
    threshold: 1.0
}
var callback = function(entries, observer) { 
    /* Content excerpted, show below */ 
};
var observer = new IntersectionObserver(callback, options);

  如果root引數指定為null或者不指定的時候預設使用瀏覽器視口做為root

  rootMargin表示root元素的外邊距。該屬性值是用作root元素和target發生交集時的計算交集的區域範圍,使用該屬性可以控制root元素每一邊的收縮或者擴張。預設值為0

  threshold可以是單一的number也可以是number陣列,target元素和root元素相交程度達到該值的時候,將會被執行

  如果只是想要探測當target元素的在root元素中的可見性超過50%的時候,可以指定該屬性值為0.5。如果想要target元素在root元素的可見程度每多25%就執行一次回撥,那麼可以指定一個陣列[0, 0.25, 0.5, 0.75, 1]。預設值是0(意味著只要有一個target畫素出現在root元素中,回撥函式將會被執行)。該值為1.0含義是當target完全出現在root元素時回撥才會被執行

  為每個觀察者配置一個目標

var target = document.querySelector(`#listItem`)
observer.observe(target)

  當目標滿足該IntersectionObserver指定的threshold值,回撥被呼叫

var callback = function(entries, observer) { 
    entries.forEach(entry => {
        entry.time;             
        entry.rootBounds;       
        entry.boundingClientRect;
        entry.intersectionRect;   
        entry.intersectionRatio;  
        entry.target;           
    });
};

  time: 可見性發生變化的時間,是一個高精度時間戳,單位為毫秒

  intersectionRatio: 目標元素的可見比例,即 intersectionRect 佔 boundingClientRect 的比例,完全可見時為 1 ,完全不可見時小於等於 0

  boundingClientRect: 目標元素的矩形區域的資訊

  intersectionRect: 目標元素與視口(或根元素)的交叉區域的資訊

  rootBounds: 根元素的矩形區域的資訊,getBoundingClientRect() 方法的返回值,如果沒有根元素(即直接相對於視口滾動),則返回 null

  isIntersecting: 是否進入了視口,boolean 值

  target: 被觀察的目標元素,是一個 DOM 節點物件

  程式碼如下所示:

const eles = document.querySelectorAll(`img[data-src]`)
const observer = new IntersectionObserver( entries => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0) {
      let oImg = entry.target
      oImg.src = oImg.getAttribute(`data-src`)
      observer.unobserve(oImg)
    }
  })
}, {
  root: document.getElementById(`list`)
})
eles.forEach(item => { observer.observe(item) })

 

預載入

  預載入圖片是提升使用者體驗的一個好辦法,提前載入使用者所需的圖片,保證圖片快速、無縫釋出,使使用者在瀏覽器網站時獲得更好使用者體驗。常用於圖片畫廊等應用中

【使用場景】

  以下幾個場景中,可以使用圖片預載入

  1、在首屏載入之前,縮短白屏時間

  2、在空閒時間為SPA的下一屏預載入

  3、預測使用者操作,預先載入資料

【三種思路】

  一般來說,實現預載入有三種思路:

  1、使用頁面無用元素的背景圖片來進行圖片預載入

<button>載入圖片</button>
<img src="img/test.png" alt="測試">
<ul class="list">
    <li id="preload1"></li>
    <li id="preload2"></li>
    <li id="preload3"></li>
    <li id="preload4"></li>
</ul>
<script>
var oBtn = document.getElementsByTagName(`button`)[0];
var oImg0 = document.images[0];
var array = ["img/img1.gif","img/img2.gif","img/img3.gif","img/img4.gif"]
var iNow = -1;
oBtn.onclick = function(){
    iNow++;
    iNow = iNow%4;
    oImg0.src = array[iNow];
}
function preLoadImg(){
    preload1.style.background = "url(`img/img1.gif`)";
    preload2.style.background = "url(`img/img2.gif`)";
    preload3.style.background = "url(`img/img3.gif`)";
    preload4.style.background = "url(`img/img4.gif`)";
}
window.onload = function(){
    preLoadImg();    
}
</script>

  2、通過new Image()或document.createElement(`img`)建立img標籤,然後通過img的src屬性來載入圖片

<button>載入圖片</button>
<img src="img/test.png" alt="測試">
<script>
var oBtn = document.getElementsByTagName(`button`)[0];
var oImg0 = document.images[0];
var array = ["img/img1.gif","img/img2.gif","img/img3.gif","img/img4.gif"]
var iNow = -1;
oBtn.onclick = function(){
    iNow++;
    iNow = iNow%4;
    oImg0.src = array[iNow];
}
var aImages = [];
function preLoadImg(array){
    for(var i = 0, len = preLoadImg.arguments[0].length; i < len; i++){
        aImages[i] = new Image();
        aImages[i].src = preLoadImg.arguments[0][i];
    }
}
window.onload = function(){
    preLoadImg(array);    
}
</script>

  3、通過XHR物件傳送ajax請求來獲取圖片,但只能獲取同域圖片

【onload和onerror】

  通過新增onload和onerror這兩個事件鉤子,可以實現圖片在載入完成和載入失敗時的函式回撥。多個資源載入可以計算出大體進度,如3/10

<button>載入圖片</button>
<img src="img/test.png" alt="測試">
<script>
var oBtn = document.getElementsByTagName(`button`)[0];
var oImg0 = document.images[0];
var array = ["img/img1.gif","img/img2.gif","img/img3.gif","img/img4.gif"]
var iNow = -1;
oBtn.onclick = function(){
    iNow++;
    iNow = iNow%4;
    oImg0.src = array[iNow];
}
var iDown = 0;
var oImage = new Image();
function preLoadImg(arr){
    function loadImgTest(arr){
        iDown++;
        if(iDown < arr.length){
            preLoadImg(arr);
        }else{
            alert(`ok`);
            oImg.onload = null;
            oImg = null;            
        }
    }
    oImage.onload = function(){
        loadImgTest(arr);
    };
    oImage.onerror = function(){
        loadImgTest(arr);
    };    
    oImage.src = arr[iDown];
}
preLoadImg(array);
</script>

  將預載入寫成一個通用的資源載入器,程式碼如下

let isFunc = function(f){
  return typeof f === `function`
}
function resLoader(config){
  this.option = {
    resourceType: `image`,
    baseUrl: ``,
    resources: [],
    onStart: null,
    onProgress: null,
    onComplete: null
  }
  if(config){
    for(i in config){
      this.options[i] = config[i]
    }
  } else {
    alert(`引數錯誤`)
    return 
  }
  // 載入器狀態 0:未啟動 1:正在載入 2:載入完畢
  this.status = 0
  this.total = this.option.resources.length || 0
  this.currentIndex = 0
}
resLoader.prototype.start = function(){
  this.status = 1
  let _this = this
  let baseUrl = this.option.baseUrl
  for(var i = 0, l = this.option.resources.length; i < l; i++){
    let r = this.option.resources[i],
        url = ``
    if(r.indexOf(`http://) === 0 || r.indexOf(`https://`) === 0){
      url = r
    } else {
      url = baseUrl + r
    }
    let image = new Image()
    image.onload = function(){_this.loaded()}
    image.onerror = function(){_this.loaded()}
    image.src = url
  }
  if(isFunc(this.option.onStart)){
    this.option.onStart(this.total)
  }
}
resloader.prototype.loaded = funtion(){
  if(isFunc(this.option.onProgress)){
    this.option.onProgress(++this.currentIndex, this.total)
  }
  if(this.currentIndex === this.total){
    if(isFunc(this.option.onComplete)){
      this.option.onComplete(this.total)
    }
  }
} 
let loader = new resLoader({
  resources: [`img1.png`,`img2.png`,`img3.png`],
  onStart: function(total){
    console.log(`start:` + total)
  },
  onProgress: function(current, total){
    console.log(current+ `/` + total)
    let percent = current/total*100
  },
  onComplete: function(total){
    console.log(`載入完畢:` + total + `個資源`)
  }
})
loader.start()

 

Webp

  在安卓下可以使用webp格式的圖片,它具有更優的影像資料壓縮演算法,能帶來更小的圖片體積,同等畫面質量下,體積比jpg、png少了25%以上,而且同時具備了無損和有損的壓縮模式、Alpha 透明以及動畫的特性

【檢測】

  是否支援webp格式的圖片的檢測方法如下

const isSupportWebp = !![].map && document.createElement(`canvas`).toDataURL(`image/webp`).indexOf(`data:image/webp`) === 0

【七牛自動轉換】

  七牛支援自動將其他格式的圖片轉換成webp格式的圖片,只需新增在圖片地址之後新增?imageView2/2/format/webp

  下面是詳細程式碼

/**
 * 若該瀏覽器支援webp格式,則將返回webp圖片的url,否則返回原url
 * @param {string} `https://static.xiaohuochai.site/20180612030117.png`
 * @return {string} `https://static.xiaohuochai.site/20180612030117.png?imageView2/1/format/webp`
 */
export const getUrlWithWebp = url => {
  const isSupportWebp = !![].map && document.createElement(`canvas`).toDataURL(`image/webp`).indexOf(`data:image/webp`) === 0
  if (isSupportWebp) {
    return `${url}?imageView2/2/format/webp`
  }
  return url
}

【pageSpeed】

  Google開發的PageSpeed模組有一個功能,會自動將影像轉換成WebP格式或者是瀏覽器所支援的其它格式

  以nginx為例,它的設定很簡單

  1、在http模組開啟pagespeed屬性

pagespeed on;
pagespeed FileCachePath "/var/cache/ngx_pagespeed/";

  2、在主機配置新增如下一行程式碼,就能啟用這個特性

pagespeed EnableFilters convert_png_to_jpeg,convert_jpeg_to_webp;

 

CDN

  圖片效能的最後一步就是分發了。所有資源都可以從使用 CDN 中受益

  CDN 可以降低從圖片站點提供自適應和高效能圖片的複雜度。大多數CDN都可以根據裝置和瀏覽器進行尺寸調整、裁剪和確定最合適的格式,甚至更多 —— 壓縮、檢測畫素密度、水印、人臉識別和允許後期處理。藉助這些強大的功能和能夠將引數附到 URL 中,使得提供以使用者為中心的圖片變得輕而易舉了

  以七牛云為例,imageView2 提供簡單快捷的圖片格式轉換、縮略、剪裁功能。只需要填寫幾個引數,即可對圖片進行縮略操作,生成各種縮圖

// 裁剪正中部分,等比縮小生成200x200縮圖
http://odum9helk.qnssl.com/resource/gogopher.jpg?imageView2/1/w/200/h/200

// 寬度固定為200px,高度等比縮小,生成200x133縮圖
http://odum9helk.qnssl.com/resource/gogopher.jpg?imageView2/2/w/200

 

Vue圖片優化

  下面來介紹一個VUE下的外掛vue-lazyload,可以實現圖片或背景圖片的懶載入、使用webp圖片等效果

  首先,使用npm安裝

npm install vue-lazyload -D

【基礎使用】

  在main.js中,使用該外掛

import Vue from `vue`
import App from `./App.vue`
import VueLazyload from `vue-lazyload`

Vue.use(VueLazyload)

// or with options
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: `dist/error.png`,
  loading: `dist/loading.gif`,
  attempt: 1
})

new Vue({
  el: `body`,
  components: {
    App
  }
})

  在模板中使用v-lazy來儲存圖片的真實地址

<ul>
  <li v-for="img in list">
    <img v-lazy="img.src" >
  </li>
</ul>

  或者使用v-lazy-container配合圖片的data-src屬性

<div v-lazy-container="{ selector: `img`, error: `xxx.jpg`, loading: `xxx.jpg` }">
  <img data-src="//domain.com/img1.jpg">
  <img data-src="//domain.com/img2.jpg">
  <img data-src="//domain.com/img3.jpg">  
</div>
<div v-lazy-container="{ selector: `img` }">
  <img data-src="//domain.com/img1.jpg" data-error="xxx.jpg">
  <img data-src="//domain.com/img2.jpg" data-loading="xxx.jpg">
  <img data-src="//domain.com/img3.jpg">  
</div>

【引數說明】

  vue-lazyload相關配置的引數說明

key       描述    預設值    型別
preLoad    預載入的寬高比    1.3    Number
error      圖片載入失敗時使用的圖片源    `data-src`    String
loading    圖片載入的路徑    `data-src`    String
attempt    嘗試載入次數    3    Number
listenEvents    想讓vue監聽的事件    [`scroll`, `wheel`, `mousewheel`, `resize`, `animationend`, `transitionend`, `touchmove`]    
adapter    動態修改元素屬性    { }    
filter     影像的SRC過濾器    { }    
lazyComponent    懶載入元件    false    

  比如,可以使用如下的配置

Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: `dist/error.png`,
  loading: `dist/loading.gif`,
  attempt: 1,
  listenEvents: [ `scroll` ]
})

【動態修改圖片的URL】

Vue.use(vueLazy, {
    filter: {
      progressive (listener, options) {
          const isCDN = /qiniudn.com/
          if (isCDN.test(listener.src)) {
              listener.el.setAttribute(`lazy-progressive`, `true`)
              listener.loading = listener.src + `?imageView2/1/w/10/h/10`
          }
      },
      webp (listener, options) {
          if (!options.supportWebp) return
          const isCDN = /qiniudn.com/
          if (isCDN.test(listener.src)) {
              listener.src += `?imageView2/2/format/webp`
          }
      }
    }
})

【設定事件鉤子】

Vue.use(vueLazy, {
    adapter: {
        loaded ({ bindType, el, naturalHeight, naturalWidth, $parent, src, loading, error, Init }) {
            // do something here
            // example for call LoadedHandler
            LoadedHandler(el)
        },
        loading (listender, Init) {
            console.log(`loading`)
        },
        error (listender, Init) {
            console.log(`error`)
        }
    }
})

【使用IntersectionObserver】

Vue.use(vueLazy, {
  // set observer to true
  observer: true,

  // optional
  observerOptions: {
    rootMargin: `0px`,
    threshold: 0.1
  }
})

【懶載入元件】

Vue.use(VueLazyload, {
  lazyComponent: true
});
<lazy-component @show="handler">
  <img class="mini-cover" :src="img.src" width="100%" height="400">
</lazy-component>

<script>
  {
    ...
    methods: {
      handler (component) {
        console.log(`this component is showing`)
      }
    }

  }
</script>

【元件中為圖片或背景圖片使用懶載入】

<script>
export default {
  data () {
    return {
      imgObj: {
        src: `http://xx.com/logo.png`,
        error: `http://xx.com/error.png`,
        loading: `http://xx.com/loading-spin.svg`
      },
      imgUrl: `http://xx.com/logo.png` // String
    }
  }
}
</script>

<template>
  <div ref="container">
     <img v-lazy="imgUrl"/>
     <div v-lazy:background-image="imgUrl"></div>

     <!-- with customer error and loading -->
     <img v-lazy="imgObj"/>
     <div v-lazy:background-image="imgObj"></div>

     <!-- Customer scrollable element -->
     <img v-lazy.container ="imgUrl"/>
     <div v-lazy:background-image.container="img"></div>

    <!-- srcset -->
    <img v-lazy="`img.400px.jpg`" data-srcset="img.400px.jpg 400w, img.800px.jpg 800w, img.1200px.jpg 1200w">
    <img v-lazy="imgUrl" :data-srcset="imgUrl` + `?size=400 400w, ` + imgUrl + ` ?size=800 800w, ` + imgUrl +`/1200.jpg 1200w`" />
  </div>
</template>

【CSS狀態】

<img src="imgUrl" lazy="loading">
<img src="imgUrl" lazy="loaded">
<img src="imgUrl" lazy="error">
<style>
  img[lazy=loading] {
    /*your style here*/
  }
  img[lazy=error] {
    /*your style here*/
  }
  img[lazy=loaded] {
    /*your style here*/
  }
  /*
  or background-image
  */
  .yourclass[lazy=loading] {
    /*your style here*/
  }
  .yourclass[lazy=error] {
    /*your style here*/
  }
  .yourclass[lazy=loaded] {
    /*your style here*/
  }
</style>

  下面是前端小站中vue-lazyload外掛的使用

// main.js
import VueLazyload from `vue-lazyload`

Vue.use(VueLazyload, {
  loading: require(`./assets/imgs/loading.gif`),
  listenEvents: [`scroll`],
  filter: {
    webp(listener, options) {
      if (!options.supportWebp) return
      const isCDN = /xiaohuochai.site/
      if (isCDN.test(listener.src)) {
        listener.src += `?imageView2/2/format/webp`
      }
    }
  }
})
// homeCategory.vue
<ul v-lazy:background-image="require(`@/assets/imgs/match-bg.jpg`)">

 

相關文章