
在性能优化这方面折腾了几年踩过不少坑也总结出了一些自己的方法。每次看到团队里的小伙伴拿着 Lighthouse 分数截图兴奋地告诉我“优化到 100 分了”我都会问一句然后呢页面真的快了吗用户真的感知到了吗坦白讲分数这东西在很多时候只是自我安慰。真正的优化不是把某个工具的打分刷上去而是让真实用户在真实网络环境下打开你的页面时觉得“快”了。今天我就围绕“更好的优化”这件事聊聊我们应该怎么想、怎么做以及哪些操作是真正有价值的。“更好的优化”包含了两层意思第一层是怎么把现有方案优化得更好、更精细化第二层是怎么让“优化”这件事本身做得更好少做无用功少自嗨。这篇内容不是什么高深的理论课而是我自己在实际项目中摸爬滚打出来的经验集合。不管你是刚接触性能调优的新手还是在团队里负责技术攻坚的同学应该都能从里面找到一些能直接用的思路和方案。1. 大多数人的优化方式为什么最后都白做了先说一个我观察了很久的现象。很多团队做优化流程基本是这样的老大觉得页面慢丢下一句“优化一下”然后前端同学打开 DevTools 里的 Performance 面板点一下录制刷新页面看看长任务再看看 Network 面板里哪个文件大然后开始压缩代码、拆包、加懒加载。折腾一两周Lighthouse 分数从 60 提到了 90皆大欢喜。但这种做法有一个致命问题我们优化的是工具展示给我们的指标而不是用户实际体验到的瓶颈。1.1 一个从 4 秒到 4.5 秒的“成功优化”去年我的一个朋友做电商小程序团队花了两周做性能优化把首页的首次内容绘制时间从 4 秒降到了 3.2 秒看着挺不错的对吧结果我帮他看了下监控数据真实用户在 4G 网络下从点击进入页面到能完成第一次交互平均耗时反而从 4 秒涨到了 4.5 秒。原因是他们为了优化首页展示速度把大量逻辑和弹窗代码做了延迟加载主接口的数据结构又没动结果页面首帧是出来了但用户点按钮没反应后面的一堆脚本才排队下载执行。这就是典型的“优化了展示速度牺牲了交互体验”。我们以为自己在优化实际上只是把压力从渲染阶段推到了交互阶段。用户感知到的整体速度没有提升甚至更差了。1.2 你测的是本地局域网用户用的是弱网还有一个几乎人人都犯过的错我们测试性能的时候用的都是公司的高速网络。不管 Chrome DevTools 里的 Network 模拟调成 Slow 3G 还是 Fast 3G它模拟的只是下载速度而不是真实的网络延迟和带宽波动。真实场景是什么呢用户在地铁上信号时好时坏用户在电梯里网络完全断开又恢复用户的手机是两年前的老机型CPU 处理能力只有你测试机的一半。这些变量叠加在一起你本地测出来的那些数字——无论用 Lighthouse 还是 WebPageTest——都只是理想环境下的参考值不能代表用户的真实体验。这也就解释了为什么很多团队明明做了优化业务数据却没什么变化。方向错了越努力越尴尬。1.3 所有没有目标的优化都是耍流氓优化必须先回答一个问题我们究竟要改善什么是用户打开页面的等待时间是动画交互的流畅度还是降低服务器压力节省成本不同目标对应的优化手段完全不一样。拿我自己举例如果目标是降低跳出率那我优先解决的是首屏内容的加载速度和骨架屏的呈现如果目标是提升成交转化率那我优先解决的是“加入购物车”和“结算”这两个操作路径上的网络请求阻塞和渲染卡顿如果目标是省钱那我优先做的是 CDN 命中率优化、接口缓存策略调整和图片资源的二次压缩。目标不同投入的精力和方向就完全不同。在开始动代码之前先花半天时间把目标理清楚把当前的真实数据测出来把用户的技术环境设备分布、网络分布、浏览器版本摸一遍然后用数据定义一个“优化完成”的标准。有了这个标准优化就不是无底洞了。2. 一条链路拆解先找瓶颈再谈优化抛开那些花里胡哨的工具不看一个页面从用户发起请求到你看到完整内容大致要经过 DNS 解析、TCP 连接、TLS 握手、HTTP 请求发送、服务器处理、响应返回、浏览器解析 HTML、加载 CSS/JavaScript、构建 DOM 树和 CSSOM、渲染布局绘制、JavaScript 执行和用户交互响应。这条链路上任何一个环节拖后腿都会成为瓶颈。2.1 用 Performance 面板定位真正的瓶颈Chrome DevTools 的 Performance 面板是排查前端性能瓶颈的第一站。很多人用它只是看看那个“DOMContentLoaded 时间”或者截个图就算完事其实远远不够。我常用的做法是打开 Performance 面板在设置里把 CPU 降速调整为 6 倍网络用 Fast 3G 模拟然后录制整个页面加载过程。录制完成后重点看三层信息第一层概览区域里有没有长任务即主线程被占用的时间。如果主线程上有超过 50ms 的长任务并且这些长任务出现在首屏渲染期间那就说明 JavaScript 的执行阻塞了页面渲染。这时候你把长任务展开看它下面的调用栈就能找到具体是哪个函数在作祟。第二层Network 区域里哪些请求是串行的。注意浏览器对同一个域名下的 HTTP/1.1 连接数有限制通常是 6 个左右。如果你发现很多静态资源都堆积在这个 6 连接的队列里那就说明你还没有启用 HTTP/2 或者并发连接数管理不当。第三层渲染区域里有没有频繁的 reflow 和 repaint 事件。这两个事件一旦频繁发生页面就会感觉“卡卡的”尤其在滚动和动画的时候。2.2 链路中的常见瓶颈分布我把日常项目里最常见的瓶颈整理成了一张表你们可以根据自己的实际情况对号入座瓶颈位置典型表现常见原因优化切入点DNS 解析瀑布图开头有较大灰色块DNS 解析时间长服务商不稳定升级 DNS 服务商开启 DNS 预解析TLS 握手瀑布图前面有连续的加密握手痕迹TLS 往返次数多未启用会话复用开启 TLS 1.3设置正确的会话缓存参数服务器响应TTFB 时间长后端接口慢、数据库查询慢、缺少缓存增加接口缓存升级后端架构优化查询静态资源加载单个资源下载时间长资源体积大、未启用压缩、未用 CDN开启 gzip/Brotli走 CDN压缩图片JavaScript 执行主线程长任务多框架自身体积大、第三方库过多、同步任务重代码拆分、路由懒加载、Web Worker 分担计算CSS 渲染阻塞首屏样式迟迟不出现CSS 文件过大、关键 CSS 未内联内联关键 CSS延迟加载非关键样式可以这么说80% 的性能问题都能在这张表里找到归属。你不需要一开始就把整条链路的所有环节都优化一遍只需要先找出最突出的一两个瓶颈集中打破。2.3 Lighthouse 分数只是参考系不是目标我很喜欢用 Lighthouse但它绝对不能成为优化的唯一标准。Lighthouse 的评分逻辑是给每一类指标设定一个理想值区间你离理想值越近分数越高。问题是这些指标之间的权重在真实用户体验里并不是一成不变的。比如 Performance 这一项里Lighthouse 特别看重 First Contentful Paint 和 Largest Contentful Paint。如果你的页面是一个图片非常多的瀑布流想把 LCP 做高就需要优化图片加载但如果你的页面是一个在线文档编辑器用户更关心的其实是输入响应速度这时候 LCP 的参考意义就没那么大了。所以我是这么用 Lighthouse 的把它当做一个检测工具用来发现浅层问题比如有没有未压缩的图片、有没有阻塞渲染的脚本、浏览器缓存策略是否合理。发现了问题之后回到 Performance 面板去深入定位然后再在真实设备上验证。分数出来了记录一下不要把它当成项目目标。3. 指标选取的学问哪些数字值得优化哪些只是自我安慰性能指标非常多从最经典的 DOMContentLoaded、Load到后来的 First Paint、First Contentful Paint、Largest Contentful Paint、Cumulative Layout Shift、Time to Interactive、Total Blocking Time、Interaction to Next Paint再到 First Input Delay这个指标现在已经被新指标取代了但在一些旧系统里还能看到。如果每一个指标都想做到最好你就把自己逼进了死胡同。3.1 分清主次核心用户路径上的关键指标我在项目的早期阶段就会和数据团队一起确定一个共识在核心用户路径上哪些指标是北极星指标哪些是辅助参考。举个例子一个内容型 App 的启动页或者一个电商网站的商品详情页我会重点关注这几个LCPLargest Contentful Paint用户看到主要内容比如商品主图、文章标题有多快。这是用户对速度感知最直接的来源。CLSCumulative Layout Shift页面加载过程中内容有没有发生明显跳动。这直接影响用户是否愿意继续操作下去尤其对点击类操作影响极大。INPInteraction to Next Paint用户点击或输入后页面多久能给出响应。这是用户体验中“顺不顺手”的关键指标。至于那些什么 BFCache 命中率、JavaScript 长任务数量、WebSocket 连接数量这些属于偏内部的技术观测指标不是不能用但别把它们放在和用户体验指标同等重要的位置。因为这些数字再漂亮用户也不会感知到。3.2 用数据看板盯趋势而不是盯单个瞬时值性能指标这种东西单看一次测出的数字说明不了任何问题。你网络好一点数字就好看一点你的手机是新款数字就好看很多。真正有意义的是趋势。我们团队的做法是在前端监控平台比如自研的或者第三方 APM上建立一个性能看板按日、周维度展示核心指标的中位数和 P75 分位值并且按设备和网络环境拆分。为什么要看 P75因为中位数代表大众极致值代表最优P75 以上才能看出在弱网和老设备上用户的真实体验。优化如果只把中位数提上去了P75 还很难看那说明还有一大部分用户的体验很差我们就没有真正做到位。另外每次发布版本后我都会对比一下新版和老版在同一个监控维度上的数据波动。不要等用户投诉了才发现线上质量回退。CI 流水线上挂一个性能检查任务、发布后 24 小时自动报警这些都是很成熟的做法团队小的话先手动做也可以。3.3 不要陷入指标陷阱为了很低的 CLS 让页面空一大块有一次我在优化一个资讯类网站时发现它的 CLS 分数特别低低到惊人的 0.02。数据这么好看页面应该很稳定吧结果打开页面一看首屏的上半部分是一大片空白下方的内容还在通过各种异步接口慢慢加载出来。因为内容都是异步出来的自然不会有内容跳动CLS 分数自然就很低但用户看到的却是满屏空白。这类“优化”就是典型的指标陷阱。优化 CLS 的正路应该是为图片和视频等媒体元素预留固定尺寸的占位区域在字体加载期间使用匹配的 fallback 字体预留空格避免在用户交互过程中动态插入页面头部内容。这些做法是在保证内容正常展示的前提下减少布局跳动而不是把内容延迟到用户划不到的地方。4. 优化落地时那些没有写在文档里的操作细节理论说了一大堆到了实际操作阶段还是有很多容易被忽略的小细节。这些细节恰恰决定了优化方案能不能真正生效。4.1 图片优化不要只会压缩 JPEG图片优化是很多团队最先动手的环节但也是粗放操作最重的地方。很多人以为图片优化就是丢到 TinyPNG 里压一下或者把 JPEG 转成 WebP 就完事了。实际操作中你还需要考虑这些事情第一尺寸适配。很多小伙伴上传图片的时候就传了一张几兆的原始大图然后前端用 CSS 把它的显示尺寸设成 300 像素。浏览器虽然只渲染 300 像素但下载的还是几兆的原始图这不是浪费流量是什么你至少要在上传管线里生成 750、1080、1920 等几档不同宽度的图片前端依据实际容器的宽度选择加载哪一档。这一步比压缩格式转换的收益大得多。第二懒加载策略。图片懒加载确实能显著减少首屏请求数但注意一点首屏内正在显示的图片不能懒加载否则你优化了加载速度却恶化了 LCP。判断标准很简单使用 IntersectionObserver只对进入视口 200 像素以内、且不是首次渲染必须出现的图片做懒加载。第三不要一刀切全转 WebP。WebP 虽然在小体积和画质上表现不错但有些老旧的 WebView 环境不支持。稳妥的做法是使用 picture 元素配合 source 标签做兼容或者使用较新的 AVIF 格式并配合方案判断尺寸。当然如果你的 CDN 服务商本身就是支持图片实时压缩转换的那这些工作就可以下沉到 CDN 层。4.2 给静态资源一个合理的缓存策略这个钱花得最值我见过很多项目每次发版都给所有静态文件名打上一个全新的 hash 串然后不分文件类型全部设置成 no-cache或者连设置都不设置让它默认走协商缓存。这种操作在技术上是没错的但它完全没有发挥缓存的威力。正确的做法是把资源分成两类一类是文件名带 hash 的内容资源如 JS 和 CSS它们永远不变可以设置 Cache-Control: max-age31536000, immutable另一类是入口文件如 index.html或者不带 hash 的资源设置成 no-cache 或者 max-age0确保每次都能检查更新。从最终的加载效果来看这样配置之后老用户再次访问只加载入口 HTML 和一小部分动态接口其余资源全部命中强缓存页面直接秒开。这个优化思路不需要改一行业务代码纯粹靠服务器配置就能把二次访问的速度提升一个数量级。4.3 第三方脚本微信公众号 SDK 都要慎之又慎这个我要多说几句。很多团队觉得第三方脚本是现成的直接引入就好不用管优化。但实际上你在页面里塞的每一个第三方 SDK——不管是数据统计的、客服聊天的、用户行为分析的还是社交分享的——都会带来额外的 JavaScript 执行和网络请求而且不可控。我自己吃过一次亏。有一次某个页面因为接了一个在线客服 SDK打开页面后主线程被各种监听器和心跳请求占住滚动和点击都明显发闷。后来我做了一个判断把这些第三方脚本分成两类必须用户授权才加载的比如客服做成交互触发后按需加载数据统计类的比如埋点上报把它放到主包之外异步加载完全不阻塞首屏。不要迷信“加个 async 或 defer 就没问题了”。async 只是不阻塞解析但脚本一旦下载完它照样在主线任务中执行并抢占主线程。真正安全的位置是放在低优先级队列里或者在空闲时间用 requestIdleCallback 动态插入。4.4 接口缓存和预取前端不止能优化静态资源很多人的优化思维停在静态资源层面一说到“接口”就觉得那是后端的事。实际上前端视角的接口优化能做的也很多。最常见的是对返回结果稳定、时效性要求不高的 GET 接口做前端缓存。比如城市列表、配置信息、公共字典这种接口第一次请求后就把结果存在内存里或者 sessionStorage 里下次直接读缓存连请求都不发。注意要设置合理的失效时间别把用户的权限信息或者实时库存这类动态数据缓存起来否则容易出事故。预取是另一个容易被忽略的手段。如果一个页面是所有用户的主入口而另一个二级页面是大概率会跳转的那我就可以在用户停留在主入口的间隙用空闲时间悄悄把二级页面的核心数据和 JS 资源预取好。用户真正点进去的那一下页面基本是秒开的。这种体验上的提升远比把主包体积再硬砍 50KB 来得明显。我自己的经验是页面 A 到页面 B 的转化率如果超过 30%预取的收益就非常可观。5. 迭代过程中的 4 个好习惯优化不是一次性的项目它更像是一个反复迭代的过程。与其搞轰轰烈烈的优化专项不如在日常开发中养成几个好习惯。5.1 每一次代码评审都把性能影响写进去我们在团队里规定提交代码到 MR 的时候PR 描述里必须有一栏是“性能影响说明”。如果改动涉及图片资源、第三方库、接口请求频率、组件渲染逻辑中的任何一项就必须写清楚对页面加载或交互响应的影响是什么。没有写默认视为有影响会打回去补充。这听起来很繁琐但实际操作后发现它最大的价值不是那几行描述而是逼着每个人在写代码的时候就想一下这段代码在低端机上跑会不会卡引入的这个库会让主包变大多少能不能换个更轻量级的方案思维一变很多性能问题在源头就被避免了。5.2 回归测试里始终带着弱网场景我们的测试用例里固定保留几个性能回归场景Fast 3G 下的首屏加载、6 倍 CPU 降速下的滚动和动画、以及打开几十个 Tab 后的内存表现。这些用例不会每次跑但每次发版前都会抽测。如果有版本在这几个场景下出现了明显的性能下降哪怕功能完全正常也会被要求先定位问题再发布。不要觉得这些测试环境苛刻。你的用户里总有网络不好的人总有手机已经用了三年的学生党和长辈用户。既然无法保证每个人都用最新的设备和最流畅的网络那就主动适应他们这恰恰是真正的“更好的优化”。5.3 性能问题要有责任人也要有“冷静期”优化工作最忌讳的就是人人有责但人人不管。无论是前端小组还是后端小组都需要有一个明确的责任人来负责监控数据、发现异常、推动修复。这个人不一定要自己做所有优化但他必须对性能数据负责并且有权力推动相关的人去配合。所谓“冷静期”是我自己实践出来的一个规则任何优化上线之后不立刻下结论先观察两到四周。因为性能优化和业务变更不同它的效果往往有滞后性用户习惯、网络环境、缓存命中率都会影响最终结果。过早地判断一个优化方案无效或者过早地宣布它成功都容易做出错误的决策。5.4 把优化数据同步给业务方最后一个习惯可能偏管理但非常有效。每次做一次比较大的优化我都会把优化前后的性能数据整理成一张简单明了的表格或截图然后同步给产品经理、运营和客服。原因很简单客服是直接面对用户反馈的人如果用户反映页面慢了客服如果能拿出一张“我们最近一次优化让页面快了 30%你试试刷新一下再操作”的回应会比一句空泛的“我们知道了”更有说服力。同时让业务方看到优化带来的数据变化也能让他们理解技术团队的价值。我甚至见过有运营同学因为看到性能数据提升了主动配合调整了首页的广告位位置让广告图和内容资源的加载顺序更合理最终整体体验反而更好了。这种跨岗位的协作很多时候就是从一次透明同步开始的。6. 我踩过的几个坑希望你们别再踩了最后聊几个我自己亲自踩过、修复过、然后形成了肌肉记忆的坑。每个坑说起来都不算大但在关键时刻真的会让人头疼。第一个坑是框架自带的优化工具用过度。比如 Next.js 的 Image 组件、Nuxt 的自动导入、Vite 的代码拆分这些工具默认行为确实帮你省了很多事但当你对它们进行过度配置的时候可能反而会引入新的问题。我曾经为了优化 Next.js 图片组件把一堆图片全部改成优先级加载结果首页请求数暴增LCP 反而变差了。工具是双刃剑用之前一定要搞清楚它的默认策略再决定要不要覆盖。第二个坑是只优化了第一次访问忽略了二次访问。前面提到的缓存策略重写之后二次访问的速度确实快了很多但如果你不监控“新用户/老用户”这两个分组的独立数据你就很难发现新用户的体验还是很差。老用户访问快只能说明缓存生效了新用户访问快才说明你的资源体积、请求链路真的做了有效的优化。第三个坑是过分依赖 CDN 的回源。CDN 把资源缓存到边缘节点后访问确实快但如果你的 CDN 的命中率低或者回源链路设计得不合理那用户访问资源的速度反而可能比直连服务器还慢。配置 CDN 之后务必去看看命中率和回源平均耗时这两个数字它们和用户端的加载速度是强相关的。第四个坑是忽略了字体加载。字体文件通常有几百 KB 甚至上兆而且渲染时会阻塞文本显示。我见过不少页面首屏图片加载得飞快但文字迟迟显示不出来这就是字体加载阻塞导致的。正确的姿势是使用font-display: swap并且按需加载字体子集。如果是中文字体更推荐使用字体裁剪工具只打包页面上出现的字符。第五个坑是内存泄漏的累积效应。性能优化不能只盯着页面加载还要关注用户长时间停留在页面上之后的表现。单页应用里如果组件卸载时没有清理定时器、事件监听器、全局状态引用内存占用会一直往上涨。到某个临界点哪怕加载速度再快页面也会卡得动不了。每次做路由切换或者弹窗关闭动作时顺手做好清理工作这是成本最低的防泄漏手段。说白了“更好的优化”这件事本质上就是一个从模糊到清晰、从局部到整体、从一次性到常态化的演进过程。与其迷信某一个工具、某一个分数、某一个技巧不如把自己的性能观打通知道关键时刻去看哪些数据、做哪些决策、规避哪些陷阱。当你真正把性能优化融入日常的开发习惯里你就会发现它不只是一个技术动作更是一种工程素养。希望我这些年的经验能帮你少走几步弯路。