从NextJS的封装的Image组件看如何优化图片加载

lxf2023-03-14 14:01:01

前言

如何对图片加载进行优化是前端经久不衰的话题,也是工程调优的一小部分,大部分业务场景下对图片元素的展示都是直接使用原生的img标签,另外对于有统一错误兜底图,或者预览大图需求的场景会使用组件库,例如阿里ant-design和字节arco-design中的Image组件,但两者的源码都不涉及对图片加载的优化,所以在实际的生产中,即便使用了组件库,在图片加载优化上还是有一定的挖掘空间的。

通常的策略

在图片加载优化这件事情上一般的策略是前端懒加载+后端或者中台提供的缩略图能力,放cdn还是放哪就不说了,而前端对于懒加载的策略一般是判断图片是否快要进入视窗,在到达一定的阈值的时候对图片资源进行预加载,其实在pc端上,这种调优已经可以满足需求了,除了电商等对性能要求很高的to C业务,这样的编码已经可以通过了,我先前也觉得差不多得了,直到我今天中午看到NextJS里的Image组件,才发现技术这东西,可以差不多得了,也可以更加的完备,于是花了一下午时间,除去中间的开会,整理了一下它优化的思路,分享出来。

预热知识

1. 图片加载对绘制帧率的影响

图片,特别是大图,高清图的加载一直是交互体验的影响大头,轻则卡顿,重则白屏,白屏就P0,那为什么图片的加载过程会导致卡顿呢?这里贴一片大佬写的文章,讲的很清楚

引用原话:“另外图片解码也发生在这个阶段(指光栅化),而图片解码也是光栅化耗时最多的一个环节,光栅化的耗时从几毫秒到几百毫秒都有可能(图片在第一次被光栅化时被解码,一直在可见区域内的图片不会被反复重解码)

但其实浏览器在图片的解码上是有暴露出api让开发人员自己选择是否优化的,也不能说优化,因为如果解码被异步执行,等完成后再统一绘制,这会付出图片延迟出现的代价,收益是动画的流畅。大佬的另一篇文章也讲 到这个问题:

引用原话:“实际上它会造成图片延迟显示的效果,浏览器很难确定这个效果是否是页端需要的,虽然大部分情况下图片延迟显示换取动画流畅度应该是可以接受的。”

至于怎么优化,暴露的是哪个api,下面源码的时候会讲到。

2. 一些指标

CLS:累计布局位移。是衡量用户视觉稳定性的一项重要的以用户为中心的度量标准,主要正比于页面元素布局的改变频率。
(rel:如果img标签没有设置宽高属性,浏览器无法预留空间,载入时将触发元素布局改变。)

LCP:最大内容绘制。性能标准组织认为,当页面最大块载入时,页面大概呈现完成,可以作为衡量用户体验的指标
(rel:大图通常会成为最大的那个内容)

以上的两个指标也是优化的目的。

完备点的优化策略

整理出策略之前最后捋一下我们到底要什么效果,或者说优化的上限在哪里。首先,只对进入视窗的图片进行加载,如果有缩略图的话,要加载缩略图,其次,对于视窗外的图片要尽可能快的加载,另外还要保证快速滚动时滚动动画要尽可能流畅,同时还要考虑LCP、CLS两个指标。

  1. 对于缩略图。因为img加载过程没有暴露出读流数据的api,所以并不能对文件流操作从而进行缩略,缩略图的实现只能是请求服务端的缩略图,这种对上传图片进行缩略的能力就交给中台或者后台实现了,这里不多赘述。NextJS中采用配置型loader实现不同云端缩略图请求的能力
const loaders = new Map<
  LoaderValue,
  (props: ImageLoaderPropsWithConfig) => string
>([
  ['default', defaultLoader],
  ['imgix', imgixLoader],
  ['cloudinary', cloudinaryLoader],
  ['akamai', akamaiLoader],
  ['custom', customLoader],
])

  1. 对于CLS,只要在图片显示之前知道图片的宽高,或者人为定义显示区域的宽高,这样无论图片宽高是多少,都会被限制在这个范围内,从而无法影响布局,NextJS Image的做法是在外层套一个span元素,在使用组件时传入宽高,再将span元素的宽高设置上去,撑开容器即可。
<span style={wrapperStyle}>
  {hasSizer ? (
    <span style={sizerStyle}>
      {sizerSvgUrl ? (
        <img
          style={{
            display: 'block',
            maxWidth: '100%',
            width: 'initial',
            height: 'initial',
            background: 'none',
            opacity: 1,
            border: 0,
            margin: 0,
            padding: 0,
          }}
          alt=""
          aria-hidden={true}
          src={sizerSvgUrl}
        />
      ) : null}
    </span>
  ) : null}
  <ImageElement {...imgElementArgs} />
</span>

  1. 对于分层次的加载。什么叫分层次的加载,就是视窗中的图片首先加载无可厚非,剩下的视窗外的,肯定是越接近视窗的先加载,但不排除距离较远元素优先级更高从而先加载的场景,那要怎么做呢,浏览器提供了相关的api

intersectionObserver

var intersectionObserver = new IntersectionObserver(function(entries) {
  if (entries[0].intersectionRatio <= 0) return;
  loadItems(10);
  console.log('Loaded new items');
});
intersectionObserver.observe(document.querySelector('.scrollerFooter'));

当被观察元素与祖先元素相交值到达指定阈值时触发回调。这样就可以在图片元素快到达视窗时设置其src,从而进行加载。

--

img loading属性

指定以何种策略加载图片

eager:立即加载图像,不管它是否在可视视口(visible viewport)之外(默认值)。

lazy:延迟加载图像,直到它和视口接近到一个计算得到的距离,由浏览器定义。

--

link rel=preload

<link rel='reload' href="image.src" as="image" />

设置rel=preload将使浏览器对资源进行优先加载


  1. 对于保证滚动时动画尽可能的流畅 通过对图片进行decode和append分离,在decode未完成之前,只渲染外层容器。
const p = 'decode' in img ? img.decode() : Promise.resolve();
p.catch(() => {}).then(() => { 
    // append Image to container
})

... 

<img  decode='async'/>

参考

【1】2021 年 Web 核心性能指标CLS解读
【2】页面加载性能之LCP
【3】Chrome 图片解码与 Image.decode API
【4】浏览器渲染流水线解析与网页动画性能优化