3

前端性能精进之浏览器(三)——图像 - 咖啡机(K.F.J)

 1 year ago
source link: https://www.cnblogs.com/strick/p/17080155.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前端性能精进之浏览器(三)——图像 - 咖啡机(K.F.J) - 博客园

每天进步一点点 研磨生活的香甜

  HTTP Archive 在 2022 年关于多媒体的报告中指出,目前大概有 99.9% 的网站或多或少都会包含点图像。

  并且高达 70% 的移动页面和 80% 的桌面页面的 LCP 指标会受图像的影响。

  通过这些数据可知,图像在网页中占据着举足轻重的地位,优化图像,对于网页性能可以达到立竿见影的效果。

  优化的核心是控制图像的尺寸,提前、延迟或减少图像的请求,以及降低对核心 Web 指标的影响。

  本文所用的示例代码已上传至 Github

  以我目前的公司为例,活动页中图像的请求数占比最高可达 64%。

  若不做优化处理,那么将直接拉长页面的白屏时间,体验将会及其糟糕。

1)懒加载

  懒加载就是延迟请求的时机,在触发某个特定条件后,再请求。

  常用的条件是当图像出现在屏幕内时,触发请求。

  当页面很长时,并不需要在页面首屏加载时就请求所有图像,而是滚动到图像所在位置后,再将图像显示,如下图所示。

211606-20230131180950525-1277384919.jpg

  目前有 3 种方式来实现懒加载。那么在正式讲解之前,需要先了解一下视口的概念。

  视口(viewport)就是下图中的灰色部分,也就是文档内容的可视区域,图中用粗线框住的是浏览器的外壳部分(如标签页、书签栏、调试工具等)。

  

211606-20230131181005018-645872771.png

  先来讲解第 1 种懒加载:传统的 JavaScript 实现,原理就是计算图像顶部到视口顶部的距离,包括页面隐藏部分。

  若此距离大于等于当前滚动条的位置(即可视区域),那么就可以认为满足条件,需要显示图像。

  假设滚动条是在body中,那么当前可视区域的范围如下所示。

const viewTop = window.pageYOffset;
const viewBottom = window.innerHeight + viewTop;

  window.pageYOffset 表示视口上边的距离,如果没有出现垂直方向的滚动条,那么对应属性的值为 0。window.innerHeight 表示视口的高度。

  而图像到视口顶部的距离可以通过 getBoundingClientRect() 的 DOMRect.top 属性得到,如下所示。

const nodeTop = node.getBoundingClientRect().top + viewTop;

  完整的代码如下所示, blank.gif 是一张 1*1 的空白占位图,data-src 是真实的图像地址,scroll 是滚动条事件。

<img src="blank.gif" data-src="cover.jpg" width="100%" />
<img src="blank.gif" data-src="cover.jpg" width="100%" />
<img src="blank.gif" data-src="cover.jpg" width="100%" />
<script>
  window.addEventListener('scroll', () => {
    const viewTop = window.pageYOffset;
    const viewBottom = window.innerHeight + viewTop;
    // 查询包含 data-src 自定义属性的 img
    document.querySelectorAll('img[data-src]').forEach(node => {
      const nodeTop = node.getBoundingClientRect().top + viewTop;
      if (nodeTop >= viewTop && nodeTop <= viewBottom) {
        node.src = node.dataset.src;
      }
    });
  });
</script>

  当前只是为了做演示,兼容性和性能方面并未做深入优化,可以参考市面上成熟的懒加载库,例如 Layzr.js

  接下来讲解第 2 种通过 Intersection Observer 实现懒加载。

  Intersection Observer 提供了一种异步的对目标元素与视口是否相交的检测方法,即检测目标元素是否在可视区域中。

  示例代码如下,省去了位置计算的逻辑,通过 isIntersecting 属性就能判断元素的可见性。

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    // 不在可视区域内就返回
    if (!entry.isIntersecting) return;
    const img = entry.target;
    img.src = img.dataset.src;
    observer.unobserve(img); // 取消监控
  });
});
document.querySelectorAll("img[data-src]").forEach((node) => {
  observer.observe(node);
});

  最后讲解第 3 种懒加载方式:img 元素的 loading 属性。该属性会指示浏览器当图像不在可视区域时的加载方式。

  这种方式相比较前两者,最为简洁,不需要编写额外的脚本,示例代码如下所示。

<img src="cover.jpg" loading="lazy" width="300" height="400"/>

  2022 年有 91.47% 的浏览器已支持 loading 属性,并且大概有 24% 的网站在使用它,相比去年有 1.4 倍的增长。

  但是其延迟加载的规则,即图像与视口顶部的距离是多少时开始加载,全部由浏览器自行定义。

  注意,在 Chrome 中调试发现,页面打开有两三屏的图像就开始请求了,开始滚动后,剩余的图像就开始陆续请求。

  并不是说到了图像的可视区域后,才开始请求。

2)预加载

  预加载和懒加载正好相反,它是在图像还没出现在可视区域时提前请求。

  之前做过一次公司招聘的活动页,其中会涉及到好多动画和很多图像,并且需要在手机中翻页浏览。

  一开始将所有图像地址直接写在页面中,在测试环境就发现打开非常慢(如下图所示),过了几十秒后才会出现 Loading 过渡动画。

  211606-20230131181417164-190079542.jpg

  于是就对其进行优化,将图像的默认请求替换成一张空白图(与之前的懒加载一样),然后在脚本执行后再将首屏替换成真实地址。

  示例代码如下,初始化 Image 实例,在触发 load 事件时执行自定义回调,可以是替换 img 元素地址。

function loadImage(url, callback) {
  const img = new Image();
  img.src = url;
  img.onload = function () {
    //将回调函数的 this 替换为Image对象
    callback.call(img);
  };
}
document.querySelectorAll("img[data-src]").forEach((node) => {
  loadImage(node.dataset.src, function () {
    node.src = this.src;
  });
});

  在翻页时,可以将后面几页的图像进行预加载,然后在翻到那页后,不会出现等待图像加载的情况,并且动画就会更加丝滑和顺畅。

3)Data URI

  img 元素的 src 属性或 CSS 的 background-image 属性的值都可以是一个经过 Base64 编码后 Data URI,这样能减少额外的HTTP请求。

  Data URI 由协议、MIME 类型(可选)、Base64 编码设定(可选)和内容组成,格式如下:

data:[<mime type>][;base64],<data>

  在实际使用中的代码片段如下:

data:image/png;base64,/9j/4AAQSkZJRgAB...

  Base64 会以每 6 个比特为一个单元,对应某个字符,如果要编码的字节数不能被 3 整除,就用 0 在末尾补足。

  例如编码 PW,最后得到的值是 UFc=,计算过程如下图所示。

  

211606-20230131181531514-156921846.png

  虽然使用 Data URI 减少了一次 HTTP 请求,但它会让嵌入的文档体积膨胀四分之三,影响浏览器渲染。

  并且还会降低 Gzip 的压缩效率,破坏资源的缓存。

  若要使用,需要权衡利弊,尽量考虑小尺寸和低更新频率的图像。

  大多数页面至少有一张超过 100 KB 的图像,而在页面尺寸排行中,前 10% 的页面至少有一张接近 1 MB 或更大的图像。

  因此,压缩或降低图像的大小,可以显著地提升页面性能。

1)压缩

  图像压缩分为有损和无损。

  前者会改变图像本身,减少信息量,降低图像质量,文件无法还原,但是压缩效率会比较高。

  后者会优化数据存储方式,利用算法描述重复信息,文件可以还原,但是压缩效率比有损低。

  在线压缩网站 TinyPNG 采用智能有损压缩技术对图像进行处理,在我实际使用时,发现最高可压缩 70% 以上的大小。

  原理就是通过合并图中相似的颜色,将 24 位的 PNG 图像压缩成小得多的 8 位色值的图像,并且去掉了不必要的元数据。

  经过压缩后的图像,人的肉眼并不会看出与原图明显的差异。

  若是要用代码对图像进行压缩,可以采用三种触发时机。

  第一种是在图像上传到服务器后,通过成熟的第三方 Node 库(例如 imageminnode-ffmpeg 等)进行压缩处理。

  第二种是在访问图像地址时,带上各类参数,动态的对图像进行压缩或裁剪,例如 cover.png?w/100,按比例裁剪成 100 的宽度。

  第三种是在构建过程中对图像进行压缩,压缩后再上传到服务器中,例如 webpack 的 ImageMinimizerPlugin 插件等。

  目前市面上流行的 CDN 服务都应该会提供此类功能。

2)WebP

  WebP 是由 Google 提供的一种图像格式,支持无损和有损两种压缩。

  官方资料表明,WebP 比 PNG 格式的图像小 26%,比 JPEG 格式的图像小 25~34% 。

  在可以接受有损压缩的情况下,有损 WebP 也支持透明度,并且其文件大小通常比 PNG 小 3 倍。

  虽然表现如此优秀,但是 2022 年,WebP 格式的使用率只占 8.9%,如下图所示,GIF、JPEG 和 PNG 仍然是主流。

  

211606-20230131181729866-32800468.png

  阻碍其推广的一大问题是兼容性,好在目前 iOS 14 以上已经支持 WebP,不考虑 IE 的话,主流的浏览器都已支持 WebP 格式。

3)响应式

  响应式是指根据屏幕尺寸、像素密度或其它设备特性,动态的请求最符合场景的图像。

  像素密度(PPI)就是每英寸像素,计量设备屏幕的精细程度,值越高图像越精细,常见的屏幕有 Retina、XHDPI 等。

  接下来用一个例子来演示不同尺寸的屏幕显示不同的图像,首先为 img 元素声明 srcset 属性。

  用逗号分隔多个描述字符串,每一段包含图像地址和宽度或像素密度描述符,注意,此处的宽度是图像的原始宽度。

  然后再声明 sizes 属性,其值就是媒体查询的条件和图像的显示宽度,最后一条描述可以省略条件,如下所示。

<img srcset="cover-small.jpg 375w, cover.jpg 2449w" 
    sizes="(max-width: 375px) 375px, 800px" 
    src="cover.jpg" />

  cover-small.jpg 的原始宽度是 375px,cover.jpg 的原始宽度是 2449px。

  当设备最大宽度是 375 时,将图像宽度设为 375px,在 srcset 中锁定最接近的那张图像的描述。

  800px 是默认的图像宽度,当无法满足条件时,就采用这个值。

  如果在做媒体查询时不清楚各类屏幕尺寸的阈值,那么可以参考 Bootstrap 的 Containers

  注意,若在 srcset 声明的是像素密度,那么就不需要再额外声明 sizes 属性了。

  在 2022 年,srcset 属性的使用占比在 34%,size 属性的使用占比在 13%~19%。

  如果要同时适配特定的屏幕尺寸和像素密度,那么可以通过 picture 元素实现响应式。

  下面是一个示例,在 source 元素中,media 是媒体查询条件,srcset 的功能和 img 元素中的相同。

<picture>
  <source media="(max-width: 375px)" srcset="cover-small.jpg, cover-2x.jpg 2x" />
  <img src="cover.jpg" />
</picture>

  当都不符合条件时,就会采用默认的 img 元素。在 2022 年,picture 元素的使用占比是 7.7%。

  除了响应式图像,picture 元素还可以用来选择不同格式的图像,如下所示。

  当浏览器支持 WebP 时,就加载这种格式的图像,否则就加载后面的默认图像。

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

三、其他优化

  除了上述两类比较大的优化之外,还有一些其他的细碎优化,在此节会列举几个。

  例如对图像一个比较简单而有益的优化是预设宽高,提前占位,就能避免影响 CLS 的计算。

1)延迟解码

  图像解码是光栅化过程中一个比较耗时的步骤,当图像越大时,解码时间就越长。

  那么非合成动画(即非 CSS3 动画)就有可能因主线程被阻塞而卡顿。值得一提的是,CSS3 动画运行在合成线程中,所以不会受其影响。

  HTMLImageElement.decode() 方法可以确保图像解码后,再将图像添加到 DOM 中,如下所示。

const img = new Image();
img.src = "cover.jpg";
img.decode().then(() => {
  document.body.appendChild(img);
});

2)失败处理

  在图像请求失败时,对页面的交互并不会造成影响,但是图像会裂开,在视觉体验上比较糟糕。

  为 img 元素注册 error 事件,就能在错误时做纠正处理。

document.querySelector("img").addEventListener("error", function () {
  this.src = "../assets/img/cover-small.jpg";
});

  不过,若要想知道究竟是什么原因的错误,目前还无法做到。

  本文对图像的优化进行了系统性的梳理,首先是对请求做优化。

  为了更科学的对图像进行请求,列出了懒加载、预加载和 Data URI 三种优化方法。

  然后对尺寸做优化,讲解了压缩细节,WebP 格式的特点,以及响应式处理的妙用。

  最后再介绍了几个同样也能优化图像的方法,包括占位、延迟解码和失败处理。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK