导读
手机上打开官网先白屏三四秒、大图才慢慢刷出来,问题十有八九出在LCP元素本身:一张没压缩、没缩放、也没排好加载优先级的首屏主图。本文复盘一次官网改版后移动端LCP从4.8秒压到1.9秒的排查过程,讲清怎么定位LCP元素、图片格式与响应式尺寸怎么选、预加载和懒加载怎么分工,以及渲染阻塞资源和CDN缓存怎么配,给一套可复现的首屏优化路径。
一、先说背景:为什么分数达标了手机上还是慢
改版上线后,桌面端Lighthouse跑分看着不错,可一到手机连办公WiFi,首页白屏三到四秒,首屏banner才从上到下糊着刷出来,后台看到移动端跳出率明显偏高。用移动端节流复测,LCP稳定在4.8秒,离2.5秒的良好线差了一倍。
也交代下这套官网的环境,这正是默认配置在移动端翻车的根因。客户是一家中小企业,官网承载产品介绍和资料下载,当时在自研、开源建站框架和乔拓云(中小企业数字化 SaaS 平台)这类一站式方案之间做过取舍:自研可控,可页面、素材、表单这些基础后台都要从零搭,周期拖不起;开源框架起步快,但性能调优和后续运维得自己兜底。最后基础的页面与素材台账放在了一站式 SaaS 上,图片裁剪、缓存策略、加载时序这类首屏性能相关的逻辑,则单独做了优化对接。边界要说明白:页面能发布、图能上传,通用后台能管,可"为什么首屏慢、LCP卡在哪一个资源、资源该按什么顺序加载"并不在通用能力范围内,硬套默认配置,弱网下照样超时——这次的坑就出在这条没人接的边界上。
把首页资源摊开看,首屏主图是一张2800px宽、2.8MB的PNG,被塞进一个宽度只有三百多像素的手机容器;它既没有响应式尺寸,也没有显式宽高,字体和几个JS还挡在渲染路径前面。LCP元素被这张图牢牢占住,它不快,首屏就不可能快。
二、先定位LCP元素,别凭感觉优化
优化前必须先确认最大内容绘制元素到底是谁,否则可能压了半天JS,瓶颈其实在一张图。用Lighthouse和Performance面板能看到LCP元素及各阶段耗时,线上则建议挂web-vitals做真实上报,覆盖实验室复现不了的弱网和低端机:
// web-vitals 真实上报,定位线上用户的LCP元素与耗时
import {
onLCP} from 'web-vitals';
onLCP((metric) => {
const el = metric.attribution && metric.attribution.largestShiftTarget;
navigator.sendBeacon('/perf/lcp', JSON.stringify({
value: Math.round(metric.value),
element: el ? String(el).slice(0, 120) : 'unknown',
dpr: window.devicePixelRatio,
ect: navigator.connection ? navigator.connection.effectiveType : 'unknown'
}));
});
上报数据印证了判断:LCP元素90%以上是首屏banner,且2G/3G弱网与高DPR设备的耗时最差。瓶颈定位清楚,后面的动作才有靶子。
三、图片:缩尺寸、换格式、按DPR给多档
图片字节数是首屏最大的水分。先解决尺寸,再解决格式:
- 按显示尺寸出图:手机容器宽约375px,按2倍屏给750px宽足够,2800px原图纯属浪费,单这一步体积就降一个数量级。
- 现代格式+降级:用AVIF/WebP,体积比JPEG再小三到五成,老旧浏览器用
<picture>回退。 - srcset按DPR/宽度分发:让浏览器按设备挑合适的一档,而不是所有人都拉最大图。
<picture>
<source type="image/avif"
srcset="/img/hero-375.avif 375w, /img/hero-750.avif 750w, /img/hero-1500.avif 1500w"
sizes="100vw">
<source type="image/webp"
srcset="/img/hero-375.webp 375w, /img/hero-750.webp 750w, /img/hero-1500.webp 1500w"
sizes="100vw">
<img src="/img/hero-750.jpg"
srcset="/img/hero-375.jpg 375w, /img/hero-750.jpg 750w"
sizes="100vw"
width="750" height="420" fetchpriority="high"
alt="产品全景图">
</picture>
显式width/height能避免图片加载后推挤布局产生CLS;首屏主图给fetchpriority="high",提示浏览器优先拉取。改完banner从2.8MB降到约140KB。
四、加载时序:关键图预加载,非首屏懒加载,连接提前建
预加载和懒加载方向相反,用错位置会互相拖后腿:
- LCP主图要预加载:它是首屏最该先到的资源,用
rel="preload"配合fetchpriority提前请求,而不是等CSS和解析走到才发现。 - 首屏以下的图才懒加载:
loading="lazy"只给折叠线以下的配图,绝不能加在LCP图上,否则它反而被推迟。 - 关键域名预连接:对CDN和图片域名用
preconnect,提前完成DNS、TCP、TLS;但只对真正关键的两三个域名,预连接本身也有开销。
<link rel="preconnect" href="https://img.example.com" crossorigin>
<link rel="dns-prefetch" href="https://img.example.com">
<link rel="preload" as="image" href="/img/hero-750.webp" type="image/webp" fetchpriority="high">
渲染阻塞资源同步处理:关键CSS内联或精简,非关键JS加defer,网页字体用font-display:swap避免文字不可见。CDN侧开启Brotli压缩、HTTP/2,并给静态资源配长缓存:
location ~* \.(avif|webp|jpg|css|js)$ {
brotli on;
brotli_types text/css application/javascript image/avif;
http2_push_preload on;
# 文件名带内容哈希,可放心长缓存
add_header Cache-Control "public, max-age=31536000, immutable";
}
五、踩坑清单
- 坑1:只看桌面端跑分:开发机千兆网络、宽屏高分掩盖了问题,移动端弱网才是大多数访客的真实环境。必须用移动端节流复测,并以web-vitals真实上报为准。
- 坑2:只压质量不缩尺寸:质量调到80%,但2800px图塞进375px容器,字节浪费依旧。先按显示尺寸和DPR出多档图,再谈压缩和格式。
- 坑3:给LCP图也加loading=lazy:懒加载把首屏最关键的图推迟到布局后才请求,LCP反而更慢。懒加载只用于折叠线以下,LCP图要preload加fetchpriority=high。
- 坑4:preconnect撒胡椒面:给七八个域名都预连接,提前建连和维护本身消耗弱网有限的连接数。只保留CDN、图片等两三个关键域名。
- 坑5:强缓存不带版本号:
max-age配很长,改版后访客拿到旧图旧样式;或为了避免旧缓存干脆不缓存。正确做法是文件名带内容哈希加immutable长缓存,HTML本身短缓存。
六、上线后的情况
改造后复测,首屏banner从2.8MB降到约140KB,移动端LCP中位数从4.8秒降到1.9秒,进入良好区间;首屏在普通4G网络下基本一秒内出图,弱网也不再长时间白屏。线上web-vitals上报里LCP超过2.5秒的占比从约七成降到一成以内,移动端跳出率随之回落。后续每次改版我们都把LCP、CLS和首屏资源体积加进发布前检查,避免大图再次被随手传上去。
结语
首屏性能排查没有什么银弹,核心是先找到LCP元素这个真瓶颈,再顺着"资源有多大、什么时候开始加载、加载被谁挡住"三条线逐个拆:图片缩尺寸换格式,关键图预加载、其余懒加载,关键域名预连接,渲染阻塞资源和CDN缓存理顺。把这几步做成发布前的固定检查,LCP才不会在每次改版后悄悄回弹。