先说结论
这次优化的对象,是一个用 Vue 3、Element Plus 和 Vite 搭的单页社区应用。最后的数字不错:
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 首页 LCP(4x CPU 节流,3 轮中位数) | 634 ms | 361 ms | −43% |
| 首页最长挂载任务(4x 节流) | 412 ms | 186 ms | −55% |
| 首页首屏关键 JS 体积 | 436 KB | 171 KB | −61% |
| 无障碍得分(两页) | 83 / 82 | 100 / 100 | 满分 |
| SEO 得分(两页) | 92 / 92 | 100 / 100 | 满分 |
| CLS(布局偏移,全程) | 0.00 | 0.00 | 无回归 |
但这不是一次一路猜对的优化。中间有三个假设被实验推翻,也有两项改动明明减少了代码体积,却没能在本地测量中体现出来。相比跑分从 83 到 100,我更想记下这些走弯路的部分。
一、起点:问题到底出在哪
项目是一个AI 知识社区的单页应用。起因是 Lighthouse 跑分:性能分还过得去,但无障碍只有 83/82 分,SEO 92 分。更值得深挖的是加载指标:
| 指标 | 首页 / |
帖子详情页 /forum/post/1 |
|---|---|---|
| TTFB(服务器响应时间) | 13 ms | 3 ms |
| FCP = LCP(首次内容绘制 = 最大内容绘制) | 379 ms | 320 ms |
| 其中主线程 JS 执行占比 | 96.8% | 99.2% |
| 最长单个任务 | 226 ms | 180 ms |
| CLS(布局偏移) | 0.00 | 0.00 |
把这组数据拆开看,问题很集中:
- 服务器响应只要 13 毫秒,本地网络几乎没有开销。
- 页面却要到 379 毫秒才显示内容。
- 这 379 毫秒里,约 97% 的时间花在执行 JavaScript 上。
所以第一个排查对象不是网络,而是首屏一次性执行的 JS。报告里最显眼的两块是 Element Plus,约 246KB;另一块公共库约 249KB。当时还不知道哪些代码真的拖慢了首屏,但至少排查范围已经缩小了。


二、我是怎么测的
这类优化很容易变成「看到大包就拆」。为了尽量少猜,我用了四类工具:
- Chrome DevTools MCP:用脚本驱动真实 Chrome,跑 Lighthouse 审计,同时记录加载 trace。长任务、布局和脚本执行都从这里看。
source-map-explorer:把打包产物拆到字节级,看清那 246KB 里究竟有什么。- 自写的 Python 脚本:从 trace 原始数据中提取首屏时间、主线程繁忙时长和单个任务的构成。trace 概要页不会直接给出这些数字。
- 4x CPU 节流:模拟慢一些的设备,也用来做最后的 A/B 验证。
这里有个前提:本地快机器上测得的毫秒数,只适合比较改动前后的相对变化,不能直接当成线上数据。后面我碰到了好几次「优化小于噪声」的情况。
三、先处理首页:把非首屏模块移出关键路径
246KB 里,首页真正需要的只有 15KB
用 source-map-explorer 拆开 Element Plus 包后,问题就清楚了。首页实际只用了按钮、标签、日历和弹窗,大约 15KB。select、上传、表单等组件约占 190KB,原本只有懒加载页面会用到,却因为构建配置跟着首页一起进了包。

折叠线以下全部改为懒加载
- 首屏区块保持静态,其余 9 个区块改成异步组件
- 用 IntersectionObserver 在区块进入视口前 400px 提前挂载
- 每个区块用实测高度占位,防止布局跳动(CLS)
| 指标 | 基线 | 优化后 | 变化 |
|---|---|---|---|
| FCP(首次内容绘制) | 379 ms | 314 ms | −17% |
| LCP(最大内容绘制) | 379 ms | 314 ms | −17% |
| 最长单任务 | 226 ms | 168 ms | −26% |
| 首屏关键路径 JS | 436 KB | 171 KB | −61% |
| 首屏关键路径 CSS | index.css + EP 90.8KB | 仅 index.css 36.8KB | EP 样式移出 |
| CLS | 0.00 | 0.00 | 无回归 |
改完 9 个区块后,构建报告里还是能看到 Element Plus 的静态引用。这一度让我以为懒加载没有生效。沿着 sourcemap 逐个模块查下去,才找到首屏区块里的「活动日历」:它仍在静态引用 Element Plus。
依赖图能告诉你还有一条边,但不一定能直接看出这条边从哪里来。遇到这种隐藏引用,最后还是得回到 sourcemap 逐个模块找。
四、给 highlight.js 减肥后,为什么本地指标没有变化
帖子页的公共包里,约 150KB 来自 highlight.js 的全量语言包。它注册了 37 种语言,包括 swift、php 和 less;论坛中的代码块实际只用到 python 等少数几种。
改动不大:只保留核心和 14 种常用语言,其他语言按纯文本显示。代码不会丢,只是没有高亮。
| 项 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 公共包 JS | 248 KB | 170.9 KB | −77KB(−31%) |
| gzip 后 | ~87 KB | 67.9 KB | −19KB |
包体积确实下来了。但复测结果不太配合:
三轮复测的 FCP 中位数是 335.8ms,和优化前的 319.7ms 无法区分。
| 轮次 | FCP |
|---|---|
| 第一轮 | 335.8 ms |
| 第二轮 | 472.4 ms |
| 第三轮 | 218.7 ms |
同一份产物、同一个地址,三轮 FCP 散布在 218.7--472.4ms。测量噪声大约是 ±120ms,而减少的 77KB 在这台快机器上只对应 2--5ms 的解析开销,完全被噪声盖住了。
减少 77KB 传输和解析仍然是确定的收益,只是这套本地环境没有能力把它测出来。这个结果有点别扭,但比硬说 FCP 优化了更诚实。
五、剩下的长任务从哪里来:三个猜测都错了
给 highlight.js 减肥后,首页还留着一个 167ms 的长任务。我当时根据包体积猜了三个方向:
- 换掉日历组件(ElCalendar)
- 把长任务拆到空闲时间执行
- 减少首帧渲染的中文文本量
结果三个都没猜对。
拆开长任务:70% 是布局
对性能录制做事件级拆解:167.5ms 的长任务里,118ms 是布局(浏览器计算每个元素的位置),真正的 JS 只有约 22ms。
这先排除了第二个方向。任务里没有大块 JS 可拆,而布局也不能靠把脚本放到空闲时间来解决。
ElCalendar 没有执行
录制显示,在首屏绘制之前,日历组件的代码根本没有执行。换掉 ElCalendar 不会解决这个长任务。
第一次布局比之后慢了 25 倍
同一段录制里,布局规模相近,第一次却用了 118ms,后面再做一次只要 4.75ms。
这种「第一次贵,之后便宜」的现象让我怀疑:浏览器首次渲染中文时,可能会同步加载系统字体 PingFang 的数据。如果是这样,这笔成本在每个浏览器会话中只会出现一次。
用拉丁字符做一次对照
为了验证中文文本是否真的会拖慢布局,我做了个对照实验:
给页面加一个隐藏入口
?latin=1。在浏览器首次布局前,把页面上所有中文字符 1:1 替换为字母x。DOM 结构、样式和元素数量都不变,只换文字语种。
四轮交替测试:
| 轮次 | 变体 | FCP | 挂载任务 | 布局耗时 |
|---|---|---|---|---|
| 1 | 拉丁替身 | 150.8 ms | 52.3 ms | 17.7 ms |
| 1 | 正常中文 | 195.3 ms | 57.3 ms | 18.9 ms |
| 2 | 拉丁替身 | 228.4 ms | 54.0 ms | 20.2 ms |
| 2 | 正常中文 | 286.4 ms | 54.4 ms | 19.8 ms |
中文和拉丁字符的布局耗时几乎一样,只差 0--1.4ms。第三个猜测也不成立。
这轮实验得到了什么
- 118ms 的冷布局是浏览器会话级的一次性成本,来自首次渲染中文时加载字体数据。应用代码改不了它,站内跳转也不会反复支付这笔开销。
- 暖态下 18--20ms 的布局是页面结构成本,可优化空间 ≤10ms 级
这轮实验没有找到一个能立刻改的点,但它让我没有去换日历组件、拆一段不存在的大块 JS,或者折腾首屏中文。对性能排查来说,知道哪些事不该做,也是结果。
六、排查过程中,意外找到一次强制布局
录制中还有一处「强制布局」警告,来自背景动画组件。代码在布局前读取了画布的位置和尺寸,迫使浏览器提前做一次完整布局。全仓库只有这一处。
修复只改了 +8/−4 行:
- 画布尺寸直接根据窗口尺寸计算,不再触发布局。
- 首帧绘制放到下一帧。背景本来就在内容层下面,晚一帧看不出差异。
| 指标 | 修复前(4 轮) | 修复后 |
|---|---|---|
| 最长挂载任务 | 52.3--57.3 ms | 43.0 ms |
| 变化 | --- | −21% |
我也顺手测了常驻的背景动画:每帧只占用主线程 0.89ms,约为一帧预算的 1/18,构不成卡顿风险。项目的约束是性能优化不改观感,所以我没有继续动这段动画。为了省 0.1ms 去冒视觉变化的风险,不值。
七、补齐无障碍与 SEO 审计项
这部分不会提高性能分,但会影响读屏软件用户和搜索引擎爬虫。基线中两个页面的无障碍得分只有 83 和 82,SEO 都是 92。
| 维度 | 首页 | 帖子详情 |
|---|---|---|
| Accessibility | 83 → 100 | 82 → 100 |
| SEO | 92 → 100 | 92 → 100 |
| Agentic Browsing | 33 → 100 | 33 → 100 |
| 失败审计项 | 7 → 0 | 8 → 0 |
这里有几个容易误判的地方。
- Agentic Browsing 是 2026 年 5 月新增的维度,Lighthouse 13.3.0 起默认运行,这次用的是 13.4.1。它不计加权分,只算审计通过率。基线的 33 分,就是 3 项有效审计只通过了 CLS;修正无障碍树、补上
llms.txt后,3 项全部通过,分数直接变成 100。Google 搜索团队明确说过,llms.txt不是排名因素。这个维度看的是网站对 AI agent 是否友好,不要把它当成 SEO 加分项。 - 审计结果会漂。懒加载区块在不同轮次以不同状态进入审计,同一页面连续两轮报出的失败项都不一样。因此单轮通过不能当验收结果。我后来改用连续多轮收敛,再配合全仓库搜索同类模式。
- 组件库的实际 DOM 不一定符合直觉。Element Plus 的日历日期格是
td > div,没有按钮语义;select 的无障碍标签则要落在内部input上。动手前得先看浏览器里的 DOM,不能只看 Vue 模板。
还有一处无法避免的视觉变化:对比度审计要求我调整颜色。最终改了 18 处,都是加深一档;布局和动画没有变。
八、在 4x CPU 节流下重新验证
前面的多数测量都在本地快机器上完成。最后一轮,我把优化前后的代码放到同一个 Chrome 环境中,开启 4x CPU 节流,分别测 3 轮并取中位数。
首页加载:
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| LCP | 634 ms | 361 ms | −43% |
| 最长挂载任务 | 412 ms | 186 ms | −55% |
| 首屏前主线程繁忙 | 570 ms | 374 ms | −34% |
| CLS | 0.00 | 0.00 | 无回归 |


优化前的 3 轮数据波动很大,范围是 595--1062ms;优化后 3 轮落在 351--373ms。不只中位数下降,波动也明显变小了。
之前预测收益在节流环境下会放大 3--4 倍。实测中,按百分比计算是 2.5 倍,按绝对差值计算是 4.2 倍。原来的方向没错,但不做这轮节流对比,无法确定收益到底有多大。
这轮也留下了三个没那么漂亮的数字:
- 帖子详情页的 LCP 从 677ms 变为 670ms,只差 1%,仍在噪声内。本地没有网络传输成本,所以 highlight.js 减少的 77KB 依然体现不出来。这不算回归,也不能算成有可测的提升。
- 冷缓存首载的 FCP/LCP 约为 1100ms,暖态约为 360ms。字体的一次性成本按约 4 倍放大重现,应用层动不了,只影响会话中的第一次访问。
- 路由跳转和日历翻页在 4x 节流下耗时 60--83ms,低于 200ms 的「良好」线。
九、这次实际踩到的 11 个坑
- 懒加载会让审计结果漂移。不同轮次可能报出不同失败项,所以要看多轮是否收敛,还要在全仓库搜索同类问题。
- Element Plus 样式在运行时注入,比全局覆盖文件更晚,导致全局变量映射整体失效。最后用双类选择器的工具类规避。
- 日历日期格没有按钮语义,select 的无障碍标签落点在内部
input。组件库问题要先看现场 DOM。 - 采集工具断连会产生看似没有内容的长任务,但不能看到「空壳」就略过。有次 660ms 任务的外层是壳,内层确实有 580ms 的工作。形态相似不代表原因一样。
- EventTiming 的计时止于下一帧绘制。如果路由还没渲染完,下一帧仍是旧内容,它就会低估路由延迟。我后来补了
MutationObserver来测真实挂载时刻。 autoStop会在about:blank上因为空闲提前停止录制。关掉它,改成手动停。- 测量脚本自己也可能触发告警。只有去掉插桩重载后,控制台仍然为零,才能说应用是干净的。
- 首轮录制经常是异常值。报告里以中位数为主,同时注明数据范围。
- 做 A/B 对比时,依赖锁文件必须一致。确认逐字节相同后,可以用 Git worktree 和符号链接复用依赖,避免重复安装造成环境漂移。
- 我曾错判过注释风格,审查代理也错判过滚动条宽度。这些推断最后都被
git show和现场实测纠正了。 - 这轮性能优化的约束是不改观感。唯一例外是对比度审计要求改色:18 处各加深一档,没有改布局和动画。
十、最后,把有效和无效分开看
| 仓库记录 | 改动 | 结果 |
|---|---|---|
| M1 | 首页懒加载 | 首屏 JS −61%、FCP/LCP −17%(节流下 −43%) |
| M2 | 高亮库瘦身 | 公共包 −77KB(本地不可测,真实网络兑现) |
| M3 | 归因实验 | 三个假设证伪,锁定会话级成本与结构成本 |
| M3b | 挂载任务修复 | 挂载任务 −21% |
| M4 | 无障碍/SEO | A11y/SEO/Agentic Browsing 全 100,失败项清零 |
| M5 | 节流发布级验证 | LCP −43%、挂载任务 −55%、预测验证成立 |
全程 CLS 保持 0.00,应用控制台没有告警,代码检查也全部通过。
但这些数字有明确的适用范围:
- 它们都是实验室数据,用来比较修改前后,不是线上用户的绝对数值。真实网络会额外引入 JS 传输成本,但具体影响需要用线上数据确认。
- highlight.js 减少了 77KB,但本地测量始终没有给出超过噪声的证据。
- 「收益放大 3--4 倍」存在两种计算口径:按百分比是 2.5 倍,按绝对差值是 4.2 倍。
- 4x CPU 节流下约 1100ms 的冷会话开销来自浏览器层,这次没有在应用代码中找到可以消除它的方法。
- 无障碍修复中有 18 处颜色加深,并非完全零视觉变化。
基线报告、六个阶段的实施记录,以及从 8741ada 到 5ae3fa0 的 commit 历史,都在公开仓库 github.com/zhuabo001/wireless-ai-forum 中。文章里只放了结果,排查细节可以直接对照原始记录。
结语
本文基于与llm对话的摘要进行humantize化,作者从技术从业者视角也并不认为本文基于本地项目进行性能优化是一件多么"高大上"的硬核技术行为,也许在类似字节跳动这样的公司内,这样的事早就已经被标准化、流程化;只是在这agent高歌猛进的时代 ------ antd团队都被解散重组,"半年时间前端已死了五六次"的背景下,作为人类前端工程师,我还能做些什么,只能通过这样的事来为自己找寻一些存续的价值。欢迎读者进行分享和讨论,也希望本文能给正在焦虑的同僚们一点"慰藉"(原来还有这么菜的人厚着脸皮在分享这些东西)。