图片优化

前面的话

  本文将详细介绍前端项目中的图片相关的优化方案

 

图片格式

  目前在前端的开发中常用的图片格式有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')">

 

posted @ 2018-06-18 05:27 小火柴的蓝色理想 阅读(...) 评论(...) 编辑 收藏