186-6908-0178
当前位置:首页 >前端技术
5 次优化让首屏快 3.6 秒,只有 1 次是改代码
2026-09-23 59次浏览

5.1 秒到 1.5 秒,中间隔着 5 次改动,只有 1 次碰了业务代码。

上周我做了个受控实验:搭一个埋满典型性能坑的活动落地页,用 Lighthouse 移动端模拟(Slow 4G + 4 倍 CPU 降速),每改一处测一次。所有数字都是实测,不是估算。

先看总账:

步骤

改动

LCP

TBT

性能分






原始页

—

5.1s

0ms

77

第 1 次

JPG 换 WebP

3.7s

0ms

86

第 2 次

加 preload

2.6s

0ms

93

第 3 次

script 加 defer

2.6s

990ms

75 ↓

第 4 次

长任务切片(唯一改代码)

2.2s

0ms

99

第 5 次

按视口出图

1.5s

0ms

100

注意第 3 次那一行:改完分数反而从 93 跌到 75。不是测错了,是这次实验里最有意思的发现,后面细说。

实验设定

页面是一个大会落地页:一张 1920 宽的 JPG 头图(408KB,background-image 挂首屏),一段 30KB 的同步 JS(轮播初始化里藏着 6000 万次循环的长任务),加常规的标题、议程卡片、CTA。没有框架和第三方脚本,变量干净。

真机上跑,绝对值不会这么整齐,但每步改动的因果方向是可靠的——生产页上你永远说不清那 800 毫秒是谁贡献的,受控实验能。

Lighthouse 的分数看着抽象,LCP 分解倒是很诚实。原始页的 5.1 秒拆出来:

LCP 组成

耗时

意思




发现延迟

2.34s

浏览器很晚才知道要下这张图

加载时长

2.74s

图太大,下载要 2.7 秒

渲染延迟

0.01s

到位即渲染

后面 5 次优化,本质上就是把这四段挨个打掉。

第 1 次:JPG 换 WebP,5.1s → 3.7s

最没技术含量的一步,性价比却最高:

bash代码解读复制代码cwebp -q 90 hero.jpg -o hero.webp

408KB 变 141KB,画质肉眼无差,LCP 加载时长从 2.74s 掉到 1.33s。

1.6Mbps 带宽下,408KB 意味着光下载就 2 秒多。首屏最大的元素通常是头图,LCP 的主角就是它——头图胖,LCP 必胖。动手调代码之前,先把资源过一遍秤,图片超重是营销页最常见也最便宜的病。

第 2 次:一行 preload,3.7s → 2.6s

换完 WebP,加载时长降了,发现延迟还是 2.34s:浏览器要等 CSS 下载、同步 JS 执行完、body 开始解析,才从 style 里读到头图 URL,然后才开始下载这张最重要的图。

所以让头图插队:

html代码解读复制代码<link rel="preload" as="image" href="hero.webp" fetchpriority="high">

放在 head 里,浏览器解析到这行就发请求,不等 CSS 也不等 JS。发现延迟从 2.34s 砍到 0.58s。fetchpriority="high" 是让浏览器把带宽优先分给头图——没有它,preload 只是提前排队;CSS 里引用的背景图是全页面藏得最深的关键资源,这招对它尤其有效。

第 3 次:加 defer,LCP 没动,分数跌 18

前两次干掉的是图片的问题,第 3 次轮到那个 30KB 的同步 JS:

html代码解读复制代码<script src="app.js" defer></script>

直觉上这步稳赚:脚本不再阻塞解析。实测结果却是 LCP 纹丝不动(2.6s),TBT 从 0 涨到 990ms,性能分 93 → 75。

原因不玄:那个 6000 万次循环的长任务本来就在,只是位置变了。同步脚本时代,它执行时页面还没画出第一帧,TBT 根本看不见它(TBT 只统计首帧之后的阻塞);defer 之后首帧提前画出来了,长任务落到首帧后面执行,TBT 当场抓个正着。

阻塞用户的事件从头到尾就那么多,defer 没有消灭它,只是把它从「看不见的窗口」挪进了「看得见的窗口」。这步的教训:优化前先想清楚指标在度量哪一段,别只盯一个指标——LCP 好看了 TBT 爆了,体验是继续烂着的。

第 4 次:切片,唯一一次改代码,TBT 990ms → 0

长任务的处理思路一句话:任何超过 50ms 的任务都该拆开。

原始版本是同步跑完 6000 万次循环(模拟真实项目里「预计算 + 建索引」的重活):

javascript代码解读复制代码function initCarousel() {   var acc = 0;   // 大循环跑到底,主线程卡死
~1.5 秒   for (var i = 0; i < 6e7; i++) { acc = (acc * 31 + i) % 1000003; }   window.__initSum = acc;
}

切片版拆成 60 片,每片 100 万次,片间用 setTimeout 让出主线程:

javascript代码解读复制代码function initCarousel() {   var acc = 0, i = 0, N = 6e7, STEP = 1e6;  
 (function chunk() {     var end = Math.min(i + STEP, N);     for (; i < end; i++) { acc = (acc *
 31 + i) % 1000003; }     if (i < N) { setTimeout(chunk, 0); }     else { window.__initSum = acc; }
    })(); }
    

每片不到 50ms,输入响应和渲染帧都能在片间插进来。实测:TBT 990ms → 0,LCP 顺带 2.6s → 2.2s(首帧渲染不再被挡),性能分 75 → 99。

要诚实地说:这 0.4 秒是顺带的,这步真正救的是交互。总工作量一点没少,6000 万次还是 6000 万次,只是不再一次全塞给主线程。setTimeout 是最朴素的做法,任务更重还有 scheduler.yield 和 Web Worker,但「先拆开」比选哪个 API 重要。

第 5 次:别给手机喂 1920 的图,2.2s → 1.5s

最后回头看图片本身:手机视口 412px 宽,2 倍屏也只需要 824px 的图,我却给所有设备发 1920 宽原图。

bash代码解读复制代码cwebp -resize 840 560 -q 90 hero.jpg -o hero-sm.webp

141KB 变 25KB,加载时长 1.5s → 0.83s,LCP 收在 1.5s,性能分 100。

工程里这步通常是响应式图片(srcset/sizes)或图片服务按宽度出图,原理相同:渲染尺寸决定需要的像素,别多给。第 1 次省的是「格式的浪费」,这次省的是「尺寸的浪费」,两者经常被混为一谈——压了格式没压尺寸,头图照样虚胖。

复盘:3.6 秒里,代码只值 0.4 秒

把 5 次改动按杠杆归类:

杠杆

对应改动

贡献




资源体积

WebP、按视口出图

-1.4s、-0.7s

资源发现

preload + fetchpriority

-1.1s

主线程

defer(暴露问题)、切片(解决问题)

TBT 990ms → 0

4 次配置级的改动拿走 3.2 秒,唯一那次改代码贡献 0.4 秒 LCP 加一个干净的 TBT。不是说代码不重要——没有切片,页面就是个「图片秒开但点不动」的页面,TBT 990ms 是真实存在的卡顿。它想说的是:动手改代码之前,先把配置层和资源层的便宜捡完。

操作顺序三步:跑一次 Lighthouse 移动端测试,看 LCP 分解四段里哪段最长;发现延迟长 → preload,加载时长长 → 压图出小图,渲染延迟长 → 查长任务;配置层治完还剩长任务,再动代码——切片,而不是删功能。

判断「该动哪一刀」永远比「能动多少刀」值钱。你的页面最近一次跑 Lighthouse 是什么时候?评论区报个数,我猜不少人从没看过自己页面的 LCP 分解——那里面的每一项,都对应一行能落地的改动。


作者:kyriewen
链接:https://juejin.cn/post/7688212417864024091
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


回到顶部