
做前端性能优化最忌讳的一件事就是上来就改代码。很多时候我接到一个页面变慢、白屏时间过长的反馈第一反应不是打开编辑器而是先打开浏览器开发者工具和性能面板把这个页面到底慢在哪一步搞清楚。因为性能优化本质上是定位瓶颈、解决问题、验证效果三个动作的循环其中第一步定位往往决定了后面所有工作是不是白费。这篇文章会把前端性能优化的常用手段做一个系统梳理。覆盖网络传输、构建打包、运行时渲染、图片资源、监控排查几个部分每一部分都会给出可以直接落地的做法以及我实际踩过的坑。适合有一定前端基础、想系统了解性能优化知识点的开发者也适合那些项目遇到卡顿、却不知道从哪里下手的同学。内容不追求高深的理论重点是把一套能反复使用的排查思路和优化手段讲透。1. 性能优化到底在优化什么1.1 先回答一个问题你的产品慢在哪性能优化之前必须要先定义“慢”。不同场景下用户的感知完全不同打开一个后台管理系统用户在意的是白屏时间、登录跳转的速度打开一个移动端H5页面用户在意的是首屏是否有内容出来、图片是否清晰打开一个数据大屏或复杂交互页面用户在意的是交互响应是否跟手、图表渲染的时候页面会不会卡死。这三类问题对应的技术本质分别是网络加载、首屏渲染、运行时性能。如果你一上来就去做代码层面的优化比如把某个函数的循环改成用哈希表、把某个DOM操作改成批量更新结果发现用户抱怨的是白屏时间太长那你的优化方向就完全错了。我的做法是先跑两轮检查第一轮用性能测试工具看综合得分和核心指标第二轮用浏览器自带的性能录制看实际加载瀑布图把时间消耗在哪个阶段精确到毫秒级。这两轮做完基本能判断是网络问题、构建体积问题还是渲染性能问题然后再决定动哪里。这个流程看起来简单但我见过很多人绕过去直接开干最后优化了个寂寞。1.2 优化工作的两条底层原则第一原则是优先解决关键路径。所谓关键路径就是从用户发出请求到页面出现可用内容之间所有必须完成的资源加载和渲染步骤。HTML要下载解析、CSS要下载并构建样式树、JavaScript要下载并执行这些环节都是阻塞的。你优化任何一个非关键路径上的东西用户都感知不到但你在关键路径上省下100毫秒用户立马就感觉快了。一个很有代表性的例子是有些团队会把大段的JavaScript放到页面底部加载以此减少首屏阻塞。这个思路在传统多页应用里有效但现在很多框架应用是路由级别的按需加载如果你没有做代码分割页面底部依然是一整包几MB的JS放哪里都没用。所以优化的前提是理解自己的架构再决定手段。第二原则是性能优化必须有量化目标。没有目标的优化等于没有优化。业界比较通用的参考线是首屏内容绘制在1.8秒以内、最大内容绘制在2.5秒以内、可交互时间在3.4秒以内、累计布局偏移小于0.1。你可以根据业务场景调整阈值但一定要有数字。否则优化了一个星期你只能说“好像快了”无法向团队或用户交代。性能优化有一个很扎心的现实你感受到的“快”和小部分用户的“慢”经常是两回事。必须用可量化的指标驱动优化靠主观感受做性能优化最后一定返工。2. 网络层优化把请求数量和体积压下来2.1 资源压缩与编码选型网络层优化的核心目标有两个减少请求数量、降低每个请求的传输体积。这两件事做好了首屏耗时的下限就被锁定了。先说压缩。文本资源JavaScript、CSS、HTML、JSON目前主流方案是用Gzip或Brotli压缩。Gzip是默认选项兼容性广Brotli压缩率普遍比Gzip高15%到20%但需要服务器支持。如果你的静态资源托管在支持Brotli的平台上直接开Brotli实测一个多MB的JavaScript文件能被压到200KB左右效果非常可观。需要注意的一点是现在的构建工具默认会把静态资源再压缩一次比如打包工具自带的压缩插件那个压缩是去掉注释和空格的代码压缩和传输层的Gzip压缩是两个概念两者都做才算完整。压缩之外还要注意编码选型。比较典型的是Base64内联小图。很多人会把小图标转成Base64塞进CSS里想减少请求数。这个思路对几个10KB以内的小图确实有效但一旦图稍微大一点比如超过20KBBase64编码会让图片体积增加约33%带来的下载时间反而超过了省掉的请求时间得不偿失。换句话说优化手段都有适用边界不是什么都套用。还有容易被忽略的一部分是服务端响应时间。浏览器发出请求后如果服务端处理接口花了800毫秒那你前端怎么优化资源体积都是白搭。所以排查网络问题时我习惯把服务器响应时间作为第一个检查项如果它在总耗时里占比过高问题根本不在前端应该找后端同事一起处理。2.2 缓存策略让第二次访问接近零耗时缓存是做网络优化时性价比最高的一环。HTTP缓存策略的核心是动静态资源分开对待。静态资源JavaScript、CSS、图片的文件名里带着内容哈希内容变了哈希就变这样就可以放心用长缓存。我一般设置一年有效期。因为文件名哈希变了浏览器会当作新文件去请求文件名没变直接走本地缓存。动态接口资源一般不缓存或只做协商缓存避免拿到脏数据。实践里很多人会忽略的一个点是缓存和版本发布的配合。如果你设置了长缓存但文件没有版本号那发布新版本后用户拿到的还是旧文件这是线上事故最常见的来源之一。所以静态资源必须带哈希这是一个硬约束不是建议。另一个容易忽视的优化是给关键资源加合理的加载优先级提示。用预加载告诉浏览器某些关键资源需要提前加载用预连接提前和第三方域名建立连接。这些提示不能乱加加多了反而会抢占带宽但给首屏关键字体、关键脚本加上确实能带来可感知的速度提升。比如一个页面用到字体文件字体文件加载慢了会导致文字白屏或者闪一下提前预加载字体能明显改善这个体验。2.3 请求合并与并行加载的边界关于合并请求传统做法是把很多小图片合成一张雪碧图把很多小JavaScript文件合并成一个文件以此减少HTTP请求。这个思路在HTTP/1.1时代是绝对正确的因为浏览器对同一域名的并发连接数有限制。但现在HTTP/2已经普及多路复用让多个小请求可以在同一条连接上并行传输请求数量不再是瓶颈反而单个请求过大会拖累关键资源的加载。所以现在的建议是不要为了合并而合并。代码按模块拆分是合理的它带来的额外请求在HTTP/2下几乎不是问题。而我实际遇到的反而是另一种情况很多团队把第三方库和大业务代码全部打进一个文件导致首屏加载一个几MB的包这种“伪合并”才是当前最普遍的性能杀手。还有一个关于并行的细节浏览器对同一个域名的连接数在HTTP/1.1下有上限所以以前会用多域名分散资源来提升并行度。HTTP/2取消了连接数限制但把资源分散到多个域名反而会破坏多路复用增加DNS查询和连接建立的成本。如果你的服务已经支持HTTP/2老老实实把资源放在同域名下就好不要为了“并行”做反向优化。3. 构建期优化从打包阶段就把性能问题解决3.1 代码分割按需加载的关键手段代码分割是目前解决JavaScript包体积过大最有效的手段。它的思路很简单不是把整个应用的代码一次性打包下发而是按路由、按组件、按实际使用场景拆分成多个小块用户访问哪个路由就只加载那个路由用到的代码。以最常见的路由级分割为例一个单页应用如果有登录页、首页、详情页三个路由不加分割时首屏要加载包含所有页面代码的大包做了分割之后首屏只加载登录页或首页对应的代码块其他路由的代码等用户真正跳转时再异步加载。这个优化对首屏加载的收益非常直观代码量大的项目首屏脚本体积能下降一半以上。实现上也并不复杂现代框架和脚手架都内置了这个能力比如路由懒加载在配置里把组件改为动态导入就能完成。但要注意代码分割的粒度不能太细。我把整个页面的每个组件都单独拆成一个文件时也遇到过问题首屏确实小了但模块数量太多浏览器生成大量HTTP请求在低端移动网络下反而更慢。合理的粒度是路由级别拆一层、页面内明显独立的业务模块拆一层不要无脑细拆。3.2 消除无用代码Tree Shaking的局限与对策Tree Shaking的意思是摇掉那些被引入但没有使用的代码。现代构建工具在打包时会分析ES Module的静态导入导出关系把没有被引用的导出删掉这样可以显著减小打包体积。但Tree Shaking有几个局限需要了解。第一它只对ES Module的静态导入生效对CommonJS的require方式无能为力所以你的业务代码和依赖库最好都使用ES Module写法。第二副作用问题一个模块如果被判断为有副作用比如修改了全局对象、做了运行时绑定即使里面的函数没被用到整个模块也不会被摇掉。解决方式是给第三方库配置sideEffects: false但前提是你能确认这个库真的没有副作用否则可能把正常代码摇掉线上直接报错。我自己踩过一次坑某个项目中有一个包含很多枚举配置的工具文件文件里没有做任何DOM操作看起来是纯数据模块但实际上它初始化时往某个全局缓存里注册了数据被sideEffects: false摇掉后线上很多页面拿不到配置直接白屏。所以配置副作用标记要谨慎一定要在依赖升级后做回归测试。3.3 预加载与预连接的合理使用构建期还能做的一件重要事情是通过构建产物分析工具查看打包后的体积分布。常见的工具能输出一个可视化页面把每个模块占的体积按大小排列。我每次做性能优化第一步就是看这个图找出体积最大的几个模块判断它们是必需的还是可以异步加载还是说重复引入了同一个库的多个版本。举个例子我曾经在一个项目里发现某个日期处理库被引入了三个版本分别来自不同业务模块的传递依赖光这一个库就占了几百KB。处理的思路是在构建配置里加别名强制统一版本并把不用的实例替换掉体积立刻降下来。这种问题不做包体积分析是绝对发现不了的。相比之下预加载和预连接这类构建期配置更多是锦上添花。它们的原理是让浏览器提前做一些准备工作比如提前解析DNS、提前建立TCP连接、提前下载关键资源把等待时间隐藏在其他任务的执行期间。我一般只给首屏确定会用的资源配置预加载给外部域名配置预连接。配置多了反而会浪费网络资源甚至导致关键资源的加载被推迟。4. 运行时渲染优化让页面操作不卡顿4.1 减少布局抖动读写分离与批量更新网络和构建层解决的是“打开慢”运行时优化解决的是“用起来卡”。两者要分开治很多团队把首屏优化做完之后发现用户还是抱怨卡原因就是运行时性能没跟上。运行时性能的第一大杀手是布局抖动。浏览器的渲染流程里JavaScript修改样式后浏览器不一定马上计算布局它会把修改动作合并在适当的时机统一计算。但如果你在修改样式之后立刻读取布局信息比如读取某个元素的offsetHeight、getBoundingClientRect浏览器为了给你返回值就必须强制提前执行布局计算。这时候如果下一次修改又读了如此反复强制布局的次数和计算量会成倍增加页面就会掉帧。具体到代码层面一个常见的错误是在循环里交替执行“修改样式读取位置”。解决办法是遵循读写分离先把所有要修改的样式改完再统一读取布局信息。很多浏览器API已经提供了合并手段比如用动画帧回调整合多次DOM修改让修改集中到同一帧完成。如果你用的是现代框架框架本身已经做了批量更新但如果写的是原生操作或者接入了高频事件就要特别注意这个问题。运行时优化有个核心心法让主线程少干活。浏览器的主线程既要执行JavaScript、又要做样式计算、布局、绘制任何一项超时用户就会感觉到卡。排查的时候按住一个原则把主线程上每个任务的耗时放大来看哪个函数占的时长最多哪个就是优化目标。4.2 长列表与大数据的渲染难题渲染几百行表格、几千条聊天记录、几万个点的图表是前端性能优化的经典场景。直接把这些数据全部渲染到DOM上不卡才奇怪每多一个DOM节点浏览器在布局、绘制、事件绑定上的成本都会增加页面内存和样式计算量呈指数级上升。通用的解法是虚拟滚动核心思路是只渲染可视区域内的节点滚动时动态替换。比如一个列表有5万条数据页面可视区域只能显示20条虚拟滚动就只渲染20条左右的节点滚动的过程中不断销毁旧的、新建新的。这个方案的性能收益非常明显但同时会带来一个代价滚动条的滚动行为需要自己模拟因为整个列表的高度是撑出来的但实际DOM节点只有可视区那几行。好一点的虚拟滚动组件会处理好这个细节但项目接入时还是要注意滚动容器的尺寸和边距计算否则会出现滚动条抖动、数据空白的怪问题。如果只是展示用、不需要高频交互还有一种更简单的思路分页或分批加载。滚动到底部时加载下一页数据这种做法虽然“土”但在很多业务场景里比虚拟滚动更可靠尤其适合列表项高度不固定、每个项里面有复杂图文的场景。我在实际项目中遇到过为了追求技术先进强行对高度不固定的列表做虚拟滚动结果各种边界问题层出不穷最后换成分批加载页面稳定了性能也达标了。方案选型一定要贴合业务数据特征。4.3 动画、合成层与硬件加速CSS动画和JavaScript动画的性能差距很大。一个常见的误区是“JS动画比CSS动画功能强所以都用JS”其实浏览器对CSS动画有充分的优化空间比如动画可以跑在合成线程上不占用主线程而JavaScript动画每一步都在主线程里执行只要主线程忙动画就会卡。所以我的建议很明确能用CSS实现的动画就交给CSS比如位移、缩放、旋转、透明度变化这些都是常见动画类型而且大部分框架已经做了CSS动画优先的处理。JavaScript动画只用于那些确实需要精确控制的场景比如图表中的补间动画、拖拽类的交互。如果要进一步做硬件加速可以用transform: translateZ(0)或will-change: transform让元素提升到合成层让动画在合成线程运行。但合成层不是越多越好每个提升到合成层的元素都会占用独立的内存纹理移动端内存吃紧大量合成层反而会造成性能下降甚至崩溃。我的经验是只在真正有动画的容器上使用用完及时消除不要为了“流畅”给所有元素都加。5. 图片优化包袱最重但也最出效果5.1 格式选型WebP、AVIF与传统格式图片通常占据页面总传输体积的很大比例甚至很多门户类页面首屏有一半以上的字节都花在图片上。图片的优化方向主要是三个角度格式、尺寸、加载方式。格式方面JPEG适合照片类内容PNG适合需要透明背景的图形但两者的压缩效率都不算高。WebP在同等画质下通常能比JPEG小25%到34%而且支持透明是目前兼容性和效果最平衡的方案。AVIF压缩率更高但编码速度偏慢、兼容性略差适合对体积极其敏感的纯内部系统或工具类页面。另外SVG是矢量格式适合图标、Logo这类简单图形体积小、无限缩放不模糊但复杂图像不要用SVG渲染成本高得离谱。实际落地时我建议用构建插件或图像处理服务自动生成多格式图片并配合picture标签按浏览器支持情况选择格式。这样用户什么浏览器就拿到什么格式不存在兼容问题代码也不用手工维护。同时要注意图片质量参数的设置很多图像处理工具默认质量是100对体验提升微乎其微却白白多占了几十倍体积。我一般在保证视觉无差异的情况下把质量压到75到85之间肉眼基本看不出来体积却能少一大截。5.2 响应式图片与懒加载的正确姿势图片并不是“显示多大就加载多大”这么简单。一个常见的优化点是响应式图片同一个图片在手机屏幕上可能只需要400px宽在桌面端需要1000px宽。如果都给它加载一张2000px宽的原图移动端用户浪费了大量流量速度必然受影响。方案很简单通过srcset和sizes属性给不同视口指定不同尺寸的图片让浏览器自己选择合适的加载。懒加载延迟加载则是给非首屏图片使用的。页面可视区域之外的图片用户不滚到那个位置就不加载这样可以减少首屏的网络请求量。现代浏览器原生支持loadinglazy属性不需要任何JavaScript代码就能实现。但要注意一点首屏内、用户一定会立刻看到的图片不要加懒加载加了反而会延迟加载时机造成首屏图片闪现空白。图片优化里还有一个经常被忽视的参数是解码方式。给图片设置异步解码可以让图片解码不阻塞主线程的渲染在某些低端设备上能明显改善卡顿代价是图片可能出现短暂模糊但一帧两帧的模糊完全可以用性能换。另外CSS背景图片没有原生懒加载能力如果页面引用了大量背景图可以通过IntersectionObserver观察元素进入视口后再动态设置背景图这是一种相对麻烦但确实有效的手段。6. 性能监控与排查把优化变成可持续的习惯6.1 用自动化测试快速定位方向工具推荐的话我首推浏览器自带的性能审计工具。它能自动跑一轮测试输出性能、可访问性、最佳实践、SEO几个维度的评分并给出每个性能指标的详细数据。它的价值不只是打分而是每条分数旁边都带了解释和优化建议对刚开始接触性能优化的同学来说它其实就是一本进阶教材。我习惯的做法是把审计工具接入自动化流程每次代码合入之前跑一轮定一个基线分数比如性能评分不低于85分。低于就会在流水线里拦下把性能问题和代码错误放在同一优先级。这样优化不是一次性的事而是一个持续的习惯效果会随版本迭代慢慢累积。需要注意审计工具跑出来的分数受环境网络波动影响尤其是移动端模拟数据可能不稳定判断趋势比看单次分数更靠谱。6.2 浏览器调试工具里的性能面板如果审计告诉你是哪方面慢那性能面板就是告诉你怎么慢的地方。性能面板可以录制一段页面交互过程然后展示一段如同火焰图般的任务时间轴每条横杠代表一个任务横杠越长这个任务耗时越长。我排查卡顿类问题的标准流程是打开性能面板录制用户反馈的操作比如点击某个按钮、滚动页面停止录制后看主线程火焰图。找出那些明显比其他横杠长出一大截的任务点击进去就能看到它具体是哪个函数、哪一行代码导致的。这种定位方式比看完代码猜快得多。还有一个容易被忽略的检查点是网络面板里的瀑布流图。它会清晰展示每个资源是何时发现、何时开始下载、下载了多久。我在实际项目里用瀑布流发现过很多奇怪问题某个接口在瀑布流里出现了两次、某张图片是阻塞加载导致后续脚本全部被推后这些单看代码是看不出来的。6.3 常见性能问题速查表下面这张表是我在实际项目中高频遇到的性能问题、定位方法和处理方案整理成速查形式遇到同类问题可以直接对照排查。现象常见原因定位手段处理方案首屏白屏时间长首屏加载了过大的JavaScript包打包产物分析、Lighthouse路由级代码分割、按需加载页面滚动卡顿布局抖动或重排频繁性能面板火焰图读写分离、批量DOM更新图片加载后闪现图片未指定宽高或懒加载误用浏览器调试工具覆盖检查给图片设置宽高、首屏不加懒加载高流量下接口响应慢动态资源无缓存或缓存策略不合理网络面板查看缓存命中情况设置合理的缓存策略与回源策略低端设备CPU占用高过度使用合成层或复杂CSS选择器性能面板录制分析减少will-change滥用、优化选择器动画掉帧大量主线程任务阻塞帧率图表改用CSS动画或合成层静态资源变化后用户拿到旧版缓存策略和版本号不匹配线上抓包对比文件名添加内容哈希长缓存配合发布6.4 上线后的真实性能监控性能优化不是改完代码就结束了。用户的真实网络环境、设备性能千差万别开发环境里的优化效果和线上相差可能很大所以上线后一定要做真实环境监控。我的建议是至少在页面里接一个简单的性能统计脚本把关键的几个指标数据上报到监控平台。这样你能看到真实用户的首屏耗时分布、哪些地域、哪些网络状态下性能最差也就不用再靠用户投诉来感知性能问题了。现在主流的监控平台都有开放接入方案一个脚本、几个配置就能跑起来成本很低。还有一个心得性能优化是持续演进的过程不是一次上线就万事大吉。你在某个版本里优化的点很可能在下一个业务迭代里被某个新功能破坏。这也是为什么我一直强调要把性能得分、关键指标接入自动化流程的原因——只有让性能检查成为常态优化成果才能保持得住。写到这里我把前端性能优化从定位、网络、构建、运行时、图片到监控的完整链路过了一遍。根据我个人在项目里反复踩坑的经验最想强调的一点是性能优化永远先分析后动手。没有做定位分析就直接优化你大概率是在用战术上的勤奋掩盖战略上的懒惰。把核心指标量化出来、把瓶颈定位准确再选择对应的优化手段整个过程就变成了一道可推理的工程题而不是靠运气的动作。最后一个小建议从你的项目里挑一个用户反馈最多的页面先跑一轮审计、再看一次火焰图动手前把当前指标记下来你会发现改起来特别有方向感也会更有信心。