前面的話
本文將詳細介紹前端專案中的圖片相關的優化方案
圖片格式
目前在前端的開發中常用的圖片格式有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不支援半透明,也不支援動畫
【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`)">