前端性能优化指南
性能优化一直以来都是前端工程领域中的一个重要部分。网站应用的性能(加载速度、交互流畅度)优化对于提高用户留存、转化率等都有积极影响。可以理解为,提升你的网站性能,就是提升你的业务数据(甚至是业务收入)。
性能优化广义上会包含前端优化和后端优化。后端优化的关注点更多的时候是在增加资源利用率、降低资源成本以及提高稳定性上。相较于后端,前端的性能优化会更直接与用户的体验挂钩。从用户体验侧来说,前端服务 5s 的加载时间优化缩减 80%(1s) 与后端服务 50ms 的响应优化缩减 80%(10ms) 相比,用户的体验提升会更大。因此很多时候,与体验相关的性能的瓶颈会出现在前端。
目的
-
让页面加载的更快
-
对用户操作响应更及时,为用户带来更好的使用体验
-
减少请求,降低服务器负荷,节省资源
原则
-
建立性能优化意识
-
目标:比你最快的竞争对手快至少20%
-
选择正确的指标
-
从具有代表性的用户使用的设备收集数据
-
深入理解业务
如何衡量
以用户为中心的性能指标:
1、哪些指标能够最准确的衡量用户所感受到的性能?
2、如何针对实际用户来衡量这些指标?
3、如何解读衡量结果以确定应用是否速度快?
4、了解应用的实际用户性能之后,如何避免性能下降并在未来提高性能?
基本概述(雅虎前端优化35条)
内容
1、尽量减少HTTP请求数
80%的终端用户响应时间都花在了前端上,其中大部分时间都在下载页面上的各种组件:图片,样式表,脚本等等。减少组件数必然能够减少页面提交的HTTP请求数,这是让页面更快的关键。
合并文件是通过把所有脚本放在一个文件中的方式来减少请求数的,当然,也可以合并所有的CSS。
2、减少DNS查找
3、避免重定向
4、让Ajax可缓存
5、延迟加载
6、预加载
7、减少DOM元素数量
8、划分到不同域名
9、尽量减少Iframe的使用
10、避免404错误
服务器
1、使用CDN
2、添加Expires或Cache-Control响应头,使用缓存
3、启用Gzip
4、配置Etag
5、尽早输出缓存
6、Ajax请求使用Get
7、避免src、href为空
Cookie
1、减少cookie使用
2、静态资源使用无cookie域名
CSS
1、把样式表放在head中
2、不要使用CSS表达式
3、使用代替@import
4、不要使用filter(已废弃,可用来解决IE老版本png背景透明问题)
Javascript
1、脚本放在页面底部
2、使用外部JS和CSS
3、压缩JS和CSS
4、移除重复脚本
5、减少DOM操作
6、使用高效的事件处理
图片
1、优化图片
2、使用CSS Sprite
3、不要在HTML中缩放图片
4、使用体积小、可缓存的favicon.ico
移动端
1、保证所有组件小于25K
2、打包内容为分段(multipart)文档
性能数据采集/监控
W3C Navigation Timing API的性能指标
W3C性能小组引入了 Navigation Timing API ,实现了自动、精准的页面性能打点;开发者可以通过 window.performance 属性获取。
performance.timing接口(定义了从navigationStart至loadEventEnd的 21 个只读属性)performance.navigation(定义了当前文档的导航信息,比如是重载还是向前向后等)
W3C Navigation Timing V2处理模型图:
从当前浏览器窗口卸载旧页面开始,到新页面加载完成,整个过程一共被切分为 9 个小块:提示卸载旧文档、重定向/卸载、应用缓存、DNS 解析、TCP 握手、HTTP 请求处理、HTTP 响应处理、DOM 处理、文档装载完成。每个小块的首尾、中间做事件分界,取 Unix 时间戳,两两事件之间计算时间差,从而获取中间过程的耗时(精确到毫秒级别)。
指标解读:
| 字段 | 说明 |
|---|---|
| navigationStart | 当前浏览器窗口的前一个网页关闭,发生unload事件时的Unix毫秒时间戳。如果没有前一个网页,则等于fetchStart属性 |
| unloadEventStart | 如果前一个网页与当前网页属于同一个域名,则返回前一个网页的unload事件发生时的Unix毫秒时间戳。如果没有前一个网页,或者之前的网页跳转不是在同一个域名内,则返回值为0 |
| unloadEventEnd | 如果前一个网页与当前网页属于同一个域名,则返回前一个网页unload事件的回调函数结束时的Unix毫秒时间戳。如果没有前一个网页,或者之前的网页跳转不是在同一个域名内,则返回值为0 |
| redirectStart | 返回第一个HTTP跳转开始时的Unix毫秒时间戳。如果没有跳转,或者不是同一个域名内部的跳转,则返回值为0 |
| redirectEnd | 返回最后一个HTTP跳转结束时(即跳转回应的最后一个字节接受完成时)的Unix毫秒时间戳。如果没有跳转,或者不是同一个域名内部的跳转,则返回值为0 |
| fetchStart | 返回浏览器准备使用HTTP请求读取文档时的Unix毫秒时间戳。该事件在网页查询本地缓存之前发生 |
| domainLookupStart | 返回域名查询开始时的Unix毫秒时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值 |
| domainLookupEnd | 返回域名查询结束时的Unix毫秒时间戳。如果使用持久连接,或者信息是从本地缓存获取的,则返回值等同于fetchStart属性的值 |
| connectStart | 返回HTTP请求开始向服务器发送时的Unix毫秒时间戳。如果使用持久连接(persistent connection),则返回值等同于fetchStart属性的值 |
| connectEnd | 返回浏览器与服务器之间的连接建立时的Unix毫秒时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束 |
| secureConnectionStart | 返回浏览器与服务器开始安全链接的握手时的Unix毫秒时间戳。如果当前网页不要求安全连接,则返回0 |
| requestStart | 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的Unix毫秒时间戳 |
| responseStart | 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳 |
| responseEnd | 返回浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的Unix毫秒时间戳 |
| domLoading | 返回当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的readystatechange事件触发时)的Unix毫秒时间戳 |
| domInteractive | 返回当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的Unix毫秒时间戳 |
| domContentLoadedEventStart | 返回当前网页DOMContentLoaded事件发生时(即DOM结构解析完毕、所有脚本开始运行时)的Unix毫秒时间戳 |
| domContentLoadedEventEnd | 返回当前网页所有需要执行的脚本执行完成时的Unix毫秒时间戳 |
| domComplete | 返回当前网页DOM结构生成时(即Document.readyState属性变为“complete”,以及相应的readystatechange事件发生时)的Unix毫秒时间戳 |
| loadEventStart | 返回当前网页load事件的回调函数开始时的Unix毫秒时间戳。如果该事件还没有发生,返回0 |
| loadEventEnd | 返回当前网页load事件的回调函数运行结束时的Unix毫秒时间戳。如果该事件还没有发生,返回0 |
关键性能指标
| 字段 | 描述 | 计算公式 | 备注 |
|---|---|---|---|
| First Meaningful Paint (FMP) | 首屏时间 | 无 | 无 |
| fpt | First Paint Time,首次渲染时间(白屏时间) | responseEnd - fetchStart | 从请求开始到浏览器开始解析第一批HTML文档字节的时间差 |
| tti | Time to Interact,首次可交互时间 | domInteractive - fetchStart | 浏览器完成所有HTML解析并且完成DOM构建,此时浏览器开始加载资源 |
| ready | HTML加载完成时间, 即DOM Ready时间 | domContentLoadEventEnd - fetchStart | 如果页面有同步执行的JS,则同步JS执行时间 = ready - tti |
| load | 页面完全加载时间 | loadEventStart - fetchStart | load = 首次渲染时间 + DOM解析耗时 + 同步JS执行 + 资源加载耗时 |
| firstbyte | 首包时间 | responseStart - domainLookupStart | 无 |
区间段耗时
| 字段 | 描述 | 计算公式 | |
|---|---|---|---|
| dns | DNS查询耗时 | domainLookupEnd - domainLookupStart | 无 |
| tcp | TCP连接耗时 | connectEnd - connectStart | 无 |
| ttfb | Time to First Byte(TTFB),请求响应耗时。 | responseStart - requestStart | TTFB有多种计算方式 |
| trans | 内容传输耗时 | responseEnd - responseStart | 无 |
| dom | DOM解析耗时 | domInteractive - responseEnd | 无 |
| res | 资源加载耗时 | loadEventStart - domContentLoadedEventEnd | 表示页面中的同步加载资源 |
| ssl | SSL安全连接耗时 | connectEnd - secureConnectionStart | 只在HTTPS下有效 |
注意点
-
通过window.performance.timing所获的的页面渲染所相关的数据,在单页应用中改变了url但不刷新页面的情况下是不会更新的。因此如果仅仅通过该api是无法获得每一个子路由所对应的页面渲染的时间。如果需要上报切换路由情况下每一个子页面重新render的时间,需要自定义上报。
-
通过window.performance.getEntries()所获取的资源加载和异步请求所相关的数据,在页面切换路由的时候会重新的计算,可以实现自动的上报。
SPA模式
Navigation Timing API可以监控大部分前端页面的性能。但随着SPA模式的盛行,类似Angular/Reac/Vuet等框架的普及,页面内容渲染的时机被改变了,W3C标准无法完全满足原来的监控意义。以Chrome为首的浏览器一直在推动以用户为中心的性能指标,并且逐步开放API。如lighthouse,Web Vitals等提供浏览器插件/命令行工具/NPM包。
关注点
| 体验 | 指标 |
|---|---|
| 是否发生 | 首次绘制(FP)首次内容绘制(FCP) |
| 是否有用 | 首次有效绘制(FMP)/主角元素计时 |
| 是否可用 | 可交互事件(TTI) |
| 是否令人愉快 | 耗时较长的任务(在技术上不存在耗时较长的任务) |
关键指标
| 指标 | 说明 |
|---|---|
| FP(First Paint) | 页面在导航后首次呈现出不同于导航前内容的时间点 |
| FCP(First Contentful Paint) | 首次绘制任何文本,图像,非空白canvas或SVG的时间点 |
| TTI(Time to Interactive) | 从页面开始加载到页面主要资源加载之间的时间 |
| LCP(Largest Contentful Paint) | 可视区域“内容”最大的可见元素开始出现在页面上的时间点 |
| CLS(Cumulative Layout Shift) | 表示用户经历的意外 layout 偏移的频率 |
| TBT(Total Blocking Time) | 表示从 FCP 到 TTI 之间,所有 long task 的阻塞时间之和 |
FP和FCP可能是相同的时间,也可能FP先于FCP。下图展示了 FP 和 FCP 的区别:

通过 window.performance.getEntriesByType('paint') 获取两个时间点的值。
performance.getEntriesByType('paint');

LCP可以通过 Chrome 的 PerformanceObserver API 计算它:

// Create a variable to hold the latest LCP value (since it can change).
let lcp;
// Create the PerformanceObserver instance.
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
// Update `lcp` to the latest value, using `renderTime` if it's available,
// otherwise using `loadTime`. (Note: `renderTime` may not be available if
// the element is an image and it's loaded cross-origin without the
// `Timing-Allow-Origin` header.)
lcp = lastEntry.renderTime || lastEntry.loadTime;
});
// Observe entries of type `largest-contentful-paint`, including buffered
// entries, i.e. entries that occurred before calling `observe()`.
po.observe({type: 'largest-contentful-paint', buffered: true});
// Send the latest LCP value to your analytics server once the user
// leaves the tab.
addEventListener('visibilitychange', function fn() {
if (lcp && document.visibilityState === 'hidden') {
console.log('LCP:', lcp);
removeEventListener('visibilitychange', fn, true);
}
}, true);
优化 FP/FCP
从文档的 <head> 中移除任何阻塞渲染的脚本或样式表,可以减少首次绘制和首次内容绘制前的等待时间。
花时间确定向用户指出“正在发生”所需的最小样式集,并将其内联到 <head> 中(或者使用 HTTP/2 服务器推送)),即可实现极短的首次绘制时间。
应用 shell 模式可以很好地说明如何针对渐进式网页应用实现这一点。
优化 FMP/TTI
确定页面上最关键的界面元素(主角元素)之后,您应确保初始脚本加载仅包含渲染这些元素并使其可交互所需的代码。
初始 JavaScript 软件包中所包含的任何与主角元素无关的代码都会延长可交互时间。 没有理由强迫用户设备下载并解析当前不需要的 JavaScript 代码。
一般来说,您应该尽可能缩短 FMP 与 TTI 之间的时间。 如果无法最大限度缩短此时间,界面绝对有必要明确指出页面尚不可交互。
对于用户来说,其中一种最令人失望的体验就是点按元素后毫无反应。
避免出现耗时较长的任务
拆分代码并按照优先顺序排列要加载的代码,不仅可以缩短页面可交互时间,还可以减少耗时较长的任务,然后即有希望减少输入延迟及慢速帧。
除了将代码拆分为多个单独的文件之外,您还可将大型同步代码块拆分为较小的块,以便以异步方式执行,或者推迟到下一空闲点。 以异步方式在较小的块中执行此逻辑,可在主线程中留出空间,供浏览器响应用户输入。
最后,您应确保测试第三方代码,并对任何低速运行的代码追责。 产生大量耗时较长任务的第三方广告或跟踪脚本对您业务的伤害大于帮助。
数据上报
使用的img标签get请求
- 不存在AJAX跨域问题,可做跨源的请求
- 很古老的标签,没有浏览器兼容性问题
var i = new Image();
i.onload = i.onerror = i.onabort = function () {
i = i.onload = i.onerror = i.onabort = null;
}
i.src = url;
navigator.sendBeacon
大部分现代浏览器都支持 navigator.sendBeacon方法。这个方法可以用来发送一些统计和诊断的小量数据,特别适合上报统计的场景。
- 数据可靠,浏览器关闭请求也照样能发
- 异步执行,不会影响下一页面的加载
- API使用简单
window.addEventListener('unload', logData, false);
function logData() {
navigator.sendBeacon("/log", analyticsData);
}
当浏览器支持sendBeacon方法,优先使用该方法,使用img方式降级上报。