前端最全图片优化技巧

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

1.选择图片格式

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

图片格式压缩方式透明度动画浏览器兼容适应场景JPEG有损压缩不支持不支持所有复杂颜色及形状、尤其是照片 渐进式吃cpuGIF无损压缩支持支持所有简单颜色,动画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属性,这样我们就可以实现图片加载了:

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

运行代码并观察控制台的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>
 

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

上面代码中,标签内部有两个标签和一个标签。

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

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

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

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

标签的type属性

除了响应式图像,标签还可以用来选择不同格式的图像。比如,如果当前浏览器支持 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>
 

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

浏览器按照标签出现的顺序,依次检查是否支持type属性指定的图像格式,如果支持就加载图像,并且不再检查后面的标签了。上面例子中,图像加载优先顺序依次为 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的使用了。

总结

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

转自https://www.toutiao.com/i6830037252471521804/?timestamp=1590417218&app=news_article&group_id=6830037252471521804&use_new_style=0&req_id=20200525223338010016017025054C3895

上一篇:防止.JPG类型木码的方法


下一篇:NVIDIA A100 GPUs上硬件JPEG解码器和NVIDIA nvJPEG库