Web性能优化(四):图片优化

3,248 阅读12分钟

--本文采自本人公众号【猴哥别瞎说】

必要性

图片的优化重要么?

作为一个开发者,直观感觉:重要!我们曾在工程化中为了压缩最终的代码文件大小,尝尽了许许多多的优化方式,最终优化的大小可能就是几百 kb。但是,现在网页上的一张图片,随随便便就可以有几百 kb 的大小呢。

如果直观感觉还不够明显,那么我们来看看网站的统计结果吧。网站 HTTP-Archive 会定期抓取 Web 上的站点,记录资源的加载情况。过去一年的统计结果如下:

可以看到,Web 请求中,有一半的流量是来自图片的请求。这个比重,是不是有点超乎意料呢?

雅虎军规和 Google 官方的最佳实践也都将图片优化列为前端性能优化必不可少的环节——图片优化的优先级可见一斑。

不同格式图片的适用场景

广泛应用的 Web 图片格式有 JPEG/JPG、PNG、WebP、Base64、SVG 等,这些格式都有各自的特点。

接下来,我们会从优点、适用场景与缺点这几个方面,对这几种格式做详细展开。

JPEG/JPG

优点

JPG 最大的特点是有损压缩。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉。

适用场景

JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。

使用 JPG 呈现大图,既可以保住图片的质量,又不会带来令人头疼的图片体积,是当下比较推崇的一种方案。

缺点

有损压缩在上文所展示的轮播图上确实很难露出马脚,但当它处理矢量图形Logo等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。

此外,JPEG 图像不支持透明度处理,透明图片需要召唤 PNG 来呈现。

PNG

优点

PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。

PNG 图片具有比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPG 的局限性,唯一的 BUG 就是体积太大。

PNG-8 与 PNG-24 的选择题 什么时候用 PNG-8,什么时候用 PNG-24,这是一个问题。

理论上来说,当你追求最佳的显示效果、并且不在意文件体积大小时,是推荐使用 PNG-24 的。

但实践当中,为了规避体积的问题,我们一般不用PNG去处理较复杂的图像。当我们遇到适合 PNG 的场景时,也会优先选择更为小巧的 PNG-8。

如何确定一张图片是该用 PNG-8 还是 PNG-24 去呈现呢?好的做法是把图片先按照这两种格式分别输出,看 PNG-8 输出的结果是否会带来肉眼可见的质量损耗,并且确认这种损耗是否在我们(尤其是你的 UI 设计师)可接受的范围内,基于对比的结果去做判断。

适用场景

前面我们提到,复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,我们一般会交给 JPG 去存储。

考虑到 PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。

缺点

体积太大是唯一的BUG。

SVG

SVG(可缩放矢量图形)是一种基于 XML 语法的图像格式。它和本文提及的其它图片种类有着本质的不同:SVG 对图像的处理不是基于像素点,而是是基于对图像的形状描述。

优点

和性能关系最密切的一点就是:SVG 与 PNG 和 JPG 相比,文件体积更小,可压缩性更强。

当然,作为矢量图,它最显著的优势还是在于图片可无限放大而不失真这一点上。这使得 SVG 即使是被放到视网膜屏幕上,也可以一如既往地展现出较好的成像品质——1 张 SVG 足以适配 n 种分辨率。

此外,SVG 是文本文件。我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。这使得 SVG 文件可以被非常多的工具读取和修改,具有较强的灵活性。

适用场景

SVG 是文本文件,我们既可以像写代码一样定义 SVG,把它写在 HTML 里、成为 DOM 的一部分,也可以把对图形的描述写入以 .svg 为后缀的独立文件(SVG 文件在使用上与普通图片文件无异)。

在实际开发中,我们更多用到的是后者。很多情况下设计师会给到我们 SVG 文件,就算没有设计师,我们还有非常好用的在线矢量图形库。

缺点

SVG 的局限性主要有两个方面,一方面是它的渲染成本比较高,这点对性能来说是很不利的。另一方面,SVG 存在着其它图片格式所没有的学习成本(它是可编程的)。

Base64

Base64 并非一种图片格式,而是一种编码方式。

适用场景

大家不妨打开淘宝首页,然后打开开发者工具,在源码中搜索“base64”关键字,你会发现 Base64 码出现的地方真的不少。而且它对应的图片往往是非常小的 Logo。

既然 Base64 这么棒,我们何不把大图也换成 Base64 呢?

这是因为,Base64 编码后,图片大小会膨胀为原文件的 4/3(这是由 Base64 的编码原理决定的)。如果我们把大图也编码到 HTML 或 CSS 文件中,后者的体积会明显增加,即便我们减少了 HTTP 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失。 在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 HTTP 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势。

因此,Base64 并非万全之策,我们往往在一张图片满足以下条件时会对它应用 Base64 编码:

  • 图片的实际尺寸很小
  • 图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是雪碧图的补充)
  • 图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)

WebP

WebP 是今天在座各类图片格式中最年轻的一位,它于 2010 年被提出, 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩。

优点

WebP 像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身。

WebP 的官方介绍对这一点有着更权威的阐述:

与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小 25-34%。 无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。对于有损 RGB 压缩可接受的情况,有损 WebP 也支持透明度,与 PNG 相比,通常提供 3 倍的文件大小。

图片优化是质量与性能的博弈,从这个角度看,WebP 无疑是真正的赢家。

缺点

WebP 纵有千般好,但它毕竟太年轻。我们知道,任何新生事物,都逃不开兼容性的大坑。

此外,WebP 还会增加服务器的负担——和编码 JPG 文件相比,编码同样质量的 WebP 文件会占用更多的计算资源。

适用场景

现在限制我们使用 WebP 的最大问题不是“这个图片是否适合用 WebP 呈现”的问题,而是“浏览器是否允许 WebP”的问题,即我们上文谈到的兼容性问题。具体来说,一旦我们选择了 WebP,就要考虑在 Safari 等浏览器下它无法显示的问题,也就是说我们需要准备 PlanB,准备降级方案。

如果决定使用 WebP,兼容性处理是必不可少的。

看完了不同类型的图片对应的不同适用场景,是否会有一些感悟与想法呢?

刚刚聊得是:选择什么样的图片类型?其实对于优化而言,还有另一个角度:是否有必要加载对应图片?毕竟能够不加载图片,是最好的情况。

下面,我们就来聊聊图片的懒加载。

图片懒加载

所谓“多做多错,少做少错,不做不错”。在性能优化的过程中,最好的优化结果是:不发送请求。对于图片优化而言,亦是如此。

图片懒加载在一些图片密集型的网站中运用比较多。

通过图片懒加载可以让一些当前不可视的图片不去加载,避免一次性加载过多的图片导致请求阻塞(浏览器一般对同一域名下的并发请求的连接数有限制),这样可以提高网站的加载速度,进而改善用户体验。

思路

对于图片懒加载,其基本思路是这样的:只要是不在当前视图前的图片,都不去加载。只有监控到对应图片出现在当前可视窗口了,才选择加载。可以这么做:

第一步: html 中需要懒加载的img标签的src设置缩略图或者不设置src,然后自定义一个属性,值为真正的图片或者原图的地址(比如下面的data-src),并且定义一个通用的类名,表示该图片是需要懒加载的(比如下面例子的lazy-image,也可以使用 img 元素(次优方案)),这有两个作用:

1. 为以后获取需要懒加载图片的img元素  2. 可以给这个类名设置背景图片,作为图片未加载前的过度图片,比如显示为loading的图片。

  //HTML
  <img data-src="https://bji/cms/banner_20191212_2.jpg" class="lazy-image"/>
  //CSS属性
  .lazy-image { 
    background: url('../img/loading.gif') no-repeat center; 
} 

第二步:页面加载完后,我们需要获取所有需要懒加载的图片的元素集合,判断是否在可视区域,如果是在可视区域的话,设置元素的src属性值为真正图片的地址。

更详细的,判断是否在可视区域的方法,有如下两种。

方式一:getBoundingClientRect

inViewShow() {     
    let imageElements = Array.prototype.slice.call(document.querySelectorAll('.lazy-image'))    
    let len = imageElements.length     
    for(let i = 0; i < len; i++) {         
        let imageElement = imageElements[i]        
        const rect = imageElement.getBoundingClientRect() // 出现在视野的时候加载图片         
        if(rect.top < document.documentElement.clientHeight) {             
            imageElement.src = imageElement.dataset.src // 移除掉已经显示的             
            imageElements.splice(i, 1)             
            len--             
            i--         
        }     
    } 
}

这里判断是否出现在可视区域内,是通过元素的 getBoundingClientRect 属性内部的 top 值和页面的 clientHeight 进行对比,如果 top 值小于 clientHeight,则说明元素出现在可视区域了。BoundingClientRect 函数是获取某个元素相对于视图的位置集合,各参数如下图所示。需要注意的是:bottom 和 right 和我们平时使用的 right 和 bottom 是不一样的。

方式二:Intersection Observer

上面我们利用元素的 BoundingClientRect 的 top 属性和 body 的 clientHeight 来判断元素是否可见,这种传统的获取元素是否可见的方式有一个确定:需要监听 scroll 事件。scroll 事件是很容易发生的,并且伴随着大量计算,会造成资源浪费。

一个新的Web API 是 Intersection Observer,它可以不用监听scroll事件,只要元素一可见便调用回调,在回调里面我们来调用对应的元素可见时的处理逻辑。

if ("IntersectionObserver" in window) {        
    let lazyImageObserver = new IntersectionObserver((entries, observer) => {          
        entries.forEach((entry, index) => {            
            // 如果元素可见            
            if (entry.intersectionRatio > 0) {              
                let lazyImage = entry.target              
                lazyImage.src = lazyImage.dataset.src              
                lazyImage.classList.remove("lazy-image")              
                lazyImageObserver.unobserve(lazyImage)              
                // this.lazyImages.splice(index, 1)            
            }          
        })        
    })        
    this.lazyImages.forEach(function(lazyImage) {          
        lazyImageObserver.observe(lazyImage);        
    })      
}

总结

本文主要讲述了两个关于图片优化的知识:不同格式图片的适用场景、图片懒加载。图片优化是性能优化的一大步,希望对你有所帮助。


前端性能优化系列:

(一):从TCP的三次握手讲起

(二):针对TCP传输过程中的堵塞

(三):HTTP协议的优化

(四):图片优化

(五):浏览器缓存策略

(六):浏览器是如何工作的?

(七):webpack性能优化