ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

LCP优化:为什么压图没用?真正的瓶颈可能是一段文本

LCP优化:为什么压图没用?真正的瓶颈可能是一段文本 1. 先搞清楚LCP到底在量什么先说个我自己的真实经历。上个月优化一个内容站运营天天抱怨首屏慢我一看性能报告首屏那个Banner图片已经压到40KBWebP格式还加了懒加载按理说不该拖后腿了但实测LCP照样稳定在4秒左右。当时我就觉得不对劲打开DevTools的Performance面板一查结果大跌眼镜浏览器标记的最大渲染元素根本不是那张Banner而是Banner下面一段被折叠在首屏之外的文章摘要文本。很多人跟我一开始一样以为LCP就是首屏最大的那块图但实际上LCP的全称是Largest Contentful Paint它衡量的不是视觉上最显眼的元素而是在首次加载过程中渲染面积最大的那个内容元素——这个元素可以是图片但也完全可能是文本块、视频封面甚至是一个带背景色的容器。一旦你一开始就锁定了错误的目标后面再怎么压图、换格式、加CDN都是在给一个不是瓶颈的资源做优化LCP自然纹丝不动。1.1 LCP不是首屏最大图片那么简单LCP的候选元素类型其实很有限规范里只认这么几类img标签、svg里的image、video的poster封面、通过url()加载的背景图以及包含文本节点的块级元素。很多开发者只看图片这一项这是最大的认知盲区。文本节点虽然没有图片那么起眼但一段字号不小、占满容器宽度的标题或摘要它在屏幕上占据的像素面积可能远超一张被压缩过的Banner图。我举个例子你就明白了。假设首屏是640px宽的手机屏幕顶部Banner图片实际渲染尺寸是640x200px面积是128000平方像素而紧跟着Banner的是一条文章标题字号20px行距1.5占据640x40px的容器虽然文字本身只涂满了一部分像素但浏览器在计算LCP候选的时候算的是元素边界盒和其在视口内的可见区域尺寸够大、在首屏内完整可见的标题文本很可能就会盖过那张图片。这里要提醒一个关键细节LCP的判定和元素的内容完成加载时间有关。图片需要等资源下载完、解码完成才算渲染而文本只要DOM解析到、样式计算完成、布局确认哪怕字体还在加载浏览器也可能会用后备字体把它画出来计入LCP。所以很多场景下文本节点反而比图片更容易抢先成为LCP元素而且它出现得早数值看起来会更快但也正因为它出现得早一旦它前面有任何阻塞LCP就会被卡住。1.2 为什么看起来最大不等于LCP元素视觉上的最大和浏览器判定出来的最大经常不是一回事。这里面有几个原因。第一个是可见性问题。LCP规范里要求候选元素必须在视口内可见如果一张图片虽然尺寸巨大但大部分区域被折叠在首屏之下或者被其他元素遮住那浏览器计算的时候只算它在本视口内的可见部分。反过来有些元素在首屏内占的面积不大但它的渲染面积计算方式会把它撑得很大。比如说一个设置了透明背景、内部只有一个居中文本的整屏容器它的边界盒就是整屏哪怕你肉眼看到的只有中间几个字浏览器也会认为这个元素占了整个屏幕它的LCP时间等于里面文本首次绘制的时间。第二个是元素的嵌套关系。如果父容器和子元素同时都是候选元素LCP算法会取两者里面积更大的那一个但并不是说父容器一定赢。实际表现是一个包含图片和文本的卡片组件如果卡片本身没有自己的背景、边框它的渲染面积和文本子元素接近积分上可能不分胜负但一旦卡片的背景色填充了整个可视区那这个卡片本身就会成为霸主级候选元素所有的加载时间都被算到了它头上。第三个是隐藏内容的坑。我见过一个项目首屏有个轮播组件第二张、第三张图都通过CSS隐藏了但它们的img依然在DOM里浏览器在计算候选时会把隐藏元素排除。可问题在于如果这个隐藏动作是异步的比如轮播初始化之前所有图都可见初始化之后才隐藏那在某个时间点上它可能已经被计入LCP候选了。这类闪一下造成的指标污染比单纯的大图更难查。还有一个容易忽略的是content-visibility: auto。这个属性可以让视口外的元素跳过渲染按理说对性能友好但如果你把它用在了LCP候选元素附近浏览器在判定可见区域时可能会产生预料之外的结果——如果元素的可见状态判定被延后LCP的计时也会被拖慢。不是不能用而是用的时候要明确知道这个元素到底会不会进入候选集。2. 我踩的坑LCP元素识别错在哪说实话最初我的排查方向完全错了。我把所有注意力都放在Banner图上觉得只要图片够小、加载够快LCP就一定能降下来。于是我先做了一堆标准动作把Banner从PNG转成WebP用preload提前加载给img加fetchpriorityhigh再上一套智能裁剪把体积从120KB压到40KB。结果跑分一看LCP从4.6秒变到4.3秒几乎没变化。这个时候我才意识到问题不在图片资源本身。后来我用Performance面板录制了一次加载过程在Timeline里找到标着LCP标记的那一条点开详情才看清楚浏览器视作LCP元素的是Banner下面那段摘要文字的父容器。那一刻我整个人是有点懵的但回头想想又觉得完全合理——那段摘要文字区域在手机上确实占了很大一块而且因为字体加载和CSS渲染的阻塞它的首次绘制时间被拖到了4秒之后。2.1 背景图的坑CSS background-image不在LCP候选里先说说我在这轮排查里发现的第一个坑CSSbackground-image虽然可以出现在LCP候选里但前提是它的加载时机能被浏览器正确感知。你可能会想既然规范里写了背景图可以作为候选那我把Banner改成背景图不也一样吗问题在于background-image的URL是写在CSS里的浏览器的预加载扫描器在解析HTML时看不到它只有在CSS下载并解析完成后才发起请求天然就比img标签晚一步。而且背景图作为LCP候选浏览器计算的时间点是图像数据可用的那一刻不是背景绘制完成的时刻。如果你的背景图是一张很大的精灵图或者通过CSS渐变加半透明叠加出的视觉效果浏览器在判定面积和绘制时间时可能和你预期的不一致。我踩过的具体案例是把一个首屏主视觉从img改成CSS背景图后LCP直接上涨了300毫秒左右——不是图片变慢了而是发现资源和请求它的时机整体后移了。所以如果你有LCP压力的场景首屏关键图尽量用img标签不要用CSS背景图。这不是说背景图不能用而是你要明确知道它在加载链路里处于劣势。如果实在要背景图建议把它换成img并用CSS做覆盖布局或者至少用link relpreload把图片提前拉起来把这个劣势补回来。2.2 文本节点的存在感被严重低估很多前端对文本就是LCP候选这件事没有概念。我自己以前也没怎么重视直到这次调优才明白文本在LCP算法里的地位比想象中高。原因是文本元素一旦进入渲染管道只要字体或后备字体可用就会立刻绘制并计入LCP时间。你可以把它理解为图片需要网络传输解码两段等待而文本只需要DOM ready样式计算布局所以它天然是首屏渲染里最早出现的候选元素。但也正因为这样文本容易抢跑成为LCP元素。如果你的首屏里有一个大标题它的面积足够大那么LCP的计时就绑定到这个标题的首次绘制上和下面的图片无关。这时候你再去优化图片对LCP根本毫无帮助。你需要做的是让那个标题文本尽可能早地渲染出来或者从布局上调整把LCP候选让给一个加载路径更可控的资源。这里有个很现实的优化矛盾为了视觉一致性使用自定义字体但Web字体加载会导致文本在字体就绪前不可见FOIT有些浏览器会等待最多3秒才显示后备字体。这段等待时间如果恰好发生在LCP候选文本上LCP就会变得奇慢无比。我在调优中就遇到过一次——标题文本用了思源黑体字体文件2MB低网速下加载太久文本一直没显示LCP硬生生被拖到4秒。后面改成font-display: swap再对标题单独用font-display: block做有意的短暂等待才把这部分时间控制住。2.3 视口外的元素和transform的影响还有一个特别隐蔽的坑LCP候选元素的面积计算不是以元素的原始大小为准而是以它在视口内的可见区域为准。如果一个元素通过transform: translateY()移到了屏幕外哪怕它的边界盒巨大它的可见面积也会变小甚至归零。这看起来像是帮我减负但实际可能引发反效果。我调试一个营销页时发现首屏有个从底部滑入的动画卡片动画初始状态把卡片定位在视口下方等首屏绘制完成后再滑上来。结果Chromium在记录LCP候选时把卡片的可见面积算成了很小因为初始状态在视口外于是LCP候选就落到了另一张加载更慢的图片头上。为了让动画顺滑这个卡片又是用JS驱动的导致LCP计算和用户实际看到的最大元素完全错位。最终我把动画改成CSSanimation让浏览器能在渲染前就知道最终位置候选判定才恢复正常。这里给一个通用的操作建议如果首屏有入场动画、位移动画尤其是有transform和opacity变化的元素最好在requestAnimationFrame或字体加载完成后再触发避免它们干扰LCP候选的判定。实在不行可以在上线后用DevTools的LCP回放功能逐帧看一下确认实际标记的元素是不是你期望的那一个。3. 怎么定位真正的LCP元素定位LCP元素这件事说难也不难关键是方法要对。我的经验是分三步走先用Performance面板看整体瀑布再用LCP Breakdown看时间分段最后用PerformanceObserver在真实业务环境里持续监控。普通场景下做到前两步就够了但如果是大流量站点第三步能帮你在回归上线后第一时间发现LCP漂移。3.1 Performance面板的实际用法Chrome DevTools的Performance面板是最直接的工具。操作路径打开要测的页面打开DevTools切到Performance点击录制然后刷新页面等页面完全加载后停止录制。在生成的Timeline里找到标记为LCP的紫色条不同版本可能颜色不同点击它右侧的摘要面板会显示这个LCP元素的具体信息包括元素选择器、渲染时间、加载涉及的请求链接。有一点要注意Performance面板不是在所有情况下都能精准标记LCP元素。有些版本里你需要勾选Web Vitals复选框才能看到LCP标记还有如果你在录制过程中切换了Tab或者页面有大量跨域iframe标记可能丢失。碰到这种情况我建议用下面要说的LCP Breakdown工具它在定位元素方面更明确。另外强烈建议你同时打开Network面板按时间排序找到LCP元素对应的网络请求看看它是什么时候开始加载的、有没有被延迟。很多时候LCP元素本身加载不慢但被前面的某个大JS阻塞了。这种情况下Performance面板里能很直观地看到LCP条和某个Script条的时间重叠。3.2 LCP Breakdown和Web Vitals扩展LCP Breakdown是PageSpeed Insights里的一项诊断数据它把LCP时间拆成四段TTFB服务器响应时间、资源加载延迟Resource Load Delay、资源加载时间Resource Load Time、元素渲染延迟Element Render Delay。这对我们定位问题特别有意义。我给你翻译一下TTFB高说明服务端或网络链路有问题可能是后端接口慢、CDN回源慢也可能是首字节前的重定向太多资源加载延迟高说明浏览器明明已经发现了资源但迟迟没有发起请求常见原因是预加载扫描器没有扫到、被其他关键请求排队了资源加载时间长那就是资源本身太大或者带宽受限需要压缩体积或换CDN节点元素渲染延迟高则是资源下载完成了但主线程还在忙没法执行布局和绘制典型的元凶是大JS、长任务和CSS解析。Web Vitals扩展是另一个好用的工具它会在页面上直接覆盖显示当前的LCP、CLS、INP等指标点击LCP指标还能弹出一个气泡告诉你具体的LCP元素长什么样。我用它做快速验证改完代码后刷新一下一眼就能看到LCP有没有变化、元素有没有切换。这个反馈速度比Performance面板快得多适合日常开发迭代时用。3.3 用PerformanceObserver自己在业务里埋点如果你要做系统性的监控而非一次性调优建议在代码里接入PerformanceObserver监听LCP元素并上报。代码不复杂下面是一个可以直接用的最小示例。// main.js new PerformanceObserver((entryList) { const entries entryList.getEntries(); const lastEntry entries[entries.length - 1]; if (lastEntry lastEntry.element) { const element lastEntry.element; const tagName element.tagName; const className typeof element.className string ? element.className : String(element.className); const src element.currentSrc || element.src || ; const size { width: Math.round(lastEntry.size / 1000), height: Math.round(lastEntry.size / 10) }; console.log([LCP Debug], { time: lastEntry.startTime, tagName, className, src, renderTime: lastEntry.renderTime || lastEntry.loadTime }); } }).observe({ type: largest-contentful-paint, buffered: true });这段代码的核心价值在于它能在生产环境捕获到真实的LCP元素而不依赖本地模拟。我建议把上报数据里加上entry.startTime和lastEntry.element的标签信息方便在前端监控平台上一眼看出LCP元素是否有变化。我自己就遇到过这样的情况A/B测试改了首屏布局结果LCP元素从图片切成了文本而这个信息只有通过埋点才能第一时间拿到。这里再说一个埋点时的注意点largest-contentful-paint这个Observer可以在同一次加载里触发多次事件因为LCP候选元素在加载过程中会不断变化。取最后一个条目才是最终的LCP元素但同时把历史条目的变化记录下来能让你直观看到候选元素在时间线上是怎么漂移的这对分析问题很有帮助。4. 对症下药按真实LCP元素做优化找到真正的LCP元素之后优化方向就清晰了。我这里给大家一个优先级思路先把LCP元素的资源加载链路打最短再优化主线程的渲染阻塞最后才是压缩资源体积本身。很多人一上来就压图效果有限原因就在这里。4.1 给真正的LCP资源提优先级先做资源预加载和优先级提升。如果LCP元素是图片用fetchpriorityhigh告诉浏览器这个资源很重要同时也可以配合preconnect提前建连。如果是文本元素那就得从CSS和字体加载入手。我整理了一个针对图片LCP的推荐配置组合优化项推荐做法注意点资源加载link relpreload asimage hrefbanner.webp fetchpriorityhigh只在LCP元素上使用别给所有图片加连接预建link relpreconnect hrefhttps://cdn.example.com和图片的CDN域名保持一致图片格式WebP/AVIF根据浏览器能力降级下一步可以加srcset做分辨率适配懒加载LCP图片不要加loadinglazy其他首屏以下图片可以加解码方式给img加decodingasync避免阻塞主线程渲染这套组合看上去简单但能解决大部分图片型LCP的延迟问题。特别是preload加fetchpriorityhigh它能让浏览器在HTML解析早期就发现并请求这张图而不是等到CSS和JS解析完才开始。对于在HTML里靠后位置出现的首屏图这个优化尤其明显。4.2 减少渲染阻塞如果LCP元素是文本而且DOM位置靠前那核心优化点就是让主线程尽快完成布局和绘制。这里常见的阻塞源有三个同步脚本、未拆包的CSS、以及耗时的长任务。先说同步脚本。放在head里的同步script会阻塞HTML解析如果它执行时间超过几百毫秒LCP必然被拖慢。解决办法是把脚本加defer或async让脚本下载和HTML解析并行。有一个注意点是async脚本执行顺序不固定如果你的脚本之间有依赖关系慎用async用defer更安全。再说CSS。CSS是渲染阻塞资源head里的CSS文件过大会推迟首次渲染。但这个也不能一味砍因为CSS本身是必要的。我的做法是把首屏关键CSS内联进HTML非关键CSS异步加载。具体用mediaprint加onload切换的方式实现CSS异步加载或者用relpreload加onload把CSS当资源预加载加载完成后再注册为样式。后者更现代但在老浏览器上有兼容问题需要加一层兜底。还有长任务。就算脚本和CSS都优化了如果主线程被一堆第三方的初始化逻辑占满LCP照样上不去。最简单的排查方法是打开Performance面板看有没有超过50ms的长任务黄色长条然后把它们拆分成多个小任务或者延迟到requestIdleCallback里执行。这里有一个很实际的技巧第三方脚本比如数据统计、客服浮窗能用async就用async能不阻塞首屏就晚点加载很多页面的LCP就是被这些隐形杀手拖垮的。4.3 骨架屏和文本预绘制的取舍现在很多站点喜欢用骨架屏提升首屏体验但骨架屏和LCP之间有可能存在冲突。原因是骨架屏通常是一个个占位块这些占位块本身也可能成为LCP候选。骨架屏渲染得越早LCP的计时可能从真实内容出现变成了骨架块出现从指标层面看是变快了但用户实际感知的内容加载并没有变快这就造成指标好看体验没变的假象。我不否定骨架屏的价值但建议做骨架屏的时候留意一点不要让骨架屏的占位面积比真实内容还大。常见的问题是骨架屏的灰色背景块用了大面积的容器盒子导致它成为LCP候选。解决办法是给骨架屏的占位元素设置content-visibility: auto或visibility: hidden注意不是display: none因为display: none的资源加载可能受影响让它们不被LCP算法计入候选。如果LCP元素是文本另一个有意思的优化点是尽早暴露文本。不要让文本等字体加载完成才渲染设置font-display: swap就是告诉浏览器先用后备字体画出来Web字体加载好了再替换。这样LCP时间能提前一大截。代价是有FOUT无样式文本闪烁问题但用FOUT换LCP在很多业务场景下是值得的。5. 常见问题与排查技巧这部分我把自己调优过程中踩过的坑、以及同行问得比较多的问题整理成清单方便大家按图索骥。很多看起来神秘的现象排查到最后往往就是某个小细节。5.1 为什么图片加载很快LCP还是很慢这是最常被问到的问题。如果你在Network面板里看到图片请求只花了100毫秒就完成了但LCP还是3秒那大概率是LCP元素压根不是这张图片。我用Performance面板验证过很多次都是这个原因。还有一种情况是图片确实慢了但它慢不是因为在传输而是在排队。浏览器对同域的并发连接数有限制如果首屏有几十个小图片请求把连接占满了LCP图片虽然优先级高也得排队等前面的连接释放。解决办法是质数筛选优先保障LCP资源减少首屏小图片数量合并图片或转成雪碧图给CDN域名做分片增加并发通道。另外loadinglazy加在首屏图片上是一个经典的自杀式优化。浏览器要在布局完成后才能判断图片是否在视口内首屏图片一旦用懒加载它的加载时机至少要晚一个布局周期LCP直接被拖慢几百毫秒。注意检查你全站的图片首屏内一张都不要加懒加载。5.2 字体FOIT对LCP的影响字体的坑在前面几次提到过这里单独展开说。CSS标准里font-display有auto、block、swap、fallback、optional五个值很多前端就用了默认的auto以为无所谓但auto在Chrome里的表现接近block也就是说字体加载期间文本会隐藏最多等3秒才显示后备字体。这个隐藏期如果覆盖了LCP文本的渲染时间LCP会直接飙升。我自己遇到过最典型的情况是页面首屏文本用了全局定义的font-face字体源指向了一个慢速存储桶导致文本在近2秒内不可见而LCP文本又恰好是这个标题。最后我把font-display改成swapLCP从3.2秒降到了1.6秒。视觉上确实有字体闪烁但用户能立即看到文本内容体验好很多。如果你对字体切换前后的字形差异很敏感也可以使用font-display: fallback它只给一个极短的阻塞窗口约100ms如果字体没加载完就先用后备字体当字体加载完成后仅在原始文本尚未完全渲染时替换。这个方法适合UI要求较高的场景。5.3 移动端和弱网下的LCPPC上的LCP优化方案放到真实移动网络环境下经常要大打折扣。原因很简单移动端网络带宽小、延迟高、设备CPU弱TCP和TLS握手过程变慢图片解码耗时也更长。我建议移动端调优时把Performance面板的CPU降速设为4倍网络设为Slow 4G模拟出接近真实用户的环境再测。移动端还有一个特别容易忽略的因素是运营商代理和Service Worker。有些网络环境会缓存或拦截某些请求导致浏览器拿到的下载速度和预期完全不同。Service Worker则会在离线优先策略下改变资源的加载路径如果你的站点用Workbox之类的库做了运行时缓存一定要检查Service Worker的缓存策略是否给LCP资源开了先网络后缓存的路径否则第一次访问的LCP会被拖慢。对于移动端弱网我额外建议做分级资源方案为弱网用户准备更小的图片、更精简的CSS甚至直接砍掉非关键模块。你可以通过navigator.connection.effectiveType判断当前的网络类型动态下发资源版本。这个方法在核心业务屏上效果非常显著但要注意做好降级避免因为判断错误导致页面加载异常。最后再分享一个我最近用得很顺手的小技巧给LCP元素加一个独立的id然后在上报LCP的埋点里把元素的id一起打出来。这样不管业务怎么改版只要LCP元素没变你的监控看板就是稳定的万一LCP元素因为某些操作漂移了埋点数据里能立刻看到候选元素从hero-img变成了article-title这时候你就知道优化方向该切换了。说实话LCP优化这个事百分之八十的功夫不在拖拽压缩和安装插件上而在于搞清楚你优化的到底是不是那个制约全局的元素。方向对了哪怕只是给图片加一条preload都能砍掉一秒钟方向错了压到40KB也好上全套CDN也罢LCP该4秒还是4秒。希望这篇分享能帮你节省几个通宵排查的时间实实在在把首屏速度提上去。
返回列表