前端性能优化之图像优化

图像优化问题主要可以分为两方面:图像的选取和使用,图像的加载和显示。

图像基础

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

图像是否必需

图像资源优化的根本思想:压缩。无论是选取何种图像的文件格式,还是针对于同一种格式压缩至更小的尺寸,其本质都是用更小的资源开销来完成图像的传输和展示。
我们首先需要思考下要达到期望的信息传递效果,是否需要图像?这不仅是因为图像资源与网页上的其他资源(html/css/js等)相比有更大的字节开销,出于对节省资源的考虑,对用户注意力的珍惜也很重要,如果一个页面打开之后有很多图像,那么用户其实很难快速梳理出有效的信息,即便获取到了也会让用户觉得很累。一个低感官体验的网站,它的价值转化率不会很高。
当确定了图像的展示效果必须存在时,在前端实现上也并非一定就要用图像文件,还存在一些场景可以使用更高效的方式来实现所需的效果。

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

因此,我们在选择使用某种资源之前,如果期望达到更优的性能效果,则需要先思考这种选择是否必须。

矢量图和位图

当确定了图像是实现展示效果的最佳方式时,接下来就是选择合适的图像格式。图像文件可以分为两类:矢量图和位图。

  • 矢量图
    矢量图的优点时能够在任何缩放比例下呈现出细节同样清晰的展示效果。其缺点是对细节的展示效果不够丰富,对足够复杂的图像来说,比如要达到照片的效果,若通过svg进行矢量图绘制,则所得文件会大的离谱,但即便如此也很难达到照片的真实效果。目前几乎所有的浏览器都支持svg。
    svg标签所包括的部分就是该矢量图的全部内容,除了必要的绘制信息,可能还包括一些元数据,比如xml命名空间、图层以及注释信息。但这些信息对浏览器绘制一个svg来说不是必要的,所以在使用之前可通过工具去除这些元数据来达到压缩的目的。
  • 位图
    位图是通过对一个矩阵中的栅格进行编码来表示图像的,每个栅格只能编码表示一个特定的颜色,如果组成图像的栅格像素点越多且每个像素点所能表示的颜色范围越广,则位图图像的显示效果就会越逼真,当然图像文件也就越大。虽然位图没有像矢量图那种不受分辨率影响的优秀特性,但对于复杂的照片却能提供较为真实的细节体验。

分辨率

在前端开发过程中书写css时,经常会为图像设置显示所需的长宽像素值,但在不同的设备屏幕上,有时候相同的图像以及相同的设置,其渲染出来的图像会让人明显感觉出清晰度有差别。产生这个现象的原因涉及两种不同的分辨率:屏幕分辨率和图像分辨率。
图像分辨率展示的就是该图像文件所包含的真实像素值,比如一个200px200px的分辨率的图像文件,它就定义了长宽各200各像素点的信息。设备分辨率则是显示器屏幕所能显示的最大像素值,比如一台电脑的显示器分辨率为2560px1600px。更高的设备分辨率有助于显示更绚丽多彩的图像,这其实很适合矢量图的发挥,因为其不会失真。而对于位图来说,只有图像文件包含更多的像素信息时,才能更充分地利用屏幕分辨率。为了能在不同的分辨率下使项目中所包含的图像都能得到恰当的展示效果。可以利用picture标签和srcset属性提供图像的多个变体。

<img src='photo.jpg' srcset='[email protected],[email protected]' alt='photo'/>

除了ie和其他较低版本的浏览器不支持,目前主流的大部分浏览器都已支持img标签的srcset属性。在srcset属性中设置多种分辨率的图像文件以及使用条件,浏览器在请求之前便会先对此进行解析,只选择最合适的图像文件进行下载,如果浏览器不支持,务必在src属性中包含必要的默认图片。
使用picture标签则会在多图像文件选择时,获得更多的控制维度,比如屏幕方向、设备大小、屏幕分辨率等。

<picture>
<source media="(min-width:800px)" srcset="photo.jpg,[email protected]"/>
<source media="(min-width:450px)" srcset="photo.jpg,[email protected]"/>
<img src='photo.jpg'/>
picture>

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

无损和有损压缩

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

对于图像压缩,应该是使用有损压缩还是无损压缩,可以简单分为两步进行。

  1. 确定业务所需要展示图像的颜色阶数、图像显示的分辨率以及清晰程度,当锚定了这几个参数的基准后,如果获取的图像源文件的相应参数指标过高,便可适当进行有损压缩,通过降低源文件图像质量的方法来降低图像文件大小。
    如果业务所要求的图像质量较高,便可跳过有损压缩,直接进入第二步无损压缩。所以是否要进行有损压缩,其实是在理解了业务需求后的一个可选选项,而非必要的。
  2. 当确定了展示图像的质量后,便可利用无损压缩技术尽可能降低图像大小。无损压缩是应当完成的工作环节。因此最好能通过一套完整的工程方案,自动化执行来避免繁琐的人工重复工作。

CSS Sprite

雪碧图,通过将多张小图标拼接成一张大图,有效地减少http请求数量以达到加速显示内容的技术。

通常对于雪碧图的使用场景应当满足以下条件:首先这些图标不会随用户信息的变化而变化,它们属于网站通用的静态图标;同时单张图标体积尽量小,这样经过拼接后其性能的提升才会比较乐观;若加载量比较大则效果会更好。但是不建议将较大的图片拼接成雪碧图,因为大图拼接后的单个文件体积会非常大,这样占用网络带宽的增加与请求完成所耗费时间的延长,会完全淹没通过减少http请求次数所带来的性能提升。

雪碧图的使用方式十分简单:通过css的background-image属性引入雪碧图的url后,再使用background-position定位所需要的单个图标再雪碧图上的起始位置,配合width和height属性来锁定具体图标的尺寸。

.sprite-sheet{
   background-image:url(https://xxxxx);
   background-size: 24px 600px
}

.icon-1 .sprite-sheet{
   background-position: 0 0;
   height:24px;
   width:24px;
}

.icon-2 .sprite-sheet{
   background-position: 0 -24px;
   height:24px;
   width:24px;
}

使用雪碧图来提升小图标加载性能的历史由来已久。在http1.x环境下,它确实能够减少相应的http请求,但需要注意当部分图标变更时,会导致已经加载的雪碧图缓存失效。同时在http2中,最好的方式应该是加载单张图像文件,因为可以在一个http连接上发起多次请求,所以对于是否使用此方法,需要考虑具体的使用环境和网络设置。

图像格式选择建议

首先明确告诉读者:不存在适用于任何场景且性能最优的图像使用方式。所以作为开发者,想要网站性能在图像方面达到最优,如何根据业务场景选择合适的文件格式尤其重要,图像文件的使用策略如下图所示:
前端性能优化之图像优化_第1张图片
注意:使用webp格式的图像需要考虑到浏览器的兼容性。通常的处理思路分为两种:一种是在前端处理浏览器兼容性的判断,可以通过浏览器的全局属性window.navigator.userAgent获取版本信息,再根据兼容支持情况,选择是否请求webp图像格式的资源;也可以使用标签来选择显示的图像格式。

<picture>
  <source srcset="photo.webp" type="image/webp"/>
  <img src='photo.jpg'/>
picture>

除此之外,位图对于不同缩放比的响应式场景,建议提供多张不同尺寸的图像,让浏览器根据具体场景进行请求调用。

参考文章:https://blog.csdn.net/qq_42691298/article/details/128485051

Web字体

使用web字体有多种优点:增强网站的设计感、可读性,同时还能搜索和选取所表示的文本内容,且不受屏幕尺寸与分辨率的影响,能提供一致的视觉体验。除此之外,由于每个字型都是特定的矢量图标,所以可以将项目中用到的矢量图打包到一个web字体文件中使用,以节省对图标资源的http请求次数,这样做类似雪碧图优化目的。

  1. 字体的使用
    目前网络上常用的字体格式有:EOT、TTF、WOFF与WOFF2,由于存在兼容问题,并没有哪一种字体能够适用所有浏览器,所以在实际使用中,网站开发者会声明提供字体的多种文件格式,来达到一致性的体验效果。
    在web项目中,一般会先通过@font-face声明使用的字体系列:

    @font-face {
        font-family: 'myFont';
        src: url('./my-font.ttf') format('truetype');
        src: url('./my-font.woff') format('woff'),
             url('./my-font.woff2') format('woff2');
        font-weight: 600;
        font-style: normal;
    }
    
    .my-font {
        font-family: 'myFont';
        color: red;
    }
    
    

    在上述代码中通过src字段的属性值,可以指定字体资源的位置,并且该属性值还可以提供一个用逗号分隔的列表,列表中不同字体文件格式的资源顺序同样重要,浏览器将选取其所支持的第一个格式资源。如果希望较新的woff2格式被使用,则应当将woff2声明在woff之上。

  2. 子集内嵌
    如果将所有字型都打包成一个文件来请求使用,不免就会存在许多根本用不到的字型信息浪费带宽。相较于拉丁文字体而言,包含中文字符的字体文件的大小会格外突出。可以使用unicode-range属性定义所使用的字体子集。它支持三种形式:单一取值(如U+233)、范围取值(如U+233-2ff)、通配符范围(如U+2??),取值的含义是字体集文件中的代码索引点:

    @font-face {
        font-family: 'myFont';
        src: url('./my-font.ttf') format('truetype');
        src: url('./my-font.woff') format('woff'),
             url('./my-font.woff2') format('woff2');
        unicode-range:U+100-3ff,U+f??;
        font-weight: 600;
        font-style: normal;
    }
    

    通过使用子集内嵌,以及为字体的不同样式变体采用单独的文件,用户可以仅根据需要下载字体的子集,而不必强制他们下载可能永远都不会用到的字体子集。不过属性unicode-range也存在兼容问题,对于不支持的浏览器,可能需要手动处理字体文件。

  3. 字体文件预加载
    在默认情况下,构建渲染树之前会阻塞字体文件的请求,这将可能导致部分文本渲染延迟,对此可使用对字体资源进行加载。

<head>
<link rel="preload" href="xxxx" as ="font"/>
head>

link需要和@font-face对字体的定义一同使用,它只负责提示浏览器需要预加载给定的资源,而不指明如何使用。需要注意的是,这样做将会无条件向网络发出字体请求如果项目迭代将原本使用的字体文件修改或删除,也需要同步删除对字体预加载的设置。

参考文章:https://blog.csdn.net/weixin_46820017/article/details/116666903

注意display:none的使用

在使用位图时,经常会根据屏幕尺寸,权限控制等不同条件,响应式地处理资源的展示与隐藏。出于对性能的考虑,希望对于不展示的图像,尽量避免在首屏时进行资源请求加载。

<div style="display:none">
   <img src="img.png"/>
div>

根据html的解析顺序,img.png的图像文件会被请求。

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

css解析后发现父级使用了none,再去计算子级的样式就没有多大意义了,所以就不会去下载子级div的背景图片。

如果不清楚不同浏览器对display:none关于图像加载的控制,则可以通过开发者工具进行验证。
笔者这里推荐的做法是使用的方式进行响应式显示。

图像延迟加载

在首次打开网站时,应尽量只加载首屏内容所包含的资源,而首屏之外涉及的图片或视频,可以等到用户滚动视窗浏览时再去加载。

实现图片的延迟加载:传统方式

通过监听的方式,通过监听scroll事件与resize事件,并在事件的回调函数中去判断,需要进行延迟加载的图片是否进入视窗区域。

首先定义出将要实现延迟加载的标签结构:

<img class="lazy" alt="" src="xxxx" data-src="xxxx"/>
  • src属性,加载前的占位符图片,可用base64图片或低分辨率的图片。
  • data-src属性,通过该自定义属性保存图片真实的url外链。

对于只可上下滚动的页面,判断一个图片元素是否出现在屏幕视窗中的方法其实显而易见,即当元素上边缘距屏幕视窗顶部的top值小于整个视窗的高度window.innerHeight时,预加载的事件处理代码如下:

//在dom内容加载完毕后,执行延迟加载处理逻辑
document.addEventListener("DOMContentLoaded",function(){
    //获取所有需要延迟加载的图片
    let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
    //限制函数频繁被调用
    let active= false;
    const lazyLoad = function(){
         if(active==false){
            active = true;
            setTimeout(function(){
               lazyImages.forEach(function(lazyImage){
                    //判断图片是否出现在视窗中
                   if((lazyImage.getBoundingClientRect().top<=window.innerHeight&&lazyImage.getBoundingClientRect().bottom>=0)&&
                   getComputedStyle(lazyImage).display!=='none'){
                   //将真实的图片url赋值给src属性,发起请求加载资源
                   lazyImage.src = lazyImage.dataset.src;
                   lazyImage.classList.remove("lazy");
                   lazyImages = lazyImages.filter(function(image){
                       return image !== lazyImage;
                   })
                   //所有延迟加载图片加载完成后,移除事件触发处理函数
                   if(lazyImages.length===0){
                       document.removeEventListener("scroll",lazyLoad);
                       document.removeEventListener("resize",lazyLoad);
                       document.removeEventListener("orientationchange",lazyLoad);
                   }
    }
               });
               active=false;
            },200)
         }
    };
      document.addEventListener("scroll",lazyLoad);
      document.addEventListener("resize",lazyLoad);
      document.addEventListener("orientationchange",lazyLoad);
})

由于无法控制用户随心所欲地滑动鼠标滚轮,从而造成scroll事件被触发地过于频繁,导致过多的冗余计算影响性能。所以通过active标志位的方式进行限流。
即便如此也有潜在的性能问题,因为重复的setTimeout调用时浪费的,虽然进行了触发限制,但当文档滚动或窗口大小调整时,不论图片是否出现在视窗中,每200ms都会执行一次检查,并且跟踪尚未加载的图片数量,以及完全加载完后,取消绑定滚动事件的处理函数等操作都需要开发者来考虑。
如此看来,虽然传统的延迟加载方式具有良好的浏览器兼容性,但是实现起来比较琐碎。

实现图片的延迟加载:IntersectionObserver方式

现代浏览器大多支持了IntersectionObserver API,可以通过它来检查目标元素的可见性,这种方式的性能和效率都比较好。
IntersectionObserver:每当因页面滚动或窗口尺寸发生变化,使得目标元素与设备视窗或其他指定元素产生交集时,便会触发通过IntersectionObserver API配置的回调函数,在该函数中进行延迟加载的逻辑处理,会比传统方式显得更加简洁而高效。

//在dom内容加载完毕后,执行延迟加载处理逻辑
document.addEventListener("DOMContentLoaded",function(){
    //获取所有需要延迟加载的图片
    let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
    //判断浏览器兼容性
    if("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype){
       let lazyImageObserver = new IntersectionObserver(function (entries,observer){
           entries.forEach(function(entry){
               //判断图片是否出现在视窗中
               if(entry.isIntersecting){
                   let lazyImage = entry.target;
                   lazyImage.src = lazyImage.dataset.src;
                   lazyImage.classList.remove("lazy");
                   lazyImageObserver.unobserver(lazyImage);
               }
           })
       })
       lazyImages.forEach(function(lazyImage){
           lazyImageObserver.observer(lazyImage)
       })
    }
})

这种方式判断元素是否出现在视窗中更为简单直观,应在实际开发中尽量使用。但其问题是并非所有浏览器都能兼容。在将这种方式引入项目之前,应当确保已做到以下两点:

  1. 做好尽量完备浏览器兼容性检查,对于兼容IntersectionObserver API的浏览器,采取这种方式处理,而对于不兼容的浏览器,则使用传统的实现方式。
  2. 使用相应兼容的polyfill插件。

实现图片的延迟加载:CSS类名方式

这种方式通过css的background-image属性来加载图片,与判断标签的src属性是否有要请求图片的url不同,css中图片加载的行为建立在浏览器对文档分析基础之上。
具体来说,当dom树,cssom树以及渲染树生成后,浏览器会去检查css以何种方式应用于文档,再决定是否请求外部资源。如果浏览器确定涉及外部资源请求的css规则再当前文档中不存在,便不会去请求该资源。

<div class="wrap">
   <div class="lazy-background one"/>
   <div class="lazy-background two"/>
   <div class="lazy-background three"/>
div>

具体的实现方式是通过js来判断元素是否出现在视窗中,当在视窗中时,为其元素的class属性添加visible类名。而在css文件中,为同一类名元素定义出带.visible和不带.visible的两种包含background-image规则。
不带.visible的图片规则中的background-image属性可以是低分辨率的图片或base64图片,而带.visible的图片规则中的background-image属性为希望展示的真实图片的url。

//在dom内容加载完毕后,执行延迟加载处理逻辑
document.addEventListener("DOMContentLoaded",function(){
    //获取所有需要延迟加载的图片
    let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
    //判断浏览器兼容性
    if("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype){
       let lazyImageObserver = new IntersectionObserver(function (entries,observer){
           entries.forEach(function(entry){
               //判断图片是否出现在视窗中
               if(entry.isIntersecting){
                   entry.target.classList.add("visible")
                   lazyImageObserver.unobserver(entry.target);
               }
           })
       })
       lazyImages.forEach(function(lazyImage){
           lazyImageObserver.observer(lazyImage)
       })
    }
})

原生的延迟加载支持

除了上述通过开发者手动实现延迟加载逻辑的方式,从chrome75版本开始,已经可以通过