前端性能优化学习 07 图片优化

图片优化

前端大部分的工作都围绕在 JavaScript 和 CSS 上,考虑如何更快地下载文件,如何提供给用户复杂而优雅的交互,如何高效合理地应用有限的处理和传输资源等,这些是用户感知的全部吗?

当然,他们在前端开发和性能优化中的地位举足轻重,但 JavaScript 和 CSS 对用户感知而言,并不是最重要的部分,图像才是。我们在公众号发布文章或用 PPT 进行演讲时,都知道一条高效传递信息的原则:字不如表,表不如图。

网站作为一种信息传递的媒介,且如今各类 Web 项目中,图像资源的使用占比也越来越大,更应当注重图像资源的使用方式。如果网站中的图像资源未进行恰当的优化,那么势必会导致许多问题,诸如巨量的访问请求引发传输带宽的挑战,请求大尺寸图片需要过久的等待时间等。

图像优化问题主要可以分为两方面:图像的选取和使用,图像的加载和显示。对于加载方面的策略将放在后面的章节《资源加载优化》中深入讨论,本章将聚焦图像的选取和使用。

本章内容包括:什么是图像文件,都有哪些格式的图像文件,不同格式的图像文件适用于怎样的业务场景,以及通过怎样的优化方法能够有效提升用户对图像的体验感知等。

图片基础

HTTP Archive 上的数据显示,网站传输的数据中,60%的资源都是由各种图像文件组成的,当然这个数据是将各种类型网站平均之后的结果,要是单独看电商类面向消费者端页面的数据,这个比例可能会更大。如此之大的资源占比,也同样意味着存在很大的优化空间。

图像是否必须

图像资源优化的根本思想,可以归结为两个字:压缩。无论是选取何种图像的文件格式,还是针对同一种格式压缩至更小的尺寸,其本质都是用更小的资源开销来完成图像的传输和展示。

在深入探讨之前,我们首先思考一下要达到期望的信息传递效果,是否真的需要图像?这不仅是因为图像资源与网页上的其他资源(HTML/CSS/JavaScript等)相比有更大的字节开销,出于对节省资源的考虑,对用户注意力的珍惜也很重要,如果一个页面打开后有很多图像,那么用户其实很难快速梳理出有效的信息,即便获取到了也会让用户觉得很累。

一个低感官体验的网站,它的价值转化率不会很高。当然这个问题的答案不是通过自己简单想想就能得到的,我们可能需要在日常的开发中与产品经理及体验设计师不断沟通,不断思考,来趋近更优的方案。

当确定了图像的展示效果必须存在时,在前端实现上也并非一定就要用图像文件,还存在一些场景可以使用更高效的方式来实现所需的效果。

下面列举了两个例子,为了说明当我们在选择使用某种资源之前,如果期望达到更优的性能效果,则需要先去思考这种选择是否必需:

  • 网站中一个图像在不同的页面或不同的交互状态下,需要呈现出不同的效果(边角的裁切、阴影或渐变),其实没有必要为不同场景准备不同效果的多份图像文件,只需用 CSS 将一张图像处理为所需的不同效果即可。相对于一个图像文件的大小来讲,修改其所增加的 CSS 代码量可以忽略不计。
  • 如果一个图像上面需要显示文字,建议使用网页字体的形式通过前端代码进行添加,而不是使用带文字的图像,其原因一方面是包含了更多信息的图像文件一般会更大,另一方面是图像中的文本信息带来的用户体验一般较差(不可选择、搜索及缩放),并且在高分辨率设备上的显示效果也会打折扣。

矢量图和位图

当确定了图像是实现展示效果的最佳方式时,接下来就是选择合适的图像格式。图像文件可以分为两类:矢量图和位图。每种类型都有其各自的优缺点和适用场景。

矢量图

矢量图中的图形元素被定义为一个对象,包括颜色、大小、形状及屏幕位置等属性。它适合如文本、品牌 logo、控件图标及二维码等构图形状较简单的几何图形。矢量图的优点是能够在任何缩放比例下呈现出细节同样清晰的展示效果。其缺点是对细节的展示效果不够丰富,对足够复杂的图像来说,比如要达到照片的效果,若通过 SVG 进行矢量图绘制,则所得文件会大得离谱,但即便如此也很难达到照片的真实效果。

SVG 也是一种基于 XML 的图像格式,其全称是 Scalable Vector Graphics(可缩放的矢量图形),目前几乎所有浏览器都支持 SVG。我们可以在 Iconfont 上找到许多矢量图,或者上传自己绘制的矢量图,在上面构建自己的矢量图标库并引入项目进行使用。

标识照片的矢量图标的 SVG 标签格式,如下所示:

<svg t="1620298657941" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2974" width="200" height="200"><path d="M874.666667 117.333333H149.333333C108.8 117.333333 74.666667 151.466667 74.666667 192v640c0 40.533333 34.133333 74.666667 74.666666 74.666667h725.333334c40.533333 0 74.666667-34.133333 74.666666-74.666667V192c0-40.533333-34.133333-74.666667-74.666666-74.666667z m-245.333334 64v661.333334h-234.666666v-661.333334h234.666666zM138.666667 832V192c0-6.4 4.266667-10.666667 10.666666-10.666667h181.333334v661.333334H149.333333c-6.4 0-10.666667-4.266667-10.666666-10.666667z m746.666666 0c0 6.4-4.266667 10.666667-10.666666 10.666667h-181.333334v-661.333334H874.666667c6.4 0 10.666667 4.266667 10.666666 10.666667v640z" p-id="2975">path>svg>

SVG 标签所包括的部分就是该矢量图的全部内容,除了必要的绘制信息,可能还包括一些元数据,比如 XML 命名空间、图层及注释信息。但这些信息对浏览器绘制一个 SVG 来说并不是必要的,所以在使用前可通过工具去除这些元数据来达到压缩的目的。

位图

位图是通过对一个矩阵中的栅格进行编码来表示图像的,每个栅格只能编码表示一个特定的颜色,如果组成图像的栅格像素点越多且每个像素点所能表示的颜色范围越广,则位图图像整体的显示效果就会越逼真。虽然位图没有像矢量图那种不受分辨率影响的优秀特性,但对于复杂的照片却能提供较为真实的细节体验,如图中一幅海边的位图对于云朵及波浪的细节表现,如果用矢量图来实现是不可想象的。

前端性能优化学习 07 图片优化_第1张图片

当把图像不断放大后,就会看到许多栅格像素色块,如图所示。每个像素存储的是图像局部的 RGBA 信息,即红绿蓝三色通道及透明度。通常浏览器会为每个颜色通道分配一个字节的存储空间,即 2^8=256 个色阶值。

前端性能优化学习 07 图片优化_第2张图片

一个像素点4个通道就是4字节,一张图像整体的大小与其包含的像素数成正比,图像包含的像素越多,所能展示的细节就越丰富,同时图像就越大。

如表所示,当图像尺寸为100像素×100像素时,文件大小为39KB。随着图像尺寸在长和宽两个维度上同时增大,所产生像素数量的增加就不是简单的线性关系了,而是平方的抛物线增加,也就是说文件大小会迅速增加,在网络带宽一定的前提下,下载完一张图像会更慢。

图像尺寸 像素数量 文件大小
100像素 * 100像素 10000 39KB
200像素 * 200像素 40000 156KB
500像素 * 500像素 250000 977KB
800像素 * 800像素 640000 2.5MB

出于对性能的考虑,在使用图像时必须考虑对图像进行压缩,采用什么样的图像格式,使用什么样的压缩算法及压缩到何种程度,这将是本章接下来详细讨论的内容,但在此之前先说明关于分辨率的两个容易混淆的概念。

分辨率

在前端开发过程中书写 CSS 时,经常会为图像设置显示所需的长宽像素值,但在不同的设备屏幕上,有时候相同的图像及相同的设置,其渲染出来的图像会让人明显察觉出清晰度有差别。产生这个现象的原因涉及两种不同的分辨率:设备(或屏幕)分辨率和图像分辨率

图像分辨率表示的就是该图像文件所包含的真实像素值信息,比如一个 200像素×200像素 的分辨率的图像文件,它就定义了长宽各200个像素点的信息。

设备分辨率则是显示器屏幕所能显示的最大像素值,比如一台13英寸的 MacBook Pro 笔记本电脑的显示器分辨率为 2560像素×1600像素。这两种分辨率都用到了像素,那么它们有什么区别呢?

例如,10像素×10像素的图像分辨率,既可以使用10像素×10像素的设备分辨率来显示,又可以使用 20像素×20像素 或 30像素×30像素 的设备分辨率来显示,效果如图所示。

前端性能优化学习 07 图片优化_第3张图片

从图中可以看到更高的设备分辨率有助于显示更绚丽多彩的图像,这其实很适合矢量图的发挥,因为它不会因放大而失真。而对位图来说,只有图像文件包含更多的像素信息时,才能更充分地利用设备分辨率。为了能在不同的分辨率下使项目中所包含的图像都能得到恰当的展示效果,可以利用 picture 标签和 srcset 属性提供图像的多个变体。

用于插入图像的 img 标签,有一个 srcset 属性可以用来针对不同设备,提供不同分辨率的图像文件:

<img src="photo.jpg" srcset="[email protected] 2x,[email protected] 3x,[email protected] 4x" alt="photo">

除了 IE 和其他较低版本的浏览器不支持,目前主流的大部分浏览器都已支持 img 标签的 srcset 属性。在 srcset 属性中设置多种分辨率的图像文件及使用条件,浏览器在请求之前便会先对此进行解析,只选择最合适的图像文件进行下载,如果浏览器不支持,请务必在 src 属性中包含必要的默认图片。

使用 picture 标签则会在多图像文件选择时,获得更多的控制维度,比如屏幕方向、设备大小、屏幕分辨率等。

<picture>
  <source media="(min-width: 800px)" srcset="photo.jpg, photo-2x.jpg 2x">
  <source media="(min-width: 450px)" srcset="photo-s.jpg, photo-s-2x.jpg 2x">
  <img src="photo.jpg" alt="photo">
picture>

由于 picture 标签也是加入标准不久的元素标签,所以在使用过程中,同样应当考虑兼容性问题。

有损压缩和无损压缩

压缩是降低源文件大小的有效方式,对 JavaScript 代码或网页的一些脚本文件而言,压缩掉的内容是一些多余的空格及不影响执行的注释,其目的是在不损坏正常执行的情况下,尽量缩小源文件的大小。对图像文件而言,由于人眼对不同颜色的敏感度存在差异,所以便可通过减少对某种颜色的编码位数来减小文件大小,甚至还可以损失部分源文件信息,以达到近似的效果,使得压缩后的文件尺寸更小。

对于图像压缩,应该采用有损压缩还是无损压缩?如果都采用又该如何搭配设置呢?当结合了具体的业务需求再考虑后,关于压缩的技术选型就可以简单分成两步进行。

(1)首先确定业务所要展示图像的颜色阶数、图像显示的分辨率及清晰程度,当确定了这几个参数的基准后,如果获取的图像源文件的相应参数指标过高,便可适当进行有损压缩,通过降低源文件图像质量的方法来降低图像文件大小。

如果业务所要求的图像质量较高,便可跳过有损压缩,直接进入第二步无损压缩。所以是否要进行有损压缩,其实是在理解了业务需求后的一个可选选项,而非必要的。

(2)当确定了展示图像的质量后,便可利用无损压缩技术尽可能降低图像大小。和第(1)步要通过业务决策来判断是否需要所不同的是,无损压缩是应当完成的工作环节。那么最好能通过一套完善的工程方案,自动化执行来避免烦琐的人工重复工作。

图像格式

实际上,不同的图像文件格式(JPG、PNG、GIF等)之间的区别,在于它们进行有损压缩和无损压缩过程中采用了不同的算法组合,接下来我们将从不同的图像文件格式入手,看看它们的特点和使用场景,以及在具体业务中应该如何选取。

JPEG

JPEG 可能是目前所有图像格式中出现最早,同时也是使用范围最广的一种格式。它也是一种有损压缩算法,它通过去除相关冗余图像和色彩数据等方式来获得较高的压缩率,同时还能展现出相当丰富的图像内容。

JPEG 在网站开发中经常被用作背景图、轮播图或者一些商品的 banner 图,以呈现色彩丰富的内容。但由于是有损压缩,当处理 Logo 或图标时,需要较强线条感和强烈颜色对比,JPEG 图像可能会出现一些边界模糊的不佳体验,另外 JPEG 图像并不支持透明度。

接下来介绍有关 JPEG 常用的压缩编码方式,以及在工程实践中如何自动批量处理。

压缩模式

JPEG 包含了多种压缩模式,其中常见的有基于基线的、渐进式的。

基线(又叫顺序)模式的 JPEG 加载顺序是自上而下的,当网络连接缓慢或不稳定时,其是从上往下逐渐加载完成的,如图所示:

前端性能优化学习 07 图片优化_第4张图片

渐进式模式是将图像文件分为多次扫描,首先展示一个低质量模糊的图像,随着扫描到的图像信息不断增多,每次扫描过后所展示的图像清晰度也会不断提升,如图所示:

前端性能优化学习 07 图片优化_第5张图片

渐进式 JPEG 的优缺点

渐进式 JPEG 的优点是显而易见的,在网络连接缓慢的情况下,首先能快速加载出一个图像质量比较模糊的预览版本。这样用户便可据此了解图像的大致内容,来决定是否继续花费时间等待完整图像的加载。这样做可以很好地提高对用户的感知性能,用户不仅知道所访问图像的大致内容,还会感知完整的图像就快加载好了。如果你平时留心观察,应该能注意到渐进式 JPEG 已经在渐渐取代基线 JPEG 了。

通过了解两种压缩的原理不难发现,渐进式 JPEG 的解码速度会比基线的要慢一些,因为它增加了重复的检索开销。另外,通过渐进式 JPEG 压缩模式得到的图像文件也不一定是最小的,比如特别小的图像。所以是否要采用渐进式JPEG,需要综合考虑文件大小、大部分用户的设备类型与网络延迟。

创建渐进式 JPEG

如果所得到的图像不是渐进式 JPEG,那么我们可以通过许多第三方工具来进行处理,例如 imagemin、libjpeg、imageMagick 等。值得注意的是,这个步骤应当尽量交给构建工具来自动化完成,例如通过如下代码可以将该工作加入gulp 处理管道:

const gulp = require('gulp')
const imagemin = require('gulp-imagemin')
gulp.task('images', () => {
  gulp.src('image/*.jpg')
    .pipe(imagemin({
      progressive: true
    }))
    .pipe(gulp.dest('dist'))
})

在执行构建流程后,gulp 会调用 imagemin 的方法把 images 文件夹下的所有 jpg 后缀图像全部进行渐进式编码处理。

其它 JPEG 编码方式

除了常见的基线与渐进式压缩编码方式,还有几种现代的 JPEG 编码器,它们尝试以更高的保真度及压缩后更小的文件大小为目标,同时还兼容当前主流的浏览器。其中比较出色的有 Mozilla 基金会推出的 MozJPEG 和 Google 提出的 Guetzli。

MozJPEG 和 Guetzli 也都已经有了可靠的 imagemin 插件支持,其使用方式与渐进式 JPEG 处理方式类似,这里仅列出示例代码,具体工程化构建请结合项目实践自行改写:

const gulp = require('gulp')
const imagemin = require('imagemin')
const imageminMozJPEG = require('imagemin-mozjpeg') // 引入 MozJPEG 依赖包
const imageminGuetzli = require('imagemin-guetzli') // 引入 Guetzli 依赖包

// MozJPEG 压缩编码
gulp.task('mozjpeg', () => {
  gulp.src('image/*.jpg')
    .pipe(imagemin({
      imageminMozJPEG({ quality: 85 })
    }))
    .pipei(gulp.dest('dist'))
})
// Guetzli 压缩编码
gulp.task('guetzli', () => {
  gulp.src('image/*.jpg')
    .pipe(imagemin({
      imageminGuetzli({ quality: 85 })
    }))
    .pipei(gulp.dest('dist'))
})

MozJPEG 引入了对逐行扫描的优化及一些栅格量化的功能,最多能将图像文件压缩10%,而 Guetzli 则是找到人眼感知上无法区分的最小体积的 JPEG,那么两者的优化效果具体如何,又如何评价呢?

这里需要借助两个指标来进行衡量,首先是用来计算两个图像相似度的结构相似性分数(Structural Similarity index),简称 SSIM,具体的计算过程可以借助第三方工具 jpeg-archive 的 jpeg-compare 来进行,这个指标分数以原图为标准来判断测试图片与原图的相似度,数值越接近 1 表示和原图越相似。

Butteraugli 则是一种基于人类感知测量图像的差异模型,它能在人眼几乎看不出明显差异的地方,给出可靠的差别分数。如果 SSIM 是对图像差别的汇总,那么 Butteraugli 则可以帮助找出非常糟糕的部分。

下表列出了两个 MozJPEG 编码压缩后的数据比较示例:

原图大小 982KB Q=90 / 841KB Q=85 / 562KB Q=75 / 324KB
SSIM 0.999936 0.999698 0.999478
Butterraugli 1.576957 2.483837 3.66127
原图大小 982KB Q=100 / 945KB Q=90 / 687KB Q=85 / 542KB
SSIM 0.999998 0.99971 0.999508
Butterraugli 0.40884 1.580555 2.0996

不仅要考虑图像压缩的质量和保真度,还要关注压缩后的大小,没有哪种压缩编码方式在各种条件下都是最优的,需要结合实际业务进行选择。一些使用建议:

  • 一般的图片在使用一些外部工具找到图像的最佳表现质量后,再用 MozJPEG 进行编码压缩。
  • 如果不介意编码时间长,可以使用 Guetzli 会获得更高质量的图像。

虽然本节介绍了关于 JPEG 的若干编码器,也对它们之间的差别进行了比较,但需要明确的一点是,最终压缩后的图像文件大小差异更多地取决于设置的压缩质量,而非所选择的编码器。所以在对 JPEG 进行编码优化时,应主要关注业务可接受的最低图像质量。

GIF

GIF 是 Graphics Interchange Format (图形交换格式)的缩写,也是一种比较早的图像文件格式。由于对支持颜色数量的限制,256色远小于展示照片所需颜色的数量级,所以 GIF 并不适合用来呈现照片,可能用来呈现图标或 Logo 会更适合一些,但后来推出的 PNG 格式对于图形的展示效果更佳,所以当下只有在需要使用到动画时才会使用 GIF。

接下来探讨一些关于GIF的优化点。

单帧的 GIF 转化为 PNG

首先可以使用 npm 引入 ImageMagick 工具来检查 GIF 图像文件,看其中是否包含多帧动画。如果 GIF 图像文件中不包含多帧动画,则会返回一个 GIF 字符串,如果 GIF 图像文件中包含动画内容,则会返回多帧信息。

对于单帧图像的情况,同样可使用 ImageMagick 工具将其转化为更适合展示图形的 PNG 文件格式。

node-imagemagick 已经很久没有维护,建议改用 gm 模块或 wasm-imagemagick

GIF 动画的优化

由于动画包含了许多静态帧,并且每个静态帧图像上的内容在相邻的不同帧上通常不会有太多的差异,所以可通过工具来移除动画里连续帧中重复的像素信息。这里可借助 gifsicle 来实现。

用视频替代动画

当了解过 GIF 的相关特性后,不难发现如果单纯以展示动画这个目的来看,那么 GIF 可能并不是最好的呈现方式,因为动画的内容将会受到诸如图像质量、播放帧率及播放长度等因素的限制。

GIF 展示的动画没有声音,最高支持 256 色的图像质量,如果动画长度较长,即便压缩过后文件也会较大。综合考虑,建议将内容较长的 GIF 动画转化为视频后进行插入,因为动画也是视频的一种,成熟的视频编码格式可以让传输的动画内容节省网络带宽开销。

可以利用 ffmpeg 将原本的 GIF 文件转化为 MPEG-4 或 WebM 的视频文件格式,例如一个 14MB 的 GIF动画通过转化后得到的视频文件格式大小分别是:MPEG-4格式下 867KB,WebM 格式下 611KB。

另外,要知道通过压缩后的动画或视频文件,在播放前都需要进行解码,可以通过 Chrome 的跟踪工具(chrome://tracing)查看不同格式的文件,在解码阶段的 CPU 占用时,文件格式与 CPU 耗时如表所示:

文件格式 CPU 耗时(ms)
GIF 2,668
MPEG-4 1,995
WebM 2,354

从表中可以看出,相比视频文件,GIF 在解码阶段也是十分耗时的,所以出于对性能的考虑,在使用GIF前应尽量谨慎选择。

PNG

PNG 是一种无损压缩的高保真图片格式,它的出现弥补了 GIF 图像格式的一些缺点,同时规避了当时 GIF 中还处在专利保护期的压缩算法,所以也有人将 PNG 文件后缀的缩写表示成“PNG is Not GIF”。

相比于 JPEG,PNG 支持透明度,对线条的处理更加细腻,并增强了色彩的表现力,不过唯一的不足就是文件体积太大。如果说 GIF 是专门为图标图形设计的图像文件格式,JPEG 是专门为照片设计的图像文件格式,那么 PNG 对这两种类型的图像都能支持。通常在使用中会碰到 PNG 的几种子类型,有 PNG-8、PNG-24、PNG-32 等。

对比 GIF

其中 PNG-8 也称为调色板 PNG,除了不支持动画,其他所有 GIF 拥有的功能它都拥有,同时还支持完全的 alpha 通道透明。只要不是颜色数特别少的图像,PNG-8 的压缩比表现都会更高一筹。

所以在使用单帧图形图像时,应当尽量用 PNG-8 格式来替换 GIF 格式。

对于多个颜色数少的单帧图形图像来说,更好的做法也并不是将它们分别存为一个 PNG-8 文件,相比之下,雪碧图都会更好一些,因为能够大大降低HTTP请求的开销,这一点后面章节会接着介绍。

对比 JPEG

当所处理图像中的颜色超过 256 种时,就需要用到 JPEG 或者真彩 PNG,真彩 PNG 包括 PNG-24 和 PNG-32,二者的区别是真彩 PNG-24 不包括 alpha 透明通道,而加上 8 位的 alpha 透明通道就是真彩 PNG-32。

JPEG 是有损的,它拥有更高的压缩比,也是照片存储的实际标准,如果还是要用 PNG,那么很可能是在清晰的颜色过度周围出现了不可接受的“大色块”。

例如:

前端性能优化学习 07 图片优化_第6张图片

优化 PNG

PNG 图像格式还有一个优点,就是便于扩展,它将图像的信息保存在“块”中,开发者便可以通过添加一些自定义的“块”来实现额外的功能,但所添加的自定义功能并非所有软件都能读取识别,大部分可能只是特定的作图软件在读取时使用而已。

对 Web 显示而言,浏览器可能直接将这些多余的块自动忽略掉了,如果对显示没有作用,那么又何必要存储和传输这些信息呢?因此我们可以使用 pngcrush 对这些多余的块进行删除压缩。

WebP

前面介绍的三种图像文件格式,在呈现位图方面各有优劣势:GIF 虽然包含的颜色阶数少,但能呈现动画;JPEG 虽然不支持透明度,但图像文件的压缩比高;PNG 虽然文件尺寸较大,但支持透明且色彩表现力强。

开发者在使用位图时对于这样的现状就需要先考虑选型。假如有一个统一的图像文件格式,具有之前格式的所有优点,WebP 就在这样的期待中诞生了。

WebP 的优缺点

WebP 是 Google 在 2010 年推出的一种图像文件格式,它的目标是以较高的视觉体验为前提的,尽可能地降低有损压缩和无损压缩后的文件尺寸,同时还要支持透明度与动画。根据 WebP 官方给出的实验数据,在同等 SSIM 质量指数下,使用 WebP 有损文件,文件尺寸会比 JPEG 小25%~34%,而使用 WebP 无损文件,文件尺寸会比 PNG 小 26%。对于可以接收有损 RGB 压缩的情况,有损 WebP 还支持透明度,通常提供比 PNG 小 3 倍的文件大小。

WebP 的兼容性如下:

前端性能优化学习 07 图片优化_第7张图片

从图中可以发现,主流浏览器的最新版本都已支持 WebP,考虑到浏览器的市场占有率,这样的兼容性程度可以说是非常乐观的了。虽然还需要做一些兼容性处理,但我们也有足够的理由在项目中积极地使用 WebP。此外,由于有损压缩 WebP 使用了 VP8 视频关键帧编码,可能对较高质量(80~99)的图像编码来说,会比 JPEG 占用更多的计算资源,但在较低质量(0~50)时,依然有很大的优势。

如何使用 WebP

可以使用图像编辑软件直接导出 WebP 格式的图像文件,或者将原有的 JPEG 或 PNG 图像转化为 WebP 格式。这样的转化最好使用构建工具辅助完成,比如通过 npm 安装 webp-loader 后,在 webpack 中进行如下配置:

loader: [{
  test: /\.(jpe?g|png)$/I,
  loaders: [
    'file-loader',
    'webp-loader?{quality: 13}'
  ]
}]

这里值得注意的是,尽量不要使用低质量的 JPEG 格式进行 WebP 转化,因为低质量的 JPEG 中可能包含压缩的伪像,这样 WebP 不仅要保存图像信息,还要保存 JPEG 添加的失真,从而影响最终的转化效果。所以在选择转化的源图像文件时,建议使用质量最佳的。

兼容性处理

目前 WebP 格式的图像并不适用于所有浏览器,所以在使用时,应当注意兼容处理好不支持的浏览器场景。

通常的处理思路分为两种:一种是在前端处理浏览器兼容性的判断,可以通过浏览器的全局属性 window.navigator.userAgent 获取版本信息,再根据兼容支持情况,选择是否请求 WebP 图像格式的资源;也可以使用 标签来选择显示的图像格式,在 标签中添加多个 标签元素,以及一个包含旧图像格式的 标签,当浏览器在解析DOM时便会对 标签中包含的多个图片源依次进行检测。

倘若浏览器不支持 WebP 格式而未能检测获取到,最后也能够通过 标记兼容显示出旧图像格式,例如:

<picture>
  <source srcset="/path/image.webp" type="image/webp" />
  <img src="/path/image.jpg" alt="" />
picture>

这里需要注意的是 标签的顺序位置,应当将包含 image/webp 的图像源写在旧图像格式的前面。

另一种是将判断浏览器是否支持的工作放在后端处理,让服务器根据 HTTP 请求头的 Accept 字段来决定返回图像的文件格式。如果 Accept 字段中包含 image/webp,就返回 WebP 图像格式,否则就使用旧图像格式(JPEG、PNG等)返回。这样做的好处是让系统的维护性更强,无论浏览器对 WebP 图像格式的兼容支持发生怎样的改变,只需要服务器检查 Accept 字段即可,无须前端跟进相应的修改。

SVG

前面介绍的几种图像文件格式呈现的都是位图,而 SVG 呈现的是矢量图。正如我们在介绍位图和矢量图时讲到的,SVG 对图像的处理不是基于像素栅格的,而是通过图像的形状轮廓、屏幕位置等信息进行描述的。

优缺点

SVG 这种基于 XML 语法描述图像形状的文件格式,就适合用来表示 Logo 等图标图像,它可以无限放大并且不会失真,无论分辨率多高的屏幕,一个文件就可以统一适配;另外,作为文本文件,除了可以将 SVG 标签像写代码一样写在 HTML 中,还可以把对图标图像的描述信息写在以 .svg 为后缀的文件中进行存储和引用。

由于文本文件的高压缩比,最后得到的图像文件体积也会更小。要说缺点与不足,除了仅能表示矢量图,还有就是使用的学习成本和渲染成本比较高。

优化建议

即便 SVG 图像文件拥有诸多优点,但依然有可优化的空间。下面介绍一些优化建议:

(1)应保持SVG尽量精简,去除编辑器创建 SVG 时可能携带的一些冗余信息,比如注释、隐藏图层及元数据等。

(2)由于显示器的本质依然是元素点构成位图,所以在渲染绘制矢量图时,就会比位图的显示多一步光栅化的过程。为了使浏览器解析渲染的过程更快,建议使用预定义的 SVG 形状来代替自定义路径,这样会减少最终生成图像所包含标记的数量,预定义形状有 等。

(3)如果必须使用自定义路径,那么也尽量少用曲线。

(4)不要在 SVG 中嵌入位图。

(5)使用工具优化 SVG,这里介绍一款基于 node.js 的优化工具 svgo,它可以通过降低定义中的数字精度来缩小文件的尺寸。通过 npm install -g svgo 可直接安装命令号方式使用,若想用 webpack 进行工程化集成,可加入 svgo-loader 的相关配置:

module.exports = {
  ...,
  module: {
    rules: [
      {
        test: /\.svg$/,
        use: [
          {
            loader: 'file-loader'
          },
          {
            loader: 'svgo-loader',
          }
        ]
      }
    ]
  }
}

其中,可在 svgo.config.js 的配置文件中定义相关优化选项

(6)在优化过后,使用 gzip 压缩和(或)brotli 压缩。

Base64

准确地说,Base64 并不是一种图像文件格式,而是一种用于传输 8 位字节码的编码方式,它通过将代表图像的编码直接写入HTML 或 CSS 中来实现图像的展示。一般展示图像的方法都是通过将图像文件的 URL 值传给 标签的 src 属性,当 HTML 解析到 标签时,便会发起对该图像 URL 的网络请求:

<img src="https://xx.cdn.com/photo.jpg">

当采用 Base64 编码图像时,写入 src 的属性值不是 URL 值,而是类似下面的编码:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACcAAAApCAYAAAC2qTBFAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABGSURBVFhH7c6hDQAwDMCw7v+jOz4YNGBZ4Zmzs2/fMFeZq8xV5ipzlbnKXGWuMleZq8xV5ipzlbnKXGWuMleZq8xV5pLZC2GjcwSKPE5fAAAAAElFTkSuQmCC

浏览器会自动解析该编码并展示出图像,而无须发起任何关于该图像的 URL 请求,这是 Base64 的优点,同时也隐含了对于使用的限制。由于 Base64 编码原理的特点,一般经过 Base64 编码后的图像大小,会膨胀四分之三。

这对想尝试通过 Base64 方式尽可能减少 HTTP 请求次数来说是得不偿失的,较复杂的大图经过编码后,所节省的几次 HTTP 请求,与图像文件大小增加所带来的带宽消耗相比简直是杯水车薪。所以也只有对小图而言,Base64 才能体现出真正的性能优势。

作为使用指导建议,建议在考虑是否使用 Base64 编码时,比对如下几个条件:

  • 图像文件的实际尺寸是否很小。
  • 图像文件是否真的无法以雪碧图的形式进行引入。
  • 图像文件的更新频率是否很低,以避免在使用 Base64 时,增加不必要的维护成本。

格式选择建议

在了解了不同图像文件格式的特性后,显而易见的是不存在适用于任何场景且性能最优的图像使用方式。所以作为开发者,想要网站性能在图像方面达到最优,如何根据业务场景选择合适的文件格式也至关重要,图像文件使用策略如图所示:

前端性能优化学习 07 图片优化_第8张图片

这里根据使用场景的不同,以及图像文件的特性给出了一个可参考的选择策略:

  • 考虑到矢量图具有缩放不失真且表示图标时较小的文件尺寸,凡用到图标的场景应尽量使用矢量图;
  • 而对于位图的使用场景,由于在相同图像质量下其具有更高的压缩比且支持动画,所以 WebP 格式应该是我们的首选。
  • 考虑到新技术的兼容性问题,也需要采用传统的方式进行适配;包含动画时使用GIF,对图像要求有更高分辨率来展示细节且需要透明度时,建议使用PNG;
  • 而在其他场景下追求更高的图像压缩比时,可使用 JPEG。
  • 除此之外,位图对于不同缩放比的响应式场景,建议提供多张不同尺寸的图像,让浏览器根据具体场景进行请求调用。

CSS Sprite

CSS Sprite 称为图像精灵,或雪碧图(雪碧英文:sprite)。

原理就是把页面上(或是整个网站)上用到的小图片合并成一张大图片,通过 css 的 background-imagebackground-position 来使用对应的图片。

这个概念提出的早期年代还没有自动化构建工具,通常需要开发者自己制作雪碧图,用 PS 或雪碧图软件,可谓非常痛苦。

后来由于构建工具的出现,很多自动生成雪碧图的工具也大量出现,例如:

  • webpack-spritesmith
  • postcss-sprites

雪碧图的目的是减少 HTTP 请求数以提高性能,不过随着 Web 的快速发展和 UI 设计的风格调整,这项技术已经越来越少用了:

  • 由于 HTTP 新版本支持的长连接和多路复用技术,合并图片减少 HTTP 请求次数带来的性能优化性价比在慢慢降低,假设雪碧图中合并了大量当前页面不需要使用的图标。
  • 而且对于单色的图标,开发者更倾向于使用 web 字体,如阿里的 iconfont,同样可以实现减少 HTTP 请求的性能提升。
  • 修改其中一个图标就需要重新生成整个雪碧图,需要注意不要影响到其它在用的图标

所以这项技术是否使用还要基于实际场景去考虑。

Web Font 字体图标

Web 字体即通过自定义字体代替图片实现一样的效果,因为主要用于内容简单的图标,所以又叫 字体图标(icon font)

原理

计算机操作系统里面每个字符都有一个 unicode 编码,根据不同的编码浏览器会自动帮你找到对应的图形去渲染。而字体文件的作用是规定某个字符应该用什么形状来显示。unicode 字符集里面,E000 至 F8FF 属于用户造字区。用户可以在字体文件里面随便定义这些字符的形状,通过项目引入加载去找到自定义字符,这就和使用操作系统的字体是一样的了。

字体图标的实现原理就是在这个自定义区域中添加一系列的图标,我们通过指定对应的 Unicode 编码来显示图片。

浏览器流程:使用浏览器打开页面时,浏览器会读取 HTML 文件进行解析渲染。当读到某个文字时会转换成对应的 Unicode 码(可以认为是世界上任意一种文字的特定编号)。再根据 HTML 里设置的 font-family (如果没设置则使用浏览器默认设置)去查找电脑里(如果有自定义字体 @font-face ,则加载对应字体文件)对应字体的字体文件。找到文件后根据 Unicode 码去查找绘制外形,找到后绘制到页面上。

示例

加载自定义字体

/* 加载自定义字体 */
@font-face {
  font-family: 'iconfont';
  src: url('iconfont.eot');
}
.iconfont {
  font-family:"iconfont";
}
.icon-xxx:before {
  contetn: "\e7ac";
}

使用自定义字体

<i class="iconfont icon-xxx" />

优势

字体图标自然包含了减少图片的的 HTTP 请求,提高网站性能。同时相比于雪碧图,字体图标本质上是字体,可以通过 CSS 设置 font-size 改变图标大小,同时不会造成图片失真,也可以通过 color 设置字体颜色,相比续笔涂更加灵活。

缺点

  • 字体图标只能应用于颜色单一的图标,相比之下,雪碧图可以提供更好的色彩表现。
  • 仍然存在加载一个包含数百个图标的字体,但是只是用其中几个的情况
  • 为实现最大程度的浏览器支持,可能需要提供至少4种不同类型的字体文件,包括 TTF、WOFF、EOT 等。
  • 网络延迟时会导致先加载出来一个 unicode 字符串。

工具

网上有很多字体图标库可供选择:

  • iconfont
  • Font Awesome

注意 display: none 的使用

display:none 样式可能会影响图片的加载(请求)和渲染(绘制)。

(1)设置了 display:none 元素,会发起请求,但不会渲染

<img src="pic1.png" />

(2)设置了 display:none 的元素,其背景和后代元素的背景不会产生请求,但后代 元素会产生请求,以及这些图片都不会被渲染。

<div class="parent" style="display:none; background:url(pic1.png)">
  <div class="child" style="background:url(pic2.png)">div>
  <img src="pic3.png" />
div>


(3)重复图片和背景只会发送一次请求

<div style="background: url(pic1.png)">div>
<img src="pic1.png"  />
<div style="background: url(pic1.png)">
  <img src="pic1.png" style="display: none;"  />
div>

(4)不存在的元素,设置背景不会发送请求

<style>
  .test2 {background: url(pic1.png)}
style>
<div class="test1">div>

(5)设置了 visibility:hidden 的元素,其背景、后代 元素、后代元素的背景(没有设置 display:none)以及自身是 元素时,都会发起请求,并且会渲染

<img src="pic1.png" style="visibility: hidden;"  />
<div class="parent" style="height: 100px;visibility: hidden; background: url(pic2.png)">
  <div class="child" style="background: url(pic3.png)">div>
  <img src="pic4.png" />
div>

(6)使用 JS 加载的图片会发起请求

<button onclick="fetchImage()">请求图片button>
<div class="box">div>
<script>
  function fetchImage() {
    const img = new Image()
    img.src = 'pic1.png'
    img.onload = () => {
      document.querySelector('.box').append(img)
    }
  }
script>

参考

  • 【翻译】电子书:图像优化自动化实用指南
  • 从 HTTP 协议求解:为什么我们越来越不需要雪碧图了?
  • [字体图标的原理](https://jishuin.proginn.com/p/763bfbd6fc48#:~:text=而字体图标的实现原理就是在这个自定义区域中添加一系列的图标,我们通过指定对应的,Unicode 编码来显示图片。)

小结

最后给出一些方法与技巧:

  • 适合用矢量图的地方首选矢量图。、
  • 使用位图时首选 WebP,对不支持的浏览器场景进行兼容处理。
  • 尽量为位图图像格式找到最佳质量设置。
  • 删除图像文件中多余的元数据。
  • 对图像文件进行必要的压缩。
  • 为图像提供多种缩放尺寸的响应式资源。
  • 对工程化通用图像处理流程尽量自动化。

你可能感兴趣的:(性能优化,前端性能优化学习)