Google最近提出的Core Web Vitals正在成为衡量网页体验的新标准。我在几个项目中实践了这套指标体系,总结了一些实用的优化经验和踩坑记录,分享给大家。
先说说背景。做前端开发的同学都知道,网页性能优化是一个永恒的话题。从最早的YSlow规则,到后来的Lighthouse评分,再到现在的Core Web Vitals,性能衡量的标准一直在演变。Core Web Vitals是Google在2019年底提出的一套新的性能指标体系,它从三个维度来衡量网页的用户体验:加载性能、交互响应性和视觉稳定性。每个维度对应一个核心指标,三个指标合起来就是Core Web Vitals。
我所在的团队最近在做一个电商网站的性能优化,正好用Core Web Vitals作为衡量标准。优化前三个指标都不达标,优化后全部达到了Google推荐的"良好"标准。整个过程花了大概一个月,踩了不少坑,也积累了一些经验。今天把这些经验整理出来,希望对正在做性能优化的同学有帮助。
一、Core Web Vitals是什么
在说优化经验之前,先简单介绍一下Core Web Vitals的三个核心指标,方便还不了解的同学快速上手。
第一个指标是LCP(Largest Contentful Paint,最大内容绘制),衡量的是加载性能。它记录的是页面上最大的内容元素(通常是图片、视频或者大段文字)完成渲染的时间。Google的推荐标准是:LCP在2.5秒以内为"良好",2.5到4秒为"需要改进",超过4秒为"较差"。
为什么用LCP而不是传统的FCP(First Contentful Paint)或者Load事件呢?因为FCP只记录第一个内容像素绘制的时间,这个内容可能只是一个导航栏或者一个loading动画,用户真正关心的主要内容还没出来。Load事件则是所有资源都加载完成的时间,包括一些用户根本看不到的资源,比如第三方统计脚本、隐藏的图片等。LCP更准确地反映了用户感知到的页面加载速度,因为它关注的是页面上最大、最显眼的内容什么时候出来。
第二个指标是FID(First Input Delay,首次输入延迟),衡量的是交互响应性。它记录的是用户第一次和页面交互(比如点击按钮、点击链接)到浏览器真正开始处理这个交互的时间。Google的推荐标准是:FID在100毫秒以内为"良好",100到300毫秒为"需要改进",超过300毫秒为"较差"。
FID衡量的不是交互处理本身的时间,而是交互发生时浏览器有多忙。如果主线程正在执行一段很长的JavaScript代码,这时候用户点击了按钮,浏览器就需要等这段代码执行完才能响应点击,这个等待时间就是FID。所以FID高的根本原因是主线程被阻塞了,通常是因为有大量的JavaScript代码在页面加载初期执行。
第三个指标是CLS(Cumulative Layout Shift,累积布局偏移),衡量的是视觉稳定性。它记录的是页面在整个生命周期中发生的所有非预期的布局偏移的总和。比如页面加载到一半,上面突然插入了一个广告,把下面的内容都往下推了,这就是一次布局偏移。Google的推荐标准是:CLS在0.1以内为"良好",0.1到0.25为"需要改进",超过0.25为"较差"。
CLS是一个很容易被忽略但对用户体验影响很大的指标。相信大家都有过这样的经历:正在看一篇文章,页面突然跳了一下,你看到的内容被推走了;或者正要点击一个按钮,按钮突然移开了,你点到了别的东西。这些都是布局偏移导致的,非常影响体验。CLS就是用来量化这种影响的。
这三个指标分别从加载、交互、稳定性三个维度来衡量网页体验,覆盖了用户访问网页的主要场景。而且它们都是以用户为中心的指标,衡量的是用户真实感受到的体验,而不是实验室里的抽象数字。这也是Core Web Vitals比之前的性能指标更有价值的地方。
二、LCP优化经验
先说说LCP的优化。我们项目优化前LCP是4.2秒,优化后降到了1.8秒,效果比较明显。
LCP的优化核心是让页面上最大的内容元素尽快渲染出来。这个元素通常是首屏的主图、banner图,或者是大段的正文文字。优化的思路就是围绕这个元素来做文章。
第一个优化点是优化LCP元素本身的加载。如果LCP元素是一张图片,那就要确保这张图片尽可能快地加载出来。具体做法包括:
- 压缩图片。用WebP格式代替JPEG/PNG,同样的画质下体积能小30%到50%。我们项目里的主图原来都是JPEG,平均200KB左右,转成WebP之后平均只有80KB,加载速度快了很多。
- 响应式图片。用srcset属性根据不同的屏幕尺寸加载不同大小的图片,避免在手机上加载桌面端的大图。我们之前所有设备都加载同一张1920px宽的图,手机上其实只需要750px就够了,白白浪费了带宽。
- 预加载关键图片。对于首屏的主图,可以用link rel="preload"来告诉浏览器优先加载这张图片。这样浏览器在解析HTML的时候就会开始加载这张图,而不是等CSS解析完、渲染树构建完之后才开始加载。我们加了preload之后,主图的加载时间提前了将近一秒。
- 使用CDN。把静态资源放到CDN上,让用户从离自己最近的节点加载资源。这个是基础操作,但很多小项目还是会忽略。我们项目原来的图片都放在自己的服务器上,加载速度不稳定,搬到CDN之后,图片加载时间平均缩短了30%。
第二个优化点是减少关键渲染路径的阻塞。LCP元素的渲染不仅取决于它自身的加载速度,还取决于页面整体的渲染进度。如果CSS或者JavaScript阻塞了渲染,那LCP元素再快也显示不出来。
CSS方面:
- 内联首屏需要的CSS。把首屏渲染必需的CSS直接内联到HTML的style标签里,这样浏览器不需要等外部CSS文件加载完就能渲染首屏。注意只内联首屏需要的CSS,不要把所有CSS都内联,那样HTML文件会太大。
- 异步加载非关键CSS。对于首屏不需要的CSS(比如弹窗样式、底部区域的样式、第三方组件的样式),可以用media="print"然后onload切换的方式异步加载,这样不会阻塞首屏渲染。
- 避免使用@import。@import会导致CSS文件串行加载,增加渲染阻塞时间。用link标签代替@import,让CSS文件并行加载。
JavaScript方面:
- 延迟加载非关键JavaScript。对于不影响首屏渲染的JavaScript(比如统计脚本、评论插件、客服工具),加上defer或者async属性,让它们不阻塞页面渲染。defer会等HTML解析完之后再按顺序执行,async是加载完就执行,不保证顺序。根据脚本的依赖关系选择合适的方式。
- 代码分割。把JavaScript代码按路由或者功能拆分成多个小文件,首屏只加载当前页面需要的代码,其他的按需加载。我们项目用的是React,通过React.lazy和Suspense做了路由级别的代码分割,首屏的JavaScript体积从原来的800KB降到了200KB,解析和执行时间大大缩短。
- Tree shaking。确保构建工具开启了tree shaking,把没有用到的代码剔除掉。尤其是使用了大型UI库或者工具库的时候,tree shaking能剔除大量未使用的代码。我们项目里用了lodash,原来直接import _ from 'lodash',打包进去了整个库,后来改成了import { debounce } from 'lodash-es',只打包用到的函数,体积小了很多。
第三个优化点是服务端渲染或者预渲染。如果是纯客户端渲染的SPA(单页应用),首屏需要等JavaScript加载、解析、执行完之后才能渲染出内容,LCP通常会比较差。这时候可以考虑用服务端渲染(SSR)或者静态站点生成(SSG),让HTML在服务端就渲染好,浏览器拿到HTML就能直接显示内容,不需要等JavaScript。
我们项目原来就是纯客户端渲染的React应用,首屏白屏时间很长。后来改成了Next.js的SSR方案,首屏内容直接在HTML里,LCP立刻就好了很多。当然SSR也有代价,服务端的复杂度和成本会增加,需要根据项目的实际情况来选择。如果是内容不怎么变化的页面,用预渲染(构建时生成静态HTML)是更简单的方案。
三、FID优化经验
接下来说说FID的优化。我们项目优化前FID是280毫秒,优化后降到了60毫秒。
FID高的根本原因是主线程被阻塞,浏览器在用户交互的时候没空处理。所以FID的优化核心就是减少主线程的工作量,尤其是页面加载初期的工作量。
第一个优化点是减少JavaScript的体积和执行时间。这是最直接也是最有效的优化。
- 代码分割和按需加载,上面说LCP的时候已经提到了,这里不再重复。需要强调的是,不仅要按路由分割,还要按功能分割。比如一个页面里有一个很复杂的图表组件,但它在页面底部,用户不一定看得到,就可以等用户滚动到附近的时候再加载。
- 延迟执行非关键的JavaScript逻辑。比如页面初始化的时候要做很多事情:初始化统计、初始化客服、加载广告、初始化各种第三方库。这些事情很多都不是首屏必须的,可以推迟到requestIdleCallback里执行,或者等首屏渲染完之后再执行。我们把一堆初始化逻辑从页面加载初期移到了window.onload之后,主线程的阻塞时间立刻就降下来了。
- 优化长任务。JavaScript中的长任务(超过50毫秒的任务)会阻塞主线程,导致FID升高。用Chrome DevTools的Performance面板可以找到这些长任务,然后想办法拆分或者优化。拆分的方法包括:把大的循环拆成多个小的异步任务,用setTimeout或者requestAnimationFrame分片执行;把计算密集型的逻辑放到Web Worker里,不占用主线程。
第二个优化点是优化第三方脚本。第三方脚本是FID的重灾区,很多网站的FID高都是因为加载了太多第三方脚本。
- 评估每个第三方脚本的必要性。很多网站加了一堆第三方脚本:统计、分析、广告、客服、评论、社交分享、A/B测试、安全监测……每个脚本都要加载、解析、执行,加起来对性能的影响非常大。定期审计这些脚本,把没用的或者可以替代的删掉。我们项目审计了一遍,删掉了三个已经不用的统计脚本和一个重复的客服脚本,第三方脚本的体积减少了40%。
- 延迟加载第三方脚本。对于必须保留的第三方脚本,尽量延迟加载。比如统计脚本可以等页面加载完之后再加载,客服工具可以等用户点击了客服按钮之后再加载,广告脚本可以等广告区域滚动到视口附近的时候再加载。这样这些脚本就不会在页面加载初期占用主线程。
- 用iframe隔离第三方脚本。对于一些必须在页面加载初期就加载但又很耗性能的第三方脚本,可以考虑用iframe来加载。iframe有自己独立的线程,不会阻塞主页面的主线程。当然iframe也有一些限制,比如和主页面的通信比较麻烦,需要根据具体情况来选择。
第三个优化点是利用浏览器的缓存机制。如果用户是第二次访问网站,JavaScript和CSS文件应该从缓存里读取,不需要重新下载和解析。这样能大大减少主线程的工作量。
- 设置合理的缓存策略。对于不怎么变化的静态资源(比如打包后的JavaScript和CSS),设置长期的Cache-Control,比如max-age=31536000,同时在文件名里加上hash值,文件内容变了文件名就变,用户就能拿到最新版本。这样用户第二次访问的时候,这些文件直接从缓存里读,几乎不花时间。
- 用好Service Worker。Service Worker可以做更精细的缓存控制,比如缓存API响应、缓存页面、离线访问等。对于重复访问的用户,Service Worker能显著提升加载速度和响应性。我们项目加了Service Worker之后,重复访问用户的FID又降了20毫秒左右。
四、CLS优化经验
最后说说CLS的优化。我们项目优化前CLS是0.32,优化后降到了0.05,这个指标的优化效果最明显,因为原来的问题确实比较多。
CLS的优化核心是避免页面在加载过程中发生非预期的布局变化。常见的导致CLS的原因和对应的优化方法如下。
第一个原因是图片和视频没有指定尺寸。这是最常见的原因。如果img标签没有写width和height,浏览器在图片加载完之前不知道它占多大空间,等图片加载完之后,就会把周围的内容推开,造成布局偏移。
解决方法很简单:给所有的img和video标签加上width和height属性,告诉浏览器这个元素的尺寸,这样浏览器在加载图片之前就能预留好空间。现在的浏览器还支持aspect-ratio CSS属性,可以只指定宽高比,不需要写具体的像素值,更灵活。
我们项目里原来很多图片都没有写尺寸,加上之后CLS立刻降了一大截。尤其是首屏的banner图,原来加载的时候会把下面的内容往下推一大截,加了尺寸之后就不会了。
第二个原因是广告、嵌入内容和iframe没有预留空间。广告是CLS的另一个重灾区。很多网站的广告是异步加载的,广告位在广告加载完之前是没有高度的,等广告加载出来之后,突然撑开一块空间,把下面的内容都推走了。
解决方法是给广告位预留固定的高度。在广告加载之前,就用CSS给广告容器设置一个固定的高度或者最小高度,这样广告加载出来之后不会改变页面布局。如果广告的尺寸不固定,可以设置一个最常见的尺寸作为默认值,然后用CSS的object-fit来适配不同尺寸的广告。
我们项目的侧边栏广告原来就是异步加载的,每次广告出来都会把下面的推荐内容推下去。给广告容器设了固定高度之后,这个问题就解决了。
第三个原因是动态插入内容。有时候页面会在加载过程中动态插入一些内容,比如顶部的通知栏、促销横幅、Cookie提示、A/B测试的变体内容等。这些内容如果是在页面渲染完之后才插入的,就会把已经渲染好的内容往下推,造成布局偏移。
解决方法是尽量在初始的HTML里就包含这些内容,或者给这些内容预留空间。如果实在需要动态插入,尽量在页面渲染的早期插入,不要等用户已经开始滚动页面了才插入。对于Cookie提示这种固定在页面底部或者顶部的浮层,用position: fixed或者position: absolute,不占用文档流空间,这样就不会造成布局偏移。
我们项目原来有一个顶部的促销横幅,是在页面加载完之后用JavaScript插入的,每次插入都会把整个页面往下推。后来把它放到了初始的HTML里,用CSS控制显示和隐藏,就没有这个问题了。
第四个原因是Web字体导致的FOIT/FOUT。Web字体加载的时候,如果处理不好,会导致文字先以系统字体显示,等Web字体加载完之后再切换成Web字体,两种字体的宽度不一样,切换的时候就会造成布局偏移。
解决方法包括:
- 用font-display: swap,让浏览器先用系统字体显示文字,等Web字体加载完之后再替换,这样至少不会出现文字不可见的情况。
- 用size-adjust、ascent-override、descent-override等CSS属性来调整系统字体的尺寸,让它和Web字体的尺寸尽量接近,这样切换的时候布局变化就小了。
- 预加载关键的Web字体,用link rel="preload"让浏览器优先加载字体文件,减少字体切换的时间窗口。
- 如果不是必须用Web字体,考虑用系统字体栈,完全避免这个问题。
我们项目原来用了一款Google Fonts的字体,每次加载都会有轻微的布局偏移。后来给字体加了preload,并且调整了font-display,这个问题就基本解决了。
第五个原因是在DOM更新之前没有等待动画完成。比如展开一个折叠面板,或者切换一个标签页,如果动画和DOM更新的时机没配合好,就可能出现布局跳动。这种情况比较少见,但如果有复杂的交互动画,需要注意。
解决方法是用transform和opacity来做动画,不要用width、height、margin、padding这些会影响布局的属性。因为transform和opacity不触发重排,只触发合成,不会影响其他元素的位置,也就不会造成布局偏移。
五、如何监控和衡量
优化做完了,怎么知道效果好不好呢?需要建立监控和衡量机制。
第一个是实验室数据。用Lighthouse或者Chrome DevTools的Performance面板来跑性能测试,看三个指标的数值。实验室数据的好处是环境可控,可以反复测试,适合在开发过程中做对比。但实验室数据是在模拟的网络和设备条件下测的,和真实用户的体验可能有差距。
第二个是真实用户数据(RUM,Real User Monitoring)。在页面里埋点,收集真实用户的性能数据。Google提供了web-vitals这个JavaScript库,可以很方便地收集三个指标的数据,然后发送到自己的监控后台。真实用户数据的好处是反映了真实的用户体验,包括不同的网络条件、设备性能、用户行为等。我们项目用web-vitals + 自建监控后台,每天看三个指标的P75值(75分位值),因为Google的标准也是基于P75来衡量的。
第三个是Chrome User Experience Report(CrUX)。这是Google收集的真实用户体验数据,包含了大量网站的Core Web Vitals数据。可以在PageSpeed Insights里输入你的网站URL,查看CrUX中的数据。如果你的网站流量足够大,CrUX里会有你的数据,这也是Google搜索排名中使用的数据来源。
建议三个数据源结合起来看:开发过程中用Lighthouse做快速验证,上线后用RUM做持续监控,定期看CrUX的数据和Google搜索的表现。这样就能全面掌握网站的性能状况。
六、一些踩坑记录
最后分享几个我们在优化过程中踩过的坑,希望大家不要重蹈覆辙。
第一个坑是LCP元素识别错误。有时候你以为LCP元素是首屏的主图,但实际上因为主图加载太慢,浏览器在主图出来之前先渲染了一段大文字,这段文字就成了LCP元素。这时候你优化主图可能效果不大,因为LCP算的是文字的渲染时间。解决方法是用Chrome DevTools的Performance面板,在Timing里找到LCP的标记,看它对应的是哪个元素,然后针对性地优化。
第二个坑是preload用得太多。preload虽然能提前加载关键资源,但如果preload的资源太多,反而会抢占其他关键资源的带宽,适得其反。我们一开始给好几张图都加了preload,结果首屏的主图反而变慢了,因为带宽被分散了。后来只保留了最关键的一张主图的preload,效果就好了。preload要克制,只给真正关键的资源用。
第三个坑是FID在实验室里测不准。FID衡量的是用户第一次交互的延迟,但在实验室测试的时候,如果没有模拟用户交互,FID可能测不出来或者测出来的值很低。Lighthouse里的FID是通过模拟主线程阻塞来估算的,不一定准确。所以FID更依赖真实用户数据,不要只看实验室里的数值。我们一开始在Lighthouse里看到FID达标了,就以为没问题了,结果上线后真实用户的FID还是很高,因为实验室没有模拟真实的用户交互场景。
第四个坑是CLS的累积性。CLS是累积的,不是只看首屏。用户在页面上的所有操作过程中发生的布局偏移都会被算进去。我们一开始只优化了首屏的CLS,以为达标了,结果用户往下滚动的时候,懒加载的图片没有预留尺寸,又造成了布局偏移,整体CLS还是很高。后来把整个页面所有图片都加了尺寸,包括懒加载的,才彻底解决。
第五个坑是优化了性能但影响了功能。比如为了减少JavaScript体积,把一些看似不重要的功能删了,结果用户反馈说需要那个功能;为了延迟加载第三方脚本,把统计脚本也延迟了,结果统计数据不准了。性能优化不能以牺牲功能为代价,要在性能和功能之间找平衡。每次优化都要充分测试,确保功能正常。
七、写在最后
Core Web Vitals是一套以用户为中心的性能指标体系,它让我们更关注用户真实感受到的体验,而不是一些抽象的技术数字。LCP、FID、CLS三个指标分别覆盖了加载、交互、稳定性三个维度,把它们都优化好了,用户体验自然就上去了。
而且Google已经宣布Core Web Vitals会成为搜索排名的因素之一,所以优化这三个指标不仅能提升用户体验,还能对SEO有帮助。对于依赖搜索引擎流量的网站来说,这更是必须做的事情。
性能优化是一个持续的过程,不是一次优化完就完事了。随着功能的迭代、内容的更新,性能可能会退化,所以需要建立持续的监控机制,定期检查指标,发现问题及时优化。把性能当成产品质量的一部分来对待,而不是一个一次性的任务。
希望我的这些经验能对大家有帮助。如果有什么问题或者不同的看法,欢迎在评论区交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录