一、前言
在日常需求中,开发者只要能保证静态、动效、功能、埋点都正确实现,再把需求和接口文档认真理理就已经称得上信赖的好同事了。但对于性能优化,我们通常只是依赖零散的经验,如偶尔的图片压缩、点击的防抖节流这种散点式手段。本文旨在把常见的前端性能优化的手段归类整理,画大图,供大家在自己的自测流程中参考。
对于性能优化,并不是说有了这张大图,就不管开发什么项目都对着来一遍。优化是有场景与前提的,「乱优化不如不优化」。因此在优化前,有几点要注意:
01 | 依据数字而不是猜想
尽管有些开发者能凭直觉找出性能瓶颈,这并非理想做法。
正确的流程是首先进行性能分析,再根据结果的优先级进行优化。同时警惕优化措施可能带来的新问题,要用性能数据验证优化效果,而非依赖猜测。
02 | 个体数据不足信
有时候,你可能会发现某个著名网站加载异常缓慢,耗时许多秒。但仅凭单次体验就认定其"慢"是不科学的,性能评估也容易犯同样的错误。
因为个别请求的数据并不足以代表整体,响应时间会因用户环境和网络条件的不同而有所差异。更客观的方法是分析统计数据,包括平均响应时间、吞吐量(TP 值)和响应时间的分布情况等,以便全面评估性能表现。
03 | 不要过早优化和过度优化
"过早的优化是万恶之源" ------ Donald Knuth(计算机科学家)
无需过分投入于微不足道的提升。我们应该将精力集中在最关键的性能瓶颈上,而不是偏离目标进行非关键优化。
通常,极致优化的代码可读性较差,结构也可能妥协,导致维护困难。过早优化可能将这些问题带入项目,增加后期的重构难度。最佳实践是,将性能优化作为独立的阶段,在项目架构和功能基本稳定后再进行。以确保优化工作既高效又有针对性。
04 | 保持良好的编码习惯
因为开发与优化是两个独立的阶段,所以遵循良好的编码规范有助于日后的代码重构
二、性能指标
既然是优化,肯定不能通过体感上的臆测,所以需要有明确的指标来判断页面的性能是好是坏。
这些指标可以根据业务和需求自定义,但常用指标有现成的,所以普遍做法还是使用行业内都认可的指标值。根据 Lighthouse 10 规则,Web 前端性能指标主要有 5 个:
性能指标 | 权重 |
---|---|
Speed Index (速度指数) | 10% |
First Contentful Paint (FCP-首次内容绘制) | 10% |
Cumulative Layout Shift (CLS-累积布局偏移) | 25% |
Largest Contentful Paint (LCP-最大内容绘制) | 25% |
Total Blocking Time (TBT-总阻塞时间) | 30% |
01 | Speed Index
速度指数(Speed Index)并不是一个具体的时间点,而是一个综合性指标,表示页面从加载开始到页面内容基本可见的过程中,用户感受到的加载速度。
形象点描述,你打开一个网页,内容开始一点点地加载显示。一开始,可能只有一部分内容,比如顶部的导航栏,之后是一些文本,然后是图片等。速度指数就是用来衡量这个 "填充"过程的快慢。
- 如果网页内容很快就完全显示出来,速度指数就会很低,这意味着用户体验较好。
- 反之,如果内容慢慢地、逐渐出现,速度指数就会很高,这通常意味着用户体验较差。
小于 3.4s 为优秀
02 | FCP
首次内容绘制时间(First Contentful Paint ),用来衡量从用户开始打开网页到浏览器渲染出第一块内容(如文本、图像、SVG等)的时间。
如上图,FCP发生在第二帧,页面从什么也没有,到有任意元素渲染出的时间。
但其实描述不够准确,FCP 还包括一些额外的时间成本,比如:
- 上一个页面的所有卸载时间:如果用户从一个页面跳转到另一个页面,那么浏览器需要先卸载当前页面,这个卸载过程会花费一定的时间。
- 连接设置时间:浏览器与服务器建立连接所需的时间,包括DNS解析、TCP握手以及TLS协商(如果是HTTPS连接)。
- 重定向时间:如果请求的页面经历了一次或多次HTTP重定向,那么每次重定向都会额外增加时间。
小于 1.8s 为优秀
03 | CLS
累积布局偏移(Cumulative Layout Shift),衡量网页在加载过程中视觉稳定性的一个性能指标
CLS的计算方式如下
- CLS评分是基于每个不期望的布局偏移事件发生的影响范围和移动距离。
- 对于每个偏移事件,计算其影响分数(impact fraction)和移动分数(distance fraction)。
- 影响分数是指视口中被影响内容的比例。
- 移动分数是指移动的内容相对于视口的移动距离。
- 两者相乘得到每次偏移的评分,所有偏移的评分累加起来即为页面的CLS评分。
小于 0.1 为优秀
04 | LCP
最大内容绘制(Largest Contentful Paint,简称LCP)记录了页面中最大元素(可能是一张图像或者一段文本)显示在屏幕上所需的时间。这个指标的意义在于,当这个最大元素加载完成后,用户通常会认为页面基本上已经加载完成,即使背后可能还有其他的内容和脚本在继续加载和执行。
LCP的计算方式如下
- 页面开始加载时,浏览器监测所有内容的显示。
- 每次有新内容显示时,浏览器会检查其大小,并判断是否是目前为止最大的元素。
- 在页面加载过程中,对最大元素的判断可能在不断更新。
- 当用户能与页面交互或页面停止加载新的视觉内容,最大的元素就认为确定了,这个时间点就是LCP。
举个例子,如果一个大广告横幅突然在页面中插入并推动其他内容向下移动,这将会产生一个较大的偏移评分,增加页面的CLS。
小于 2.5s 为优秀
05 | TBT
总阻塞时间(Total Blocking Time),越小越好。是一个用来衡量页面在加载过程中可交互性的性能指标。它反映了页面从开始加载到成为完全可交互的过程中,主线程被长任务(在主线程上运行超过 50ms 的任务)阻塞的总时长。
TBT的计算方式如下
- 在首次内容绘制(FCP)之后和完全可交互时间(TTI)之前,每个长任务的执行时间超过50毫秒的部分都会被计算为阻塞时间。
- 对所有这些长任务的超时部分进行累加,即得到总的阻塞时间。
上述时间轴包含 5 个任务,其中 3 个是长时间运行的任务,因为它们的时长超过 50 毫秒。下图显示了每个耗时较长的任务的阻塞时间:
在主线程上运行任务所花费的总时间为 560 毫秒,但其中只有 345 毫秒被认为是阻塞时间。
小于 200 为优秀
三、通用方法
了解优化的正确时机和关键指标后,下一步是掌握常用的前端性能优化策略。
这些策略是通用的,并不专门针对任何单一的性能指标。性能优化不只涉及技术层面的改进,还涉及用户的感知体验。如果把性能优化过程看作是一套标准操作流程(SOP),那么本节内容可以视为 SOP 的起点。
01 | 资源压缩
本质就是在不过分影响用户体验和功能完整性的前提下,让被请求的资源足够小。
a. 代码资源
Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具,这种类型的压缩主要针对文本文件和代码,目的是减小它们的体积,加快传输速度,并减少浏览器解析和执行的时间。
- 压缩JavaScript: Webpack 通过使用如 Terser 这样的插件,移除 JavaScript 中的无用代码、注释、空格和换行符,以及对代码进行缩短变量名和函数名等操作以减少文件大小,提高加载速度。
- 压缩CSS: Webpack 可以利用 cssnano、OptimizeCSSAssetsPlugin 等插件来优化和最小化 CSS 代码。
- 模块优化: Webpack 可以进行 Tree shaking 来剔除项目中未引用的模块。
b. 媒体资源
媒体资源压缩针对的是图像、音频和视频等,这类资源通常是网页内容中体积最大的部分,优化它们对于提升页面加载性能至关重要。但视觉效果和文件大小要做好 trade off ,不能一味追求压缩导致资源糊化。
- 图像压缩: 通过缩小尺寸、降低分辨率、改变图像格式(如使用 JPEG、PNG、WebP 等),以及利用工具如 imagemin 对图像文件进行无损或有损压缩。
- 视频压缩: 调整视频的编码参数,如比特率、分辨率,或转换为更高效的视频格式(如H.264、H.265或VP9)来减少视频文件的大小。
- 音频压缩: 通过更改音频格式(如MP3、AAC等),减少比特率来降低音频文件的体积。
- 字体压缩: 全量字体包很大,所以具体需要哪些文案利用类似 font-spider 的工具裁剪后引入。
02 | 网络优化
网络传输怎么优化?本质就是减少请求、减少建立连接的时间、提高连接效率和稳定性。
a. 连接优化
- 使用CDN (内容分发网络)
- 地理位置优化:CDN 通过在世界各地分布的节点缓存静态资源,减少数据传输的物理距离和延迟。
- 负载均衡:CDN 提供负载均衡,能够在多台服务器之间分配请求,避免单个服务器的过载。
- DNS预解析
- 使用
dns-prefetch
和preconnect
来减少 DNS 查找时间和连接建立时间。如果你的网页需要加载第三方资源,如字体、脚本、分析工具或广告,可以预解析这些资源的域名。 - 预解析过多的域名可能会消耗用户设备上的资源
- 避免重定向
- 重定向会导致额外的HTTP请求,增加页面加载时间。需确保网站链接指向最新的、正确的地址,避免链式重定向。
- 启用HTTP/2或HTTP/3
利用新协议提供的多路复用、服务器推送和头部压缩等特性。但在实际优化中,需考虑客户端和服务器的兼容性。
HTTP/2的优化逻辑
- 多路复用(Multiplexing):减少TCP握手次数
- 在HTTP/1.x中,每个请求/响应往往需要一个单独的TCP连接,导致浏览器并行请求的数量受限,且有可能导致队头阻塞问题。
- HTTP/2允许在单个连接上并行传输多个请求和响应,有效消除了队头阻塞,并降低了同时打开多个TCP连接的开销。
- 头部压缩(Header Compression)
- HTTP/1.x中的每个请求都会携带重复的头部信息,造成不必要的数据传输。
- HTTP/2利用HPACK压缩算法,减少了这些信息的冗余和大小。
- 服务器推送(Server Push)
- HTTP/1.x中,客户端需要显式请求所有资源。
- HTTP/2允许服务器提前发送客户端可能需要的资源,减少往返时间(RTT)和延迟。
HTTP/3的优化逻辑
- 基于QUIC协议
- HTTP/2虽然解决了多路复用和头部压缩问题,但仍然依赖于TCP协议,而TCP在建立连接时需要进行三次握手,这在网络环境较差时会造成显著的延迟。
- HTTP/3是基于QUIC协议的,QUIC基于UDP,能更快建立连接,甚至实现零RTT连接恢复。
- 流优先级
- HTTP/2允许设置流的优先级,但是这在TCP的单一队列中仍然可能导致某些资源的阻塞。
- HTTP/3允许更精细的流控制和优先级设置,因为它在QUIC层面就支持多流并行传输,避免了不同流之间相互影响。
b. 缓存优化
利用用户本地的浏览器缓存来存储已下载的资源,避免在每次访问时重新下载相同的资源。
正确配置和使用缓存可以带来显著的性能提升,但如果管理不当,也可能导致用户看到过时的内容。因此,重要的是要制定合理的缓存策略,并定期评估其有效性。
-
Cache-Control:这是一个 HTTP 响应头,可以指定资源被缓存的方式和时间。例如,Cache-Control: max-age=31536000 表示资源可以在本地缓存并在请求时重用一年。
-
Expires:这是早期HTTP/1.0的缓存控制头,用于设置资源过期的绝对时间。现在通常推荐使用 Cache-Control 替代它。
-
ETag/Last-Modified:这些响应头用于验证缓存的资源是否仍然是最新的。当资源变化时,服务器会提供更新的 ETag 或 Last-Modified 值,以此来触发资源的重新下载。
03 | 代码优化
- 预加载(Preloading) :对于核心资源,使用
<link rel="preload">
提前加载。 - 懒加载(Lazy Loading) :对于非关键资源推迟加载,如下方图片、非首屏的 JavaScript 模块等。可以减少初始加载的资源数量,加快页面加载速度。
- 实现方式:
- JavaScript 监听滚动事件,当元素进入视口时,动态地将资源的URL设置到元素的
src
属性中。 - 或利用现成的懒加载库(如
lozad.js
、lazysizes
等)来实现懒加载效果
- JavaScript 监听滚动事件,当元素进入视口时,动态地将资源的URL设置到元素的
- 适用场景:
- 图像/视频 懒加载
- 无限滚动信息流
- 单页面应用,按需加载额外的JavaScript代码或模块
- 实现方式:
- 减少重绘与重排,这哥俩是浏览器渲染过程中最耗费性能的操作之二。
- 正确使用防抖节流,处理页面上频繁触发的事件时(如滚动、窗口大小调整、键盘输入等),善用防抖节流能够减少事件处理函数被调用的次数,减少不必要计算和重绘,提升性能。
- 保持良好编码习惯,便于功能上线后性能优化重构。
- ... 具体情况,具体分析
04 | 埋点监控
无需过分投入于微不足道的提升。我们应该将精力集中在最关键的性能瓶颈上。
建立一个埋点监控系统,能够让开发者,让团队更有针对性地发现并解决影响用户体验和业务成功的关键性能问题。大厂一般有现成产品,小厂可能基建需要自己搞,而开发监控系统需要注意以下几个环节:
a. 基于业务,自定义关键指标
技术是要服务业务的,那么除了标准的 Web 性能指标外,还需要根据具体业务需求定制个性化监控指标,比如:
- 白屏时间:
- 白屏时间是指用户从打开页面到页面开始有内容展示的时间。对于用户来讲,这是他们感受到页面响应的第一个指标。过长的白屏时间会让用户感觉到等待,影响用户体验。
- 首屏时间:
- 首屏时间是指用户打开页面到首屏内容渲染完成的时间。这个指标对于移动端尤其重要,因为移动用户往往对加载速度更为敏感。
- 用户可交互时间 (TTI - Time to Interactive) :
- TTI指的是页面从开始加载到主要子资源完成加载,并且能够快速响应用户输入的时间。用户可交互时间对于用户来说是一个非常直观的体验指标。
- 页面完全加载时间:
- 页面完全加载时间是指从用户发起请求到页面上所有的元素(包括图片、脚本等)完全加载完成的时间。
- 自定义事件完成时间:
- 你可能想要监控用户完成某个特定操作的时间,例如一个复杂表单的提交时间,或者一个视频内容的缓冲时间。
- 错误率:
- 页面的JavaScript错误率、API调用错误率或者资源加载失败率等,都是影响用户体验的重要因素。
b. 实现采集方法
有了指标,那么就需要制定采集方式:
- 在代码的关键路径中插入埋点方法,以捕获性能数据和错误信息。
- 使用浏览器 API,如 Performance API、Error Try...Catch等事件监听
c. 数据上报与收集
有了采集方式,还需要数据上报与收集。
- 需要设计一个 SQL 表存埋点数据,以最小的性能影响来捕获和上报数据。
- 使用批处理和节流技术来优化数据上报频率,减少对网络的影响。
d. 可视化分析
开发一个可视化(ECharts)面板,展示实时的性能数据和错误日志。
- 为了方便快速定位问题,应该确保监控数据包含足够的上下文信息,如堆栈跟踪、用户代理信息和发生错误时的操作序列
- 利用源码映射(Source Maps)帮助开发者快速定位到压缩、混淆代码中的错误位置。
e. 警报与通知
设定性能阈值,当监控到的指标超过这些阈值时,系统会自动发出警报。通过邮件、短信、群消息等工具及时通知团队成员。
上面的描述只是一个监控系统应该有的基础功能,更多细节不再赘述。
这里留一个 todolist :后续写一篇 《从 0 到 1 实现一个前端埋点监控系统》
五、总结
本文只是以手册的形式整理对于任意网页,前端侧的通用性能优化方案,并不涉及具体代码实现。
性能优化也不是一个统一的、适用于所有项目的模板过程,而应该是一个有针对性和基于上下文的行动。
盲目地进行优化没有实际的优化效果("乱优化不如不优化"),并且可能造成不必要的复杂性。