Finos

图片优化

2020-04-12  本文已影响0人  loneliness_8728

图片优化技巧

前言:对于大多数前端工程师来说,图片就是UI设计师(或者自己)切好的图,你要做的只是把图片丢进项目中,然后用以链接的方式呈现在页面上,而且我们也经常把精力放在项目的打包优化构建上,如何分包,如何抽取第三方库……..有时我们会忘了,图片才是一个网站最大头的那块加载资源(见下图),虽然图片加载可以不不阻碍页面渲染,但优化图片,绝对可以让网站的体验提升一个档次。

1.选择图片格式

如果效果真的需要图片来表现,那么选择图片格式是优化的第一步。我们经常听到的词语包括矢量图、标量图、SVG、有损压缩、无损压缩等等,我们首先说明各种图片格式的特点

图片格式 压缩方式 透明度 动画 浏览器兼容 适应场景
JPEG 有损压缩 不支持 不支持 所有 复杂颜色及形状、尤其是照片 渐进式吃cpu
GIF 无损压缩 支持 支持 所有 简单颜色,动画
PNG 无损压缩 支持 不支持 所有 需要透明时,但是体积太大
APNG 无损压缩 支持 支持 FirefoxSafariiOS Safari 需要半透明效果的动画
WebP 有损压缩 支持 支持 ChromeOperaAndroid ChromeAndroid Browser 复杂颜色及形状浏览器平台可预知
SVG 无损压缩 支持 支持 所有(IE8以上) 简单图形,需要良好的放缩体验需要动态控制图片特效

2.从图片大小开始优化

压缩图片可以使用统一的压缩工具 — imagemin,它是一款可以集成多个压缩库的工具,支持jpg,png,webp等等格式的图片压缩,比如pngquant,mozjpeg等等,作为测试用途,我们可以直接安装imagemin-pngquant来尝试png图片的压缩

  1. png压缩
  npm install imagemin
  npm install imagemin-pngquant
``

先安装imagemin库,再安装对应的png压缩库
```js
    const imagemin = require('imagemin');
    const imageminPngquant = require('imagemin-pngquant');

    (async () => {
        await imagemin(['images/*.png'], 'build/images', {
            plugins: [
                imageminPngquant({ quality: '65-80' })
            ]
        });

        console.log('Images optimized');
    })();

quailty一项决定压缩比率,65-80貌似是一个在压缩率和质量之间实现平衡的数值


  1. PG/JPEG压缩与渐进式图片
    压缩jpg/jpeg图片的方式与png类似,imagemin提供了两个插件:jpegtrain和mozjpeg供我们使用。一般我们选择mozjpeg,它拥有更丰富的压缩选项:
npm install imagemin-mozjpeg
    const imagemin = require('imagemin');
    const imageminMozjpeg = require('imagemin-mozjpeg');

    (async () => {
        await imagemin(['images/*.jpg'], 'build/images', {
            use: [
                imageminMozjpeg({ quality: 65, progressive: true })
            ]
        });

        console.log('Images optimized');
    })();
    

注意到我们使用了progressive:true选项,这可以将图片转换为渐进式图片,关于渐进式图片,它允许在加载照片的时候,如果网速比较慢的话,先显示一个类似模糊有点小马赛克的质量比较差的照片,然后慢慢的变为清晰的照片:

渐进式图片 Progressive JPEG

Progressive JPEG文件包含多次扫描,这些扫描顺寻的存储在JPEG文件中。打开文件过程中,会先显示整个图片的模糊轮廓,随着扫描次数的增加,图片变得越来越清晰。这种格式的主要优点是在网络较慢的情况下,可以看到图片的轮廓知道正在加载的图片大概是什么。在一些网站打开较大图片时,你就会注意到这种技术。

非渐进式的图片(Baseline JPEG)

这种类型的JPEG文件存储方式是按从上到下的扫描方式,把每一行顺序的保存在JPEG文件中。打开这个文件显示它的内容时,数据将按照存储时的顺序从上到下一行一行的被显示出来,直到所有的数据都被读完,就完成了整张图片的显示。如果文件较大或者网络下载速度较慢,那么就会看到图片被一行行加载的效果,这种格式的JPEG没有什么优点,因此,一般都推荐使用Progressive JPEG。

基本JPEG和渐进JPEG该什么时候使用?

当您的JPEG图像低于10K时,最好保存为基本JPEG(估计有75%的可能性会更小)
对于超过10K的文件,渐进式JPEG将为您提供更好的压缩(在94%的情况下)
Chrome + Firefox + IE9浏览器下,渐进式图片加载更快,而且是快很多,至于其他浏览器,与基本式图片的加载一致,至少不会拖后腿。

渐进式图片也有不足,就是吃CPU吃内存。

总结一下两者的区别:

渐进式jpeg(progressive jpeg)图片及其相关
简单来说,渐进式图片一开始就决定了大小,而不像Baseline图片一样,不断地从上往下加载,从而造成多次回流,但渐进式图片需要消耗CPU去多次计算渲染,这是其主要缺点。 当然,交错式png也可以实现相应的效果,但目前pngquant没有实现转换功能,但是ps中导出png时是可以设置为交错式的。

那我们怎么查看图片是渐进式还是基本的呢

通过对比保存的图片格式(格式在线分析:https://exif.tuchong.com/

也有一些网上推荐的转化工具

https://www.imgonline.com.ua/eng/compress-image.php

http://www.imagemagick.org/script/download.php


说了这么多,是不是感觉很啰嗦,接下来我们在实际项目中如何操作

实际项目中,总不能UI丢一个图过来你就跑一遍压缩代码吧?幸好imagemin有对应的webpack插件,在webpack遍地使用的今天,我们可以轻松实现批量压缩:

先安装imagemin-webpack-plugin

npm install imagemin-webpack-plugin
    import ImageminPlugin from 'imagemin-webpack-plugin'
    import imageminMozjpeg from 'imagemin-mozjpeg'

    module.exports = {
      plugins: [
        new ImageminPlugin({
          plugins: [
            imageminMozjpeg({
              quality: 100,
              progressive: true
            })
          ]
        })
      ]
    }

接着在webpack配置文件中,引入自己需要的插件,使用方法完全相同。具体可参考github的文档imagemin-webpack-plugin

同时我们推荐几种比较好用的图片压缩工具

1. docsmall在线图片压缩

https://docsmall.com/

国内公司开发在线图片压缩工具

服务器在国内,上传速度很快

页面简洁无广告,美观大方

压缩率很好,基本能压缩到原来的一半以下

压缩出的图片画质很清晰,跟原图几乎没有差别

对png、jpg格式的支持都很好

还有针对PDF的压缩功能

2. tinypng

https://tinypng.com/

国外团队开发的在线图片压缩网站,有口皆碑

唯一的问题就是上传速度不够快,毕竟是国外的

界面全英文,对英语不好的朋友来说不够友好

3. 智图

https://zhitu.isux.us/

腾讯的一个团队出品

可以自定义压缩比例,如果压出来的体积不够小,你还可以选择一个更高的压缩率

保证图片体积够小


3.通过图片按需加载减少请求压力

图片按需加载是个老生常谈的话题,传统做法自然是通过监听页面滚动位置,符合条件了再去进行资源加载,我们看看如今还有什么方法可以做到按需加载。

使用强大的IntersectionObserver
IntersectionObserver提供给我们一项能力:可以用来监听元素是否进入了设备的可视区域之内,这意味着:我们等待图片元素进入可视区域后,再决定是否加载它,毕竟用户没看到图片前,根本不关心它是否已经加载了。 这是Chrome51率先提出和支持的API,而现在,各大浏览器对它的支持度已经有所改善(除了IE,全线崩~) 废话不多说,上代码: 首先,假设我们有一个图片列表,它们的src属性我们暂不设置,而用data-src来替代:

    <li>
      <img class="list-item-img" alt="loading" data-src='a.jpg'/>
    </li>
    <li>
      <img class="list-item-img" alt="loading" data-src='b.jpg'/>
    </li>
    <li>
      <img class="list-item-img" alt="loading" data-src='c.jpg'/>
    </li>
    <li>
      <img class="list-item-img" alt="loading" data-src='d.jpg'/>
    </li>

这样会导致图片无法加载,这当然不是我们的目的,我们想做的是,当IntersectionObserver监听到图片元素进入可视区域时,将data-src”还给”src属性,这样我们就可以实现图片加载了:

<script>
export default {
  data () {
    return {
    }
  },
  mounted () {
    this.inter()
  },
  methods: {
    inter () {
      let _this = this
      this.observer = new IntersectionObserver(function (changes) {
        changes.forEach(function (element, index) {
        // 当这个值大于0,说明满足我们的加载条件了,这个值可通过rootMargin手动设置
          if (element.intersectionRatio > 0) {
            // 放弃监听,防止性能浪费,并加载图片。
            _this.observer.unobserve(element.target)
            element.target.src = element.target.dataset.src
          }
        })
      })
    },
    initObserver () {
      console.log(this.observer)
      const listItems = document.querySelectorAll('.list-item-img')
      listItems.forEach((item) => {
      // 对每个list元素进行监听
        this.observer.observe(item)
      })
    }
  }
}
</script>

运行代码并观察控制台的Network,会发现图片随着可视区域的移动而加载,我们的目的达到了。


IntersectionObserver

浏览器兼容

还是Chrome的黑科技——loading属性

从新版本Chrome(76)开始,已经默认支持一种新的html属性——loading,它包含三种取值:auto、lazy和eager(ps: 之前有文章说是lazyload属性,后来chrome的工程师已经将其确定为loading属性,原因是lazyload语义不够明确),我们看看这三种属性有什么不同:

  1. auto:让浏览器自动决定是否进行懒加载,这其中的机制尚不明确。
  2. lazy:明确地让浏览器对此图片进行懒加载,即当用户滚动到图片附近时才进行加载,但目前没有具体说明这个“附近”具体是多近。
  3. eager:让浏览器立刻加载此图片

这个现象跟chrome的lazy-loading功能的实现机制有关:

首先,浏览器会发送一个预请求,请求地址就是这张图片的url,但是这个请求只拉取这张图片的头部数据,大约2kb,具体做法是在请求头中设置range: bytes=0-2047,

而从这段数据中,浏览器就可以解析出图片的宽高等基本维度,接着浏览器立马为它生成一个空白的占位,以免图片加载过程中页面不断跳动,这很合理,总不能为了一个懒加载,让用户牺牲其他方面的体验吧?这个请求返回的状态码是206,表明:客户端通过发送范围请求头Range抓取到了资源的部分数据,详细的状态码解释可以看看这篇文章

然后,在用户滚动到图片附近时,再发起一个请求,完整地拉取图片的数据下来,这个才是我们熟悉的状态码200请求。

可以预测到,如果以后这个属性被普遍使用,那一个服务器要处理的图片请求连接数可能会变成两倍,对服务器的压力会有所增大,但时代在进步,我们可以依靠http2多路复用的特性来缓解这个压力,这时候就需要技术负责人权衡利弊了

要注意,使用这项特性进行图片懒加载时,记得先进行兼容性处理,对不支持这项属性的浏览器,转而使用JavaScript来实现,比如上面说到的IntersectionObserver:

    if ("loading" in HTMLImageElement.prototype) {
      // 支持loading
    } else {
      // .....
    }

4.通过占位图解决网速较慢视觉空白问题

当网速慢的时候,图片还没加载完之前,用户会看到一段空白的时间,在这段空白时间,就算是渐进式图片也无法发挥它的作用,我们需要更友好的展示方式来弥补这段空白,有一种方法简单粗暴,那就是用一张占位图来顶替,这张占位图被加载过一次后,即可从缓存中取出,无须重新加载,但这种图片会显得有些千篇一律,并不能很好地做到preview的效果。

这里介绍另一种占位图做法——css渐变色背景,原理很简单,当img标签的图片还没加载出来,我们可以为其设置背景色,比如:

<img src="a.jpg" style="background: red;"/>

这样会先显示出红色背景,再渲染出真实的图片,重点来了,我们此时要借用工具为这张图片"配制"出合适的渐变背景色,以达到部分preview的效果,我们可以使用
https://calendar.perfplanet.com/2018/gradient-image-placeholders/ 这篇文章中推荐的工具GIP进行转换
,这里附上在线转换的地址
https://tools.w3clubs.com/gip/

经过转换后,我们得到了下面这串代码:

background: linear-gradient(
      to bottom,
      #1896f5 0%,
      #2e6d14 100%
    )

5.响应式图片的实践

我们经常会遇到这种情况:一张在普通笔记本上显示清晰的图片,到了苹果的Retina屏幕或是其他高清晰度的屏幕上,就变得模糊了。

这是因为,在同样尺寸的屏幕上,高清屏可以展示的物理像素点比普通屏多,比如Retina屏,同样的屏幕尺寸下,它的物理像素点的个数是普通屏的4倍(2 * 2),所以普通屏上显示清晰的图片,在高清屏上就像是被放大了,自然就变得模糊了,要从图片资源上解决这个问题,就需要在设备像素密度为2的高清屏中,对应地展示一张两倍大小的图。

而通常来讲,对于背景图片,我们可以使用css的@media进行媒体查询,以决定不同像素密度下该用哪张倍图,例如:

.bg {
    background-image: url("bg.png");
    width: 100px;
    height: 100px;
    background-size: 100% 100%;
}
@media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2)
{
    .bg {
        background-image: url("bg@2x.png") // 尺寸为200 * 200的图
    }
}

这么做有两个好处,一是保证高像素密度的设备下,图片仍能保持应有的清晰度,二是防止在低像素密度的设备下加载大尺寸图片造成浪费。

那么如何处理img标签呢?

我们可以使用HTML5中img标签的srcset来达到这个效果,看看下面这段代码:

<img width="320"  src="bg@2x.png" srcset="bg.png 1x;bg@2x.png 2x"/>

这段代码的作用是:当设备像素密度,也就是dpr(devicePixelRatio)为1时,使用bg.png,为2时使用二倍图bg@2x.png,依此类推,你可以根据需要设置多种精度下要加载的图片,如果没有命中,浏览器会选择最邻近的一个精度对应的图片进行加载。
要注意:老旧的浏览器不支持srcset的特性,它会继续正常加载src属性引用的图像。

要同时适配不同像素密度、不同大小的屏幕,应该怎么办呢?

<picture>
  <source media="(max-width: 500px)" srcset="cat-vertical.jpg">
  <source media="(min-width: 501px)" srcset="cat-horizontal.jpg">
  <img src="cat.jpg" alt="cat">
</picture>

就要用到<picture>标签。它是一个容器标签,内部使用<source>和<img>,指定不同情况下加载的图像。

上面代码中,<picture>标签内部有两个<source>标签和一个<img>标签。

<source>标签的media属性给出媒体查询表达式,srcset属性就是<img>标签的srcset属性,给出加载的图像文件。sizes属性其实这里也可以用,但由于有了media属性,就没有必要了。

浏览器按照<source>标签出现的顺序,依次判断当前设备是否满足media属性的媒体查询表达式,如果满足就加载srcset属性指定的图片文件,并且不再执行后面的<source>标签和<img>标签。

<img>标签是默认情况下加载的图像,用来满足上面所有<source>都不匹配的情况。

上面例子中,设备宽度如果不超过500px,就加载竖屏的图像,否则加载横屏的图像。

<source>标签的type属性

除了响应式图像,<picture>标签还可以用来选择不同格式的图像。比如,如果当前浏览器支持 Webp 格式,就加载这种格式的图像,否则加载 PNG 图像。

<picture>
  <source type="image/svg+xml" srcset="logo.xml">
  <source type="image/webp" srcset="logo.webp"> 
  <img src="logo.png" alt="ACME Corp">
</picture>

上面代码中,<source>标签的type属性给出图像的 MIME 类型,srcset是对应的图像 URL。

浏览器按照<source>标签出现的顺序,依次检查是否支持type属性指定的图像格式,如果支持就加载图像,并且不再检查后面的<source>标签了。上面例子中,图像加载优先顺序依次为 svg 格式、webp 格式和 png 格式。

6.自动优化:CDN

使用CDN对图片自动进行优化,我在国外的CDN提供商处很少见到这类服务,倒是国内的两大新秀CDN七牛和又拍在这方面都做了大量工作。其工作方式为,向CDN请求图片的URL参数中包含了图片处理的参数(格式、宽高等),CDN服务器根据请求生成所需的图片,发送到用户浏览器。

七牛云存储的图片处理接口极其丰富,覆盖了图片的大部分基本操作,例如:

图片裁剪,支持多种裁剪方式(如按长边、短边、填充、拉伸等)
图片格式转换,支持JPG, GIF, PNG, WebP等,支持不同的图片压缩率
图片处理,支持图片水印、高斯模糊、重心处理等

当然其他cdn对于图像处理也有很丰富的处理,相关文档里也介绍很详细,可以参考cdn文档

阿里云

腾讯

我们通过如下URL请求,裁剪正中部分,等比缩小生成200x200缩略图:

http://qiniuphotos.qiniudn.com/gogopher.jpg?imageView2/1/w/200/h/200

七牛cdn

7.对Base64Url的反思

首先复习一下Base64的概念,Base64就是一种基于64个可打印字符来表示二进制数据的方法,编码过程是从二进制数据到字符串的过程,在web应用中我们经常用它来做啥呢——传输图片数据。HTML中,img的src和css样式的background-image都可以接受base64字符串,从而在页面上渲染出对应的图片。正是基于浏览器的这项能力,很多开发者提出了将多张图片转换为base64字符串,放进css样式文件中的“优化方式”,这样做的目的只有一个——减少HTTP请求数。但实际上,在如今的应用开发中,这种做法大多数情况是“负优化”效果,接下来让我们细数base64 Url的“罪状”:

第一、让css文件的体积失去控制

当你把图片转换为base64字符串之后,字符串的体积一般会比原图更大,一般会多出接近3成的大小,如果你一个页面中有20张平均大小为50kb的图片,转它们为base64后,你的css文件将可能增大1.2mb的大小,这样将严重阻碍浏览器的关键渲染路径:

css文件本身就是渲染阻塞资源,浏览器首次加载时如果没有全部下载和解析完css内容就无法进行渲染树的构建,而base64的嵌入则是雪上加霜,这将把原先浏览器可以进行优化的图片异步加载,变成首屏渲染的阻塞和延迟。

或许有人会说,webpack的url-loader可以根据图片大小决定是否转为base64(一般是小于10kb的图片),但你也应该担心如果页面中有100张小于10kb的图片时,会给css文件增加多少体积。

第二、让浏览器的资源缓存策略功亏一篑

假设你的base64Url会被你的应用多次复用,本来浏览器可以直接从本地缓存取出的图片,换成base64Url,将造成应用中多个页面重复下载1.3倍大小的文本,假设一张图片是100kb大小,被你的应用使用了10次,那么造成的流量浪费将是:(100 1.3 10) - 100 = 1200kb。

第三、低版本浏览器的兼容问题

这是比较次要的问题,dataurl在低版本IE浏览器,比如IE8及以下的浏览器,会有兼容性问题,详细情况可以参考这篇文章。

第四、不利于开发者工具调试与查看

无论哪张图片,看上去都是一堆没有意义的字符串,光看代码无法知道原图是哪张,不利于某些情况下的比对。
说了这么多 既然这种方案缺点这么多,为啥它会从以前就被广泛使用呢?这要从早期的http协议特性说起,在http1.1之前,http协议尚未实现keep-alive,也就是每一次请求,都必须走三次握手四次挥手去建立连接,连接完又丢弃无法复用,而即使是到了http1.1的时代,keep-alive可以保证tcp的长连接,不需要多次重新建立,但由于http1.1是基于文本分割的协议,所以消息是串行的,必须有序地逐个解析,所以在这种请求“昂贵”,且早期图片体积并不是特别大,用户对网页的响应速度和体验要求也不是很高的各种前提结合下,减少图片资源的请求数是可以理解的。

但是,在越来越多网站支持http2.0的前提下,这些都不是问题,h2是基于二进制帧的协议,在保留http1.1长连接的前提下,实现了消息的并行处理,请求和响应可以交错甚至可以复用,多个并行请求的开销已经大大降低,我已经不知道还有什么理由继续坚持base64Url的使用了。

总结

图片优化的手段总是随着浏览器特性的升级,网络传输协议的升级,以及用户对体验要求的提升而不停地更新迭代,几年前适用的或显著的优化手段,几年后不一定仍然如此。因地制宜,多管齐下,才能将其优化做到极致!

上一篇下一篇

猜你喜欢

热点阅读