谈谈Web应用中的图片优化技巧及反思

这篇文章,咱们将一块儿探讨,web应用中能对图片进行什么样的优化,以及反思一些“负优化”手段javascript

1、为何要对图片进行优化

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

2、从图片大小开始优化

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

PNG压缩

npm install imagemin
    npm install imagemin-pngquant

这里先安装imagemin库,再安装对应的png压缩库前端

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貌似是一个在压缩率和质量之间实现平衡的数值,腾讯AlloyTeam出品的gka图片处理工具,一样使用到了imagemin库,他们默认也是使用65-80的选项:
gka代码
用它压缩一张png图片,咱们看看效果如何:vue

这是压缩前的:
java

这是压缩后的:
webpack

从肉眼上几乎看不出区别,但实际上减小了百分之77的体积!读者能够本身保存图片进行比较。git

JPG/JPEG压缩与渐进式图片

压缩jpg/jpeg图片的方式与png相似,imagemin提供了两个插件:jpegtrain和mozjpeg供咱们使用。通常咱们选择mozjpeg,它拥有更丰富的压缩选项:github

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选项,这能够将图片转换为渐进式图片,关于渐进式图片,它容许在加载照片的时候,若是网速比较慢的话,先显示一个相似模糊有点小马赛克的质量比较差的照片,而后慢慢的变为清晰的照片:web

渐进式图片

而相比之下,非渐进式的图片(Baseline JPEG)则会老老实实地从头至尾去加载:

Baseline JPEG

张鑫旭大神的这篇文章,能够帮你更好地了解二者的区别:
渐进式jpeg(progressive jpeg)图片及其相关
简单来讲,渐进式图片一开始就决定了大小,而不像Baseline图片同样,不断地从上往下加载,从而形成屡次回流,但渐进式图片须要消耗CPU去屡次计算渲染,这是其主要缺点。
固然,交错式png也能够实现相应的效果,但目前pngquant没有实现转换功能,可是ps中导出png时是能够设置为交错式的。

在真实项目中如何操做?

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

npm install imagemin-webpack-plugin

先安装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

3、经过图片按需加载减小请求压力

图片按需加载是个老生常谈的话题,传统作法天然是经过监听页面滚动位置,符合条件了再去进行资源加载,咱们看看现在还有什么方法能够作到按需加载。

使用强大的IntersectionObserver

IntersectionObserver提供给咱们一项能力:能够用来监听元素是否进入了设备的可视区域以内,这意味着:咱们等待图片元素进入可视区域后,再决定是否加载它,毕竟用户没看到图片前,根本不关心它是否已经加载了。
这是Chrome51率先提出和支持的API,而在2019年的今天,各大浏览器对它的支持度已经有所改善(除了IE,全线崩~):
IntersectionObserver的兼容性
废话很少说,上代码:
首先,假设咱们有一个图片列表,它们的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,会发现图片随着可视区域的移动而加载,咱们的目的达到了。

这里给出一个线上demo,供你们调试学习
(ps: 这里额外介绍一个vue的图片懒加载组件vue-view-lazy,也是基于IntersectionObserver实现的)。

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

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

auto:让浏览器自动决定是否进行懒加载,这其中的机制尚不明确。

lazy:明确地让浏览器对此图片进行懒加载,即当用户滚动到图片附近时才进行加载,但目前没有具体说明这个“附近”具体是多近。

eager:让浏览器马上加载此图片,也不是此篇文章关注的功能。

咱们能够经过chrome的开发工具看看这个demo中的图片加载方式,咱们把上一个demo中的js脚本都删掉了,只用了loading=lazy这个属性。接着,勾选工具栏中的Disabled Cache后仔细观察Network一栏,细心的人应该会发现,一张图片被分为了两次去请求!第一次的状态码是206,第二次的状态码才是200,如图所示:

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

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

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

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

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

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

if ("loading" in HTMLImageElement.prototype) {
      // 没毛病
    } else {
      // .....
    }

还能够作到锦上添花!

以上介绍的两种方式,其实最终实现的效果是类似的,但这里还有个问题,当网速慢的时候,图片还没加载完以前,用户会看到一段空白的时间,在这段空白时间,就算是渐进式图片也没法发挥它的做用,咱们须要更友好的展现方式来弥补这段空白,有一种方法简单粗暴,那就是用一张占位图来顶替,这张占位图被加载过一次后,便可从缓存中取出,无须从新加载,但这种图片会显得有些千篇一概,并不能很好地作到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%
    )

最终效果以下所示:

4、响应式图片的实践

咱们常常会遇到这种状况:一张在普通笔记本上显示清晰的图片,到了苹果的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属性引用的图像。

5、安全地使用WebP图片

WebP的优点这里再也不赘述,简单来讲就是:一样尺寸的图片,WebP能保证比未压缩过的png、jpg、gif等格式的图片减小百分之40-70(甚至90)的比例,且保证较高的质量,更能够支持显示动态图和透明通道。

但目前WebP的兼容性并不太好:

但咱们能够经过两种方式,对暂未支持webp的浏览器进行兼容:

picture结合source标签

HTML5的picture标签,能够理解为相框,里面能够支持多种格式的图片,并保留一张默认底图:

<picture>
      <source srcset="bg.webp" type="image/webp">
      <source srcset="bg.jpg" type="image/jpeg"> 
      <img src="bg.jpg" alt="背景图">
    </picture>

有了这段代码,浏览器会自动根据是否支持webp格式来选择加载哪张图片,若不支持,则会显示bg.jpg,若是浏览器连picture都不支持,那么会fallback到默认的img图片,这是必不可少的一个选项。

并且这里要注意source的放置顺序,若是把jpg放在第一位,webp放在第二位,即便浏览器支持webp,那也会选择加载jpg图片。

借助cdn服务自动判断

目前,有些图片cdn服务能够开启自动兼容webp的模式,即支持webp的浏览器则将原图转换为webp图片并返回,不然直接返回原图。实现这个功能的原理是,根据浏览器发起的请求头中的Accept属性中是否包含webp格式来判断:

有则说明浏览器支持webp格式,这对开发者来讲多是最简单的兼容方案,可是依赖于后端服务。

接下来,谈一谈我认为应该反思的负优化手段:

6、对Base64Url的反思

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

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

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

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

或许有人会说,webpack的url-loader能够根据图片大小决定是否转为base64(通常是小于10kb的图片),但你也应该担忧若是页面中有100张小于10kb的图片时,会给css文件增长多少体积。

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

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

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

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

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

不管哪张图片,看上去都是一堆没有意义的字符串,光看代码没法知道原图是哪张,不利于某些状况下的比对。

说了这么多,有人可能不服气,既然这种方案缺点这么多,为啥它会从之前就被普遍使用呢?这要从早期的http协议特性提及,在http1.1以前,http协议还没有实现keep-alive,也就是每一次请求,都必须走三次握手四次挥手去创建链接,链接完又丢弃没法复用,而即便是到了http1.1的时代,keep-alive能够保证tcp的长链接,不须要屡次从新创建,但因为http1.1是基于文本分割的协议,因此消息是串行的,必须有序地逐个解析,因此在这种请求“昂贵”,且早期图片体积并非特别大,用户对网页的响应速度和体验要求也不是很高的各类前提结合下,减小图片资源的请求数是能够理解的。

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

总结

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

ps:欢迎关注微信公众号——前端漫游指南,会按期发布优质原创文章和译文,关注公众号福利:回复666能够得到精选前端进阶电子书,感谢~

图片描述

相关文章
相关标签/搜索