大致整理了一下常见的前端性能优化内容。类似一个大纲,用于自查。
通用优化
从web资源加载角度进行优化
dns缓存
浏览器在通过加载资源时,需要先进行dns域名解析,将其转换为具体的ip地址,这一步通常来说浏览器和系统都会自行对dns解析进行缓存,一般缓存都有一个TTL“有效期”,这一步你通常不需要做任何事情,不过如果碰上了dns解析相关的一些问题,在chrome中也可以通过chrome://net-internals/#dns来查看dns缓存。
不过你也可以通过 HTML 标签提前触发 DNS 解析,避免在用户点击链接时才开始解析:
1 | <!-- 预解析指定域名的 DNS --> |
资源缓存(强缓存、协商缓存)
通常我们的页面在请求一个静态资源比如js、css、图片时,可以为请求添加相应的缓存header头(例如cache-control),以便让浏览器可以根据一定的规则将资源进行缓存,这样浏览器下次再请求相同的资源时,可以直接使用缓存,而无需重复加载。
参考:浏览器缓存
CDN
CDN 即内容分发网络,是一种通过一组分布在不同地理位置的服务器来加速网页内容获取的技术架构。它会根据用户的地理位置、服务器负载等信息,使用性能最佳的边缘地址来解析 DNS 请求。
- 负载均衡:提高资源可用性,减轻源服务器压力
- 内容分发与缓存:就近原则,提高资源访问速度
http2
- http1是长连接,http2则是多路复用:通过二进制分帧,多个http请求可以在一个tcp中发起,解除了浏览器tcp连接数上限问题,使请求可以并行,并且解决了http队头阻塞问题
- 不过仍然没有解决tcp的队头阻塞:基于 QUIC 协议(UDP 之上的可靠传输协议)的http3
- 头部压缩:通过字典减少通用header头的数据传输量
- 服务器推送:识别并提前推送可能需要请求的资源,减少请求
- 请求优先级
资源压缩
- 请求资源时,使用请求头,来压缩资源并传输,减小传输体积,例如gzip和Brotli
- js、css等静态资源在打包时的压缩
- 图片、字体压缩,优先使用体积更小的资源格式,例如图片的webp/avif,字体使用woff2等
- 更小的资源转换为base64,因为资源太小,压缩收益不高,并且单独作为http发起一个请求反而成本更高
preload、prefetch
资源预加载:
- preload:重要资源,高优先级,优先加载(优先级高于普通资源加载)
- prefetch:后续可能用到的资源,当前页面加载完成并空闲时,提前加载该资源
懒加载
按需加载,提高页面性能
- 图片懒加载、数据分页
- 路由懒加载
从web整个渲染链路进行优化
参考:Chrome 网页渲染
导航与资源层面
- Service Worker 缓存: 通过缓存命中减少网络请求与延迟,离线/弱网更稳定。
解析阶段(HTML/CSS/JS)脚本加载策略:
- 使用 defer:不阻塞DOM解析,DOM 完成后、DOMContentLoaded 前按顺序执行。
- 使用 async:不阻塞DOM解析,加载即执行,互不等待。例如第三方统计、广告等
- 普通同步脚本尽量放在 body 底部(通用 HTML 场景)。
- 动态脚本默认异步;需顺序时设 script.async = false。
CSS 优化:
- 让关键 CSS 尽快可用(如预加载关键 CSS),减少阻塞脚本执行的时间。
渲染与合成(Pipeline)合成优先的动画:
- 优先使用只涉及合成的属性(如 transform、opacity),避免触发布局/重绘的动画。
- 谨慎使用 will-change 仅在必要处提示分层。
JavaScript 与任务调度,避免长任务阻塞主线程
- 将重计算放入 Web Worker。
- 将大任务切片;逐帧更新用 requestAnimationFrame。
- 低优先级、可延后的任务用 requestIdleCallback。
- 避免用 setTimeout 驱动动画(不与帧对齐、易丢帧)。
避免强制回流(布局抖动)
- 分离“读”和“写”:不要在同一任务中交替读写布局信息。
- 批量读取再批量写入,减少同步布局计算与回流。
选择低成本属性:
- 位置变化尽量用 transform 替代 top/left,降低触发布局与重绘的概率。
合理选择初始化时机:
- 了解 DOMContentLoaded(解析与同步脚本完成)与 load(所有资源完成)的差异,避免不必要地等到 load 才执行关键逻辑。
图片加载优化
- 手机适配清晰度: 根据屏幕大小自动加载合适的图片,即响应式加载图片(srcset+size属性)(picture + source标签)
- 基本的图片压缩可以用: tinypng, webpack工具
- 零散小图: 雪碧图, Data URL(base64), 字体图标
- 图片类型: webp, svg
- 播放动画: apng, svga
- 加载方式: 懒加载, 预加载
- CDN除了缓存资源以外,还提供了很多额外处理工具,例如图片转换
- 结合一些工具方案加载合适当前环境的图片资源
从应用构建角度优化
资源分割
资源分割是指在web打包过程中,将一些相对静态、基本不会变的资源(例如第三方js库)打包为一个资源,提高其缓存命中率,这样后续再web版本更新时,提高用户的二次访问的速度。
因为现代的web资源打包后,其文件名后面会携带文件的hash,如果文件内容没有变,则资源的url地址就不会变,再结合http的缓存请求头,这样资源的缓存命中率就会很高。
Tree-Shaking
Tree-Shaking是指在web打包过程中,将实际未使用到的代码给剔除,减少代码体积,以此来提高性能。这基本上是基于esmodule模块化的静态分析能力来实现。
应用性能优化
白屏优化
针对单页面应用首页白屏优化。
ssr
服务端渲染:在服务端请求数据并渲染出首屏html返回给客户端,然后客户端在通过水合接管web客户端后续的功能
ssg
静态资源生成:对于一些很少变化的页面内容,通过直接生成静态的html资源(通用html)并提供给客户端访问,来解决单页面应用白屏问题。
其他
利用骨架屏,让用户感知页面正在加载中,从用户体验角度来优化单页面应用白屏问题。
离线包
通过在客户端缓存前端web应用资源的方式,提高web应用加载速度或者弱网/断网情况下web应用的可访问性。
- app或者客户端应用,可以缓存web资源或者接口数据,再通过请求代理的方式来提高web资源加载速度,并且通过后台的延迟更新来保证应用或者数据的更新。
- 普通web应用可以通过service worker来实现
数据性能优化
- 数据分页加载
- 数据缓存:通过service worker缓存接口数据(离线可用、动态数据缓存)
- 虚拟滚动:针对大数据内容的展示,使用虚拟滚动来提高应用性能
动画性能
- 优先使用只涉及合成的属性(如 transform、opacity)来实现动画,可以借用3D 硬件加速来提高性能,避免触发布局/重绘的动画。
- 即使是使用的脱离文档流的属性实现的动画(例如left、top),虽然比影响大量元素的布局动画(如修改 margin 导致兄弟元素移位)性能更好,但仍远不如 transform 动画高效,因为布局阶段仍然会触发,且会占用主线程,存在固有成本
- 避免动画元素的布局抖动:例如在动画逻辑中访问offsetWidth、getBoundingClientRect()并立即修改元素布局(top、left、class等)时会触发浏览器 “强制同步布局”,导致性能卡顿。参考:Chrome 网页渲染
- 如果非要访问offsetWidth、getBoundingClientRect(),那么需要注意,在访问这些方法或者属性之前,是否存在新的dom布局更新。可以考虑批量读,再批量写。
js性能
- 防止阻塞主进程:requestanimationframe和requestidlecallback
- 长任务使用worker
- 减少重排(Reflow)与重绘(Repaint):优化dom操作
- 防抖节流
- 数据缓存:dom缓存、结果缓存
- 事件委托
这个可能需要具体问题具体分析。
框架性能
react
针对hooks函数组件。
防止不必要的计算
- useMemo:使用useMemo来缓存某个计算函数的执行结果,仅当依赖的数据源变更时,计算函数才会被重新执行。尤其是那种复杂的计算。
防止不必要的渲染
默认情况下,当父组件重渲染时,会重新执行子组件函数,无论子组件的 props 是否 “实际变化”,子组件都会被重新调用(因为它本身就是一个函数),这可能带来额外的不要的组件渲染。
- useCallback:和useMemo类似,只不过其针对的是函数,仅当依赖的数据源变更时,useCallback返回的才是一个新的函数,否则useCallback返回的仍然是之前的函数。
- 避免给组件传递的props中直接使用对象字面量或者匿名箭头函数,因为这会导致组件每次渲染时,会创建一个新的类型引用
- React.memo:用来包裹函数组件,仅当props变化时才会重新渲染,它仅针对引用该组件的父组件重新渲染时的场景,它属于“被动渲染的场景”,而自身state或者全局状态变更属于主动触发重新渲染,不受React.memo的影响
不过最近react出了个编译工具:React Compiler,他可以帮你分析代码并使用useMemo、useCallback 和 React.memo来进行优化,有兴趣可以试试。
合理拆分组件和状态
- 合理拆分组件的细粒度,避免局部变化导致整个组件整体重新渲染,尤其是对于大组件来说
- 注意状态影响的范围,特别是多个组件依赖一个大的全局状态的不同部分数据,由于react状态更新的特性或者使用了不可变数据,那么在该状态更新时(即使只更新了一个字段),那么引用该状态的所有组件都会触发渲染更新。
- 将大状态拆分为合适的小状态
- 利用状态管理库的选择器来让组件精确订阅状态自己需要的那一部分数据。当且仅当订阅的那部分数据变化时,才会触发组件的更新。
虚拟dom和列表优化
- 使用合适的key,来优化列表渲染更新和diff算法效率
- 大数据列表使用虚拟滚动来仅渲染可视区的内容,避免dom数量过多导致的卡顿
代码分割和资源加载
- 使用React.lazy实现路由级别的组件懒加载
- 配合Suspense提高懒加载的体验
- 对于非首屏的大型组件,考虑按需加载
vue
参考:
大部分react中的性能优化都可以应用于vue中,只不过API之类的可能有差异。
vue:防止不必要的渲染
vue中的父组件重新渲染时,其子组件也会重新渲染,只不过和react不同的是,vue会对子组件进行一个“更新检查”:检测子组件的依赖的props、事件、插槽和全局状态之类的数据没有变化,同时也会检查自身的自身依赖的全局状态 / 上下文是否变化,如果都没有变化,则会跳过子组件的重新渲染,复用之前的子组件。
props的对象字面量和内嵌箭头函数可能会导致组件触发不必要的重新渲染,这个和react类似,默认情况下,组件的更新除了自身依赖的状态变更会触发重新渲染外,如果组件的props变化了也会触发组件的重新渲染,而如果你在传递props使用的对象字面量或者为组件绑定事件使用的模板中的箭头函数,那么由于父组件更新时,每次创建的对象字面量和模板箭头函数都是一个新的引用,会导致引用的子组件无法通过props和事件的更新检查,从而产生不必要的重新渲染:
1 | <!-- 父组件:对象字面量 --> |
大数据性能
- 长列表使用虚拟滚动
- 大数据使用不可变或者shallowRef() 和 shallowReactive()来减少响应式性能开销
如何分析性能
网页web性能:
- 确认性能问题和现象是什么
- 结合工具确认性能点是什么
- network面板确认是否是接口性能
- performance
- 确认资源请求时长(是否并行、是否请求时间过长)
- 网页加载节点:Nav、DCL时间(DOMContentLoaded事件)、FP、FCP、LCP时间点和onLoad(L标志)等
- 确定主线程中是否有任务执行过长(例如js)导致主线程阻塞
- 动画性能:帧率
- chrome Lighthouse跑分,评估网站性能
统计性能
- 使用performance API来统计前端的性能指标:包括首屏渲染时长
- 通常其时间点都是基于performance中的navigationStart(网页跳转时间点)
- Web Vitals API(web-vitals库):自动采集并计算 Google 定义的 Core Web Vitals(核心网页指标)LCP、FID、CLS等
性能上报
使用图片或者Beacon Api(主流浏览器可用)上报性能数据
前端性能优化思考
- 不要过早优化:过早优化是万恶之源,注意,这里和性能评估是不一样的考虑。
- 性能优化存在着多个方面、多个角度,每个角度都存在着一些优化点。你需要知道这些优化方案是干什么的,优化的是哪方面的内容,能达到什么效果,最后才是考虑如何实现。
- 确定问题,对症下药:不要一开始上各种优化手段,没有针对性就直接去应用,往往会事倍功半(这里更多指那种比较大的优化方案,例如离线包、ssr、service worker这种)。更好的方式是定位到应用真正的性能瓶颈,需要对其进行调试,找到问题原因,最终评估方案和复杂度去选择。当然,常见的性能优化注意事项和技巧这种可以直接实际应用就好。