本文主要是介绍图片和web性能小论,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
图片让web性能变得复杂,也变得有趣了。
相信初学者一定看到过这样的案例:“当你在HTML中将一张500*500
像素的图像缩小,就会带来不必要的下载开销。”
<img src="xxx" width="100" height="100" alt="xxx" />
在这个例子中,你让浏览器将图片在视觉上缩小到了100*100
,但浏览器还是要下载那张大图。也就是说,下载所需流量没有变。但更要命的是,有些浏览器将缩放图像作为一个“卖点”但其实并没有流行的软件比如 ImageMagic 做得好。这就导致了图片质量降低的同时增大了下载量 —— 但如果你在服务端就改变了图像的大小,并提供了一个较小的版本,就可以明显节省流量。
上面例子中你可能发现了一点:是的,图片的加载实际是一次 get 请求,如果不对图像进行优化,就相当于通过网络发送了一些对用户体验没有任何帮助的额外数据。
对图片的优化在web中又许多手段,本文将会谈谈图片本身,即压缩和图片格式两方面。
图片格式和使用
更严格来说,我们接下来讨论的已经不单单是“图片”范畴了,我们可以称之为“图像”。
首先要明确的是:优化方法取决于图像的基本类型。
在游戏开发中大放光彩的ASTC纹理压缩格式和矢量图标svg等暂且不论。目前web中常见的,也可以说是主流的格式有 JPG、PNG、GIF 以及后起之秀 JPEG。其中根据所包含色彩数量和线条又被分为两大类:
- 图形:比如 Logo、图表、图标等。颜色数量较少、拥有连续的线条或其它尖锐的颜色分割;
- 照片:拥有超大数量级颜色,并且包含平滑的颜色过渡和渐变;
你可能难以想象的是,就图片格式而言,GIF 通常用来显示「图形」,而 JPEG 更适合「照片」。PNG 则两者皆可。事实上,GIF 通常被很多人忽视,但却拥有强大的性能,目前流行的 PNG8 格式都与其极其相似。
GIF
采用8位压缩的GIF最多只能处理256种颜色。支持动画,这是它强大的一点,也是早起被滥用导致现在很多人反感的一点(笔者曾听人说宁愿用CSS做动画也不愿用gif)。尽管在其规范中表明了“这个图形交换格式不是要成为一个动画平台…”
一直以来笔者对 GIF 的感受都是“快”和“小”。而GIF的特点却不只有这俩:
- 透明
- 无损
- 支持隔行扫描
隔行扫描与逐行扫描
有人可能遇到过这样的问题:为什么左边的图和右边的图大小不一致?
这是因为图片是“逐行扫描”的:比如在生成 GIF 格式文件时,会使用一个压缩算法(GIF中使用的是LZW无损压缩算法)来减小文件的大小。当压缩时,会从上到下一行一行地对像素进行扫描。这种情况下,当图像在水平方向有很多重复颜色时,可以获得更好的压缩效果。
比如,有一个500*100
像素的图像(宽: 500px
高:100px
),图像上包含一些条纹,就是说水平方向是由相同颜色线条组成的,将这个图像旋转90°
后(宽: 100px
高: 500px
),其垂直方向是由相同颜色的条纹组成的,此时后者的文件要大于前者。
“从上到下扫描”就是逐行扫描的劣势之一。相比之下隔行扫描却能够令图片在浏览器中更快的加载和显示。此特性对于那些慢网速的浏览者来说尤其实用。
PNG
目前使用的 PNG 格式基本上可以分为两种:调色板 PNG (也就是上面说的PNG8 !)和真彩色 PNG。
PNG8是指“8位PNG图片”:像素颜色不能超过256种,它支持alpha透明背景。除了压缩算法不同之外,此8位PNG格式与GIF格式极其相似;
对比 GIF,PNG8使用了比LZW更优秀的压缩算法 —— LZ77算法(当然也有横向条纹的问题),而且支持alpha非全透明效果。所以在日常使用中应该尽可能用png8代替gif!
除非在一些颜色数量极少的非常小的图像中。这时候gif的压缩率更高一些…但是这种情况下如果小图像数量超过1个则应该使用Sprite中,从而减少http请求开销。
对真彩色PNG,也有别称叫“PNG24”(不包括alpha通道)和“PNG32”(包括alpha通道) —— 拿24位PNG来说,它支持160万种不同的像素颜色且支持Alpha透明效果,这就意味着,无论透明度设置为多少,PNG图片均能够与背景很好的融合在一起。
曾有人提议可以用来在使用中代替jpeg。但是 png24 一般来说大小是 jpeg 的5倍还多,除非在需要追求显示效果的场景中,不然大可不必如此。
可以使用Adobe Fireworks软件生成 PNG8 。如果你有一张真彩色 PNG,也可以使用命令行工具
pngquant
将其转化为 PNG8!
对了,png也支持“隔行扫描”。不过支持隔行扫描的png文件在文件大小上要稍大一些。
关于PNG的压缩,想多说一点
PNG的压缩分为两个阶段,预解析和压缩。
其中“预解析”指“用差分编码(Delta encoding)预处理图片像素点中每条通道的值”。但这不是本文的重点。
而压缩阶段就是将预处理阶段得到的结果进行Deflate压缩,它由 哈夫曼编码 和 LZ77压缩共同构成!
LZ77是一个基于字典的算法,它使用前向缓冲区和一个滑动窗口实现。前向缓冲区是与动态窗口相对应的,它被用来存放输入流的前n个字节。常用滑动窗口4KB,前向缓冲区32B。
算法主要思想就是在前向缓冲区中不断寻找能够与字典中短语匹配的最长短语。如果匹配的数据长度大于最小匹配长度,那么就输出一对〈长度,距离滑动窗中对应的位置〉
数组。长度是匹配的数据长度,而距离说明了在输入流中向后多少字节这个匹配数据可以被找到:
int lz77_compress(const unsigned char *original, unsigned char **compressed, int size)
{unsigned char window[LZ77_WINDOW_SIZE], buffer[LZ77_BUFFER_SIZE], *comp, *temp, next;int offset, length, remaining, hsize, ipos, opos, tpos, i;int token, tbits;//初始化 *compressed = NULL;memset(window, 0, LZ77_WINDOW_SIZE);memset(buffer, 0, LZ77_BUFFER_SIZE);//向头信息中写入源数据字节数 hsize = sizeof(int);comp = (unsigned char *)malloc(hsize);memcpy(comp, &size, sizeof(int)); ipos = 0;//ipos指向源数据中正在处理的字节//从源数据中取数据到缓冲区中 for(i = 0; i < LZ77_BUFFER_SIZE && ipos < size; i++){buffer[i] = original[ipos];ipos++;} opos = hsize * 8;//opos是压缩数据bit的位置 remaining = size;while(remaining > 0){//标记 = type + offset(在window中) + length + next //next就是不匹配的字符 //tbit表示生成标记长度 if((length = compare_win(window, buffer, &offset, &next)) != 0){//能找到type为1 token = 0x0000_0001 << (LZ77_PHRASE_BITS - 1);token = token | (offset << LZ77_PHRASE_BITS - LZ77_TYPE_BITS - LZ77_WINOFF_BITS);token = token | (length << LZ77_PHRASE_BITS - LZ77_TYPE_BITS - LZ77_WINOFF_BITS - LZ77_BUFLEN_BITS);token = token | next;tbits = LZ77_PHRASE_BITS;}else{//没找到 ,标记就是原符号 token = 0x0000_0000;token = token | next;tbits = LZ77_SYMBOL_BITS;} //s数据处理为大端模式 token = htonl(token);//往压缩区填数据for(i = 0; i < tbits; i++){if(opos % 8 == 0){temp = (unsigned char *)realloc(comp, (opos / 8) + 1);comp = temp;}//根据长度tbits取一位一位压缩tpos = (sizeof(unsigned long) * 8) - tbits + i; bit_set(comp, opos, bit_get((unsigned char *)&token, tpos));} length++;//length是匹配数据字节长度//左移更新window把buffer中以编码的字符移到window memmove(&window[0], &window[length], LZ77_WINDOW_SIZE - length); memmove(&window[LZ77_WINDOW_SIZE - length], &buffer[0], length);//更新buffer中内容,做移除已经编码的字符,从源数据中调入新字符 memmove(&buffer[0], &buffer[length], LZ77_BUFFER_SIZE - length);for(i = LZ77_BUFFER_SIZE - length; (i < LZ77_BUFFER_SIZE) &&(ipos < size); i++){buffer[i] = original[ipos];ipos++;}remaining = remaining - length;} *compressed = comp;return ((opos - 1) / 8) + 1;
}
JPEG
JPEG 是一种有损的图像格式。一旦编辑,即使设置了100的质量也会有一定的损耗。只有在下面几个操作中是无损的:
- 旋转(且只有在90°、180° 和 270° 情况下)
- 裁剪
- 翻转(且水平或垂直)
- 在标准模式和渐进模式间切换
- 编辑图像元数据(优化 JPEG 格式图像的重要手段之一)
需要注意的是,只有在「渐进模式」时 JPEG 格式图像才支持隔行扫描,而且 IE 并不会和其它浏览器一样逐步地渲染渐进 JPEG 图像。。。
渐进式JPEG
你可以理解为“多次扫描”(扫描顺序是存储在JPEG文件中的):打开文件过程中,会先显示整个图片的模糊轮廓,随着扫描次数的增加,图片变得越来越清晰 ——
目前很多技术手段都可以去模拟「渐进式jpeg」的加载过程。比如vue中就可以用到progressive-image
库:
import Vue from 'vue'
import progressive from 'progressive-image/dist/vue'
import 'progressive-image/dist/index.css'Vue.use(progressive, {removePreview: true,scale: true
})
<img class="preview" v-progressive="实际地址" :src="预览图" />
webp!新时代的抉择
由Google开发的webp格式,集无损、有损(同时支持无损和有损压缩)、足够小、质量高等特性于一身。毫无疑问是提升访问体验的“宠儿”。
but,兼容性至今仍然是其硬伤。所以,使用时必然考虑“降级” —— 比如在阿里云OSS中配置降级代码。
图像压缩
PNG
PNG 也支持无损压缩。PNG 格式将图像信息保存在“块”中。这种方式很利于扩展,因为你可以添加一些自定义的块实现额外功能,而且不识别这些块的程序会自动忽略这些内容。但对于Web显示来说,大部分的块都并非必要,我们可以安全地将它们删除。还有一点好处,当我们将叫做gamma的块删除后,实际上会提升跨浏览器的显示效果,因为各个浏览器对gamma矫正有着迥然不同的支持!
笔者在实践中非常喜欢用 pngcrush 和 optIPNG 两个工具。
其中,pngcrush 的基本命令如下:
pngcrush -rem alla -brute -reduce src.png dest.png
支持的参数有:
-rem alla
:删除所有的块,但保留控制透明的alpha块。-brute
:使用超过100种不同的方法进行压缩,默认值是10种。加了这个参数以后会慢很多,而且大部分情况下改进的效果很小。但是如果你是离线进行这个操作,完全可以为这个操作多付出1~2秒的时间,因为这个操作可以找到效果更好的方法来压缩图像。但如果使用的场景对性能要求很高,就不要使用这个参数了。- reduce
:如有可能,尝试减少调色板中的颜色数量。src. Png
:源图片。dest. Png
:目标(优化后的)图片。
它是在MSDOS窗口中一个命令行、或从UNIX或LINUX命令行中运行的。
而 optipng 是一个跨平台命令行工具,使用起来要更加简单。拿我司之前秒杀活动分享海报来说 ——
下载后:
yum install optipng # centos
apt-get install optipng # Ubuntu
压缩前
执行命令:
find . -iname '*.png' -print0 | xargs -0 optipng -o7 -preserve
可以看到,硬生生少了一半的大小!
JPEG
JPEG文件中包含如下的元数据:
- 注释。
- 应用程序定义的内部信息( 比如Photoshop)。
- EXIF信息,比如拍摄用的相机型号、拍摄日期、拍摄位置、缩略图等,甚至还可以包含音频信息等。
这些元数据不会影响图像显示,可以被安全地移除。对元数据的处理,凑巧也是我们之前提到的对JPEG进行无损压缩的方法之一,可以将文件中那些不需要的部分直接剔除,而不会影响视觉质量。
命令行工具 jpegtran 可以通过命令行完成这些转换工作:
jpegtran -copy none -optimize src.jpg dest.jpg
参数如下:
-copy none
:设置不包含任何元数据。你还可以使用--strip-all
刚好相反;-optimize
:强制对霍夫曼表进行优化,从而获得更高的压缩比;src. jpg
:需要优化的图像;dest.jpg
:优化过的图像;
这里依然放一条之前笔者在项目开发过程中使用到的命令(Linux环境 - Ubuntu):
find . -iname '*.jpg' -print0 | xargs -0 jpegoptim --strip-all --preserve --totals --all-progressive
静态GIF转PNG
正如上面说过的,在实际开发中可以将 gif 转为 png8 格式。这个过程可以通过大名鼎鼎的 ImageMagic 来完成。(好像大多数格式之间转换的都是通过此工具实现的)
convert source.gif PNG8:destination.png
很简明,没有什么多余的配置 —— 当然,没有不代表不能加。
实际上,ImageMagic还可以实现“合成gif”、“调整图片大小”、“剪裁图片”、“压缩”、“滤镜”、“图片取反”、“边缘检测”等功能。
需要注意的是:如果很多子命令不能直接使用,则可以把他们当做magick的子命令使用,比如检测 GIF 中是否包含动画的
identify bbb.gif
# 如果上面的行不通,可以在前面加上“magick”:
magick identify gif
对于无损压缩来说,你可以反复执行同样的命令,以得到尽可能优的结果!
sprite优化
上面提到了CSS Sprite,是指将多个背景图片合并到一个较大的图片中,通过修改背景的位置,在元素上显示背景图片的一部分(将加载显示多张小图片变为加载一张大图片显示不同位置)。这项技术应该是最先被 Yahoo! 采用,通过减少 Yahoo! 首页上小图标所带来的请求数来提升性能!
随着sprite技术的“滥用”,我们现在其实越来越应该关注“sprite数量”、“维护成本”和“不重复页面”三者之间的抉择。然后有人提出了“超级sprite”和“模块化sprite” —— 其实就是将“相似”的小图像放在一起。
比如Google搜索只有两个页面,它们就选择了“将所有需要用到的图标放在一张sprite中”。
但如果你的网站有很多页面,就需要一个 不同的Sprite策略,否则维护成本将会变得非常高。最终的目标应该是可以很方便地将那些不再使用的模块从网站删除。有人曾提出一种看法:你可以将同属于一种类型的图像合并在一起。比如
- 同一个圆角框的四个角;
- 模块头部滑动门]所用的左右两边;
- 按钮相关状态(如果背景或整个按钮就是一张图片 —— 不过现在css完全可以完成这件事);
- tab相关状态(当前、悬浮和正常效果);
- 按照颜色合并(将颜色相近的图标组合在一起)
实战场景
在vite开发的项目中,我们已经可以使用开箱即用的插件vite-plugin-imagemin
了。
npm i vite-plugin-imagemin -D
安装完毕后在 Vite 配置文件中这么引入:
import viteImagemin from 'vite-plugin-imagemin';{plugins: [// 忽略前面的插件viteImagemin({// 无损压缩配置,无损压缩下图片质量不会变差optipng: {optimizationLevel: 7},// 有损压缩配置,有损压缩下图片质量可能会变差pngquant: {quality: [0.8, 0.9],},// svg 优化svgo: {plugins: [{name: 'removeViewBox'},{name: 'removeEmptyAttrs',active: false}]}})]
}
然后直接执行 npm run build
进行打包即可。
结语:亿点点想法
近期开始研究图像算法方面,琢磨怎么将这些用于前端产出,毕竟就国内来说,不以加持项目的技术研究毫无意义(除了装b)。再结合之前听说一个同学竟然在反复看我的文章,不禁感叹,比我牛逼的人还比我更努力。于是…我也去又看了几篇之前写的文章😏
于是发现,之前关于图片的有些文章不仅看待问题太单一,而且很多文章中的图片处理方法一直没有说法,就那样烂在那里了。在这种背景下,促成了这篇文章的产生。
说起来为这篇文章整理之前的想法和一些“奇思怪想”费了我两三个深夜(煎熬良久还是决定不在摸鱼的时候干这事,我真是好员工,嘿嘿),结果最后发现很多东西放在这里上下文不合适。只好作罢!
最最最后,吐槽一下,ImageMagic官方文档真是一个糟糕的文档。over~
这篇关于图片和web性能小论的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!