Skip to content

27 图片:调整策略,优化图片加载问题

你好,我是三桥。

上节课,我们学习了如何衡量LCP指标。通过比较平均值、最小值、最大值和百分位,我们可以判断问题的严重程度。

那判断之后,怎么才能优化LCP指标呢?这节课,我们就一起来解决这个问题。

不过在深入讨论优化LCP之前,我们首先需要理解浏览器是如何标记LCP元素和时间的。

LCP元素标记的原理

放开FCP和TTFB的问题不谈,FCP和LCP之间的时间主要用于加载前端资源,例如JavaScript、CSS和图片。此外还包括了渲染过程,这个过程包括了HTML和CSS解析、构建渲染树、布局和绘制等多个步骤,实际上过程很复杂。

尤其是SPA单页面项目,浏览器绘制页面的效果会更加明显。浏览器为了能够分析出视口中最大内容绘制的元素,提供了一套判断机制。

标记流程

浏览器记录LCP的流程主要包括这么几个步骤。

首先,浏览器在完成第一帧的绘制后开始记录LCP。所谓“第一帧”,通常是在FCP之后,FMP之前。FMP指的是浏览器首次绘制页面上有意义的内容的时间点,即用户认为页面开始有用内容的时间。

其次,当第一帧绘制完后,浏览器会创建一个类型为 largest-contentful-paintPerformanceEntry 对象,并标记当前的最大内容元素。

如果网页中有文本和图片,那浏览器通常会先显示文本。此时, PerformanceEntry 对象会标记最大的文本元素,例如 <p><h1> 元素。

随后,浏览器在绘制更多帧的过程中,如果最大的内容发生改变,就会再创建一个新的 PerformanceEntry 对象,同时更新最新的LCP值。如果此时是图片,那么就会标记为 <img> 元素。

最后,当浏览器视口内容完全加载完后,LCP就会停止记录,并以最后一次标记作为LCP的指标值。

在特殊情况下,如果视口内容尚未加载完成,用户就开始与网页进行交互(如点按、滚动等),浏览器就会停止记录 PerformanceEntry 对象,因为用户的交互通常会改变显示的内容。

图片

如何确定LCP元素?

根据LCP指标的规定,目前最大内容的元素类型有四种。

  1. 图片元素。 <img> 是LCP中最常见的元素,也最常影响范围最大的元素。另外还包括SVG中的 <image> 元素。
  2. <video> 视频元素。浏览器会根据 <video> 元素定义的 poster 属性值或第一帧视频画面作为最大内容。
  3. 使用CSS的 url() 函数的图片背景样式元素。
  4. 包含文本节点的块级元素。

但是,最大内容的元素是有前提条件的。

首先,元素内容必须在视口可见。如果在加载过程中被移出视口,或者尺寸大小超出视口,就不符合LCP标记条件了。

其次,LCP标记的是视口内最大的内容块,即使最后加载完成的是图片,如果图片尺寸小于文本块,那么LCP标记的仍然是文本块。

不过在一些情况下,页面的最大内容元素并非重要元素。产品通常关心的是重要元素,而非最大内容元素。所以这时我们不能只关注LCP标记的元素而忽略其它内容。

尤其是在移动设备中,需要全面分析视口内所有的文本和图片元素,包括它们的加载速度和渲染时间线。

缩短FCP与LCP的链路距离

我们的目标是优化LCP,缩短FCP与LCP之间的链路距离。

我会用谷歌浏览器开发者工具快速找出LCP的最大内容元素,并根据加载时间线找出优化方案。我们将在不缓存资源和模拟4G网络用户的条件下,尝试减少FCP和LCP之间的距离。

性能报告

我们以豌豆思维学科官网为例,观察一下官网加载后的性能报告,特别是LCP指标的位置和时间,如下图所示。

图片

这个网站的技术栈是Next.js,通过Node端获取后台配置数据,然后服务器端生成页面,并在浏览器直接渲染内容。

我们在前一节课已经讨论了使用服务器端渲染(SSR)生成页面的情况,其中FCP和LCP指标几乎相同。那么,为什么在这里会有比较长的时间差?

我们通过时间线来清晰地理解上面的报告图。

图片

实际上,浏览器在渲染页面时,请求了一张400*4666的长图片。在图片请求过程中,其它的元素和内容都先加载完成了,只有这张图片加载时间最长,导致视口页面出现一片空白,如上图左侧的屏幕截图。

当图片加载完并渲染完成后,黑色区域的内容会自定移出屏幕视口,这时就会出现短暂的内容闪烁效果。

Lighthouse报告

我们看看Lighthouse的分析报告里指出的LCP存在的问题点。

图片

诊断结果显示,有10个问题点对LCP产生了一定影响,其中包括一些我们在学习TTFB和FCP时已经讨论过的部分。

我们重点关注下图中四个红色框部分,报告展开后的详细内容如下图所示。

图片

首先,确定最大内容渲染时间元素。

图1显示,这张图片的加载延迟和加载时间占比超过80%。这说明加载LCP最大内容元素的时间过长。

其次,适当调整图片大小。

图2显示,最大内容元素的图片大小为835KB,对于4G网络而言,加载时间会更长。因此,报告建议提供适当大小的图片,节省移动网络流量,减少加载时间。

同时,报告还建议使用Next.js的 next/image 组件加载图片,由Next.js内部决定如何加载图片以及使用何种大小。

第三,使用新一代格式提供图片。

图3显示,加载的图片使用了png格式,这里建议使用WebP和AVIF格式,因为这些格式的压缩效果优于PNG,所以下载速度更快。

和第二点一样,报告还建议使用 next/image 组件方式加载图片。

最后,预加载LCP元素图片。

图4提示,如果在视口区域动态添加内容,比如图片,应考虑预加载图片以缩短LCP时间,预计可节省170毫秒的时间。

优化过程

分析完报告,我们动手优化。报告明确指出,长图片是影响LCP的主要原因。要解决这个问题,需要采取三个步骤。

第一步,增加预加载策略。

这个策略的目的是尽早下载最大内容元素图片(如上述的长图片),使其请求下载的时间尽可能提前。下图是优化前的加载时序图。

图片

时序图显示,长图片的加载开始时刻在FCP之后,请求图片总共花了2秒,最终在3.5秒时记录了LCP。

主要原因是,最大内容元素是一张大图,占据了90%的视口内容区域。当显示图片时,屏幕会出现闪烁,这是需要优化的用户体验问题。

为了实现预加载,我们使用 <link> 元素来指定预加载的具体资源,具体的代码实现如下。

import Head from 'next/head'

export default function PublicHeader() {
  return (
    <Head>
      <!-- 省略部分代码 -->
      <link rel="preload" href="https://hll-coin.61info.cn/prod/bizcenter-goods-admin/64c215abb102a90001ede256.png" as="image" />
    </Head>
  )
}

在上述代码中,需要同时使用 rel="preload" 和 as="image" ,这样浏览器才能知道需要提前加载资源。

增加了预加载代码后,我们重新测试LCP的长图片,观察优化后的结果如下图所示。

图片

时序图显示,长图片下载的时间已经提前到了FP之前。

这意味着,浏览器在接收到渲染树后,发现含有 rel="preload"link 标签,就会立即请求该图片,与HTML主文档的请求同时进行。

遗憾的是,这张图片尺寸大小为835KB,在4G网络下需要较长的下载时间。即使使用了预加载,下载时间仍远超过FCP时间,所以LCP时间并未有明显改善。

要解决这个问题,我们需要采用第二步的优化策略。

第二步,采用 next/image 组件。

在这一步中,我们将这张图片的加载方式更改为使用 next/image 组件。参考代码如下。

<Image
  src="http://hll-coin.61info.cn/prod/bizcenter-goods-admin/64c215abb102a90001ede256.png"
  width={400}
  height={4667}
  quality={80}
  priority={true}
  alt='LCP of Image'
/>

经过改造后,我们观察到LCP并未有明显变化,性能并未因为我们使用了组件加载而提高。这是因为根本原因在于图片加载速度过慢。下面是它的时序图。

图片

那就只能进行 第三步,将大图切割为小图。

要模拟FCP内容,我们将使用一张完全覆盖视口的图片,大小约400k。继续采用预加载和 z next/image 组件,以观察LCP时间的变化。

结果显示,由于图片是通过4G网络下载,尽管图片大小减少到400k并且下载时间减少,但下载时间依然超过1秒。时序图的效果如下。

图片

接下来我们继续优化,目前最大的问题依然是图片尺寸大小。

这一次,我们再把已切割的图片再次切割成四张小图,分别用预加载和 next/image 组件两种不同组合方式观察LCP时间的变化。以下是两种组合的时序图。

图片

通过时序图可以看出,尽管在图1中我们使用预加载4张大约100多KB左右的小图,但图片并未经过压缩,其下载时间仍然较长,最后一张图的加载完成时间在2.4秒。

图1还有一个特点是,在FCP时刻开始,视口逐渐渲染了已下载的图片,直到最后一张图下载完成,LCP标记才结束。

再来观察图2,我们采用 Image 组件加载4张小图,并设置 Image 组件的加载优先级。结果很明显,LCP和FCP同时出现在了一个时刻上,这说明优化方法取得了明显效果。

最终,我们通过一个小视频来回顾缩短FCP和LCP时间的优化效果。

优化小结

我们总结出LCP图片元素的六个优化建议。

  1. 避免在浏览器视口使用大尺寸图片,因为大尺寸图片会降低低速网络用户的体验。
  2. 尽可能减少浏览器视口的最大内容块区域,以减少下载和渲染时间。
  3. 如果使用服务器渲染模式,最好使用框架提供的图片组件和优先级属性,方便框架提前预判视口内容元素并提前下载重要图片。
  4. 如果使用了 Image 组件,并设定了优先级模式,就没有必要设置预链接。
  5. 如果视口区域中的图片内容是通过富文本加载的,那么建议提取内容中的图片,并提前设置预加载图片。
  6. 关于图片尺寸问题,如果图片存储在云端,那么可以根据不同的设备设置图片的大小、格式、尺寸等参数,减少图片的加载时间。

总的来说,在复杂的前端项目中,优化LCP并不容易,建议在优化时同时考虑多种方案,尤其要想清楚是FCP时间优先还是LCP时间优先。

总结

总结一下,这节课我们重点讲了两部分,LCP元素的标记原理,以及优化FCP和LCP之间的链路时间。

浏览器视口的页面内容是用户最直观的画面。浏览器标记LCP元素的原理是通过逐帧渲染来判断最大的面积块,然后选出最终元素。如果画面频繁出现闪烁、渲染、图片移出视口等情况,用户体验就会受影响。

我们需要从多个角度和层次来优化,包括减小图片尺寸,使用新一代图片格式,预加载LCP图片,图片组件等方法。结合这些方法,可以有效地缩短FCP与LCP之间的链路时间,提升用户体验。

要完全优化LCP,除了优化TTFB和FCP之外,还要不断地尝试,通过多次试验找出有效的解决方案。虽然每个项目都是不同的,但优化的思路是一致的,总能找出最佳的解决方案。

你可能已经注意到,视口中元素的闪烁效果实际上是CLS指标的布局偏移。下节课我们就来学习如何分析和避免CLS的布局偏移。

思考题

现在,我给你布置一道思考题。

挑选一个前端项目,找出LCP最大内容元素,并尝试缩短FCP和LCP之间的链路时间。

欢迎你在留言区和我交流。如果觉得有所收获,也可以把课程分享给更多的朋友一起学习。我们下节课见!