前端性能优化的核心
- 更快的资源加载速度
- 更快的渲染速度/交互速度
提到了性能就不得不说下性能指标
性能指标
用户对性能延迟的看法
Google前端页面性能RAIL模型
核心内容如下:
- Response 响应
- Animation 动画
- Idle 空闲
- Load 加载
目标 | 准则 | |
响应: 在 50 毫秒内处理事件 | 在 100 毫秒内完成由用户输入发起的转换,让用户感觉交互是即时的 |
|
动画: 在 10 毫秒内生成一帧 |
|
|
空闲: 最大限度增加空闲时间 | 最大限度增加空闲时间以提高页面在 50 毫秒内响应用户输入的几率 |
|
加载: 在 5 秒内交付内容并实现可交互 当页面加载缓慢时,用户注意力会分散,他们会认为任务已中断。加载速度快的网站具有更长的平均会话时间、更低的跳出率和更高的广告可见性。 |
|
注:
目标是在 100 毫秒内响应输入,那么,为什么我们的预算只有 50 毫秒?这是因为除输入处理外,通常还有需要执行其他工作,而且这些工作会占用可接受输入响应的部分可用时间。如果应用程序在空闲时间以推荐的 50 毫秒区块执行工作,这就意味着,如果输入在这些工作区块之一中发生,它最多可能会排队 50 毫秒。考虑到这一点,假设只有剩余的 50 毫秒可用于实际输入处理才是安全地做法。下图展示了这种影响,图中显示了在空闲任务期间收到的输入如何排队,从而减少可用的处理时间:
空闲任务如何影响输入响应预算
常规性能指标
- 首屏绘制(First Paint,FP)
- 在渲染进程确认要渲染当前响应资源后,渲染进程会先创建一个空白页面,通常把创建空白页面的这个时间点称为 First Paint,简称 FP
- 白屏时间 其实指的就是创建这个空白页面到浏览器开始渲染非空白内容的时间,比如页面背景发生变化等一种比较简单的做法是在 body 标签之前获取当前时间 performance.timing.navigationStart,或者直接获取 performance 中关于 paint 的两个数据,都可以直接作为白屏数据,这两个数据一般差别不大。
- 首屏内容绘制(First Contentful Paint,FCP)
- 当用户看见一些 "内容" 元素被绘制在页面上的时间点,和白屏是不一样,它可以是 文本 首次绘制,或 SVG 首次出现,或 Canvas 首次绘制等,即当页面中绘制了第一个 像素 时,这个时间点称为 First Content Paint,简称 FCP
- 首屏时间 / 最大内容绘制(Largest Contentful Paint, LCP)
- LCP表示可视区“内容”最大的可见元素开始出现在屏幕上的时间点
- 最大内容绘制应在 2.5s 内完成
- 首次有效绘制(First Meaning Paint, FMP)
- “首次有效绘制”表示页面的“主要内容”开始出现在屏幕上的时间点,它以前是我们测量用户加载体验的主要指标。本质上是通过一个算法来猜测某个时间点可能是 FMP,但是最好的情况也只有77%的准确率,在lighthouse6.0 的时候废弃掉了这个指标,取而代之的是 LCP 这个指标
- 可交互时间(Time to Interactive,TTI)
- 表示网页第一次完全达到可交互状态的时间点。可交互状态指的是页面上的 UI 组件是可以交互的(可以响应按钮的点击或在文本框输入文字等),不仅如此,此时主线程已经达到“流畅”的程度,主线程的任务均不超过50毫秒。在一般的管理系统中,TTI 是一个很重要的指标。
- 首字节达到时间(Time to First Byte,TTFB)
- 指的是浏览器开始收到服务器响应数据的时间(后台处理时间 + 重定向时间),是反映服务端响应速度的重要指标
- TTFB 时间如果超过 500ms,用户在打开网页的时就会感觉到明显的等待
- HTML文档被完全加载和解析完成的时间(DOMContentLoaded, DCL)
性能指标工具
Performance面板(Google)
选择开发者工具的 Performance,可以看到左边有三个按钮,一个点击是直接开始录制,第二个是重新加载页面并录制,第三个便是清除。
火焰图
Network
通过Network可以看出网络请求的详细情况, 可以将鼠标 移动 或 点击 到具体的请求上查看加载时间和加载速度
Frame
通过 Frames 指标可以查看页面每一帧渲染时 CPU 所消耗的时间、持续时间Duration、FPS值的信息
Timings 指标
通过 Timings 指标可以查看在上面列举的一些性能指标的值,如下:
- 首次绘制(First Paint,FP)
- 首次内容绘制(First Contentful Paint,FCP)
- 首屏时间 / 最大内容绘制(Largest Contentful Paint, LCP)
- HTML 文档被完全加载 和 解析完成的时间(DOMContentLoaded, DCL)
Main 指标
记录渲染进程中主线程的执行记录,点击 Main 可以看到某个任务执行的具体情况,可以分析主线程的 Event Loop,分析每个 Task 的耗时、调用栈等信息
面板中会有很多的 Task,如果是耗时长的 Task,其右上角会标红,这个时候就可以选中标红的 Task,定位到耗时函数,然后针对性去优化。
Main 指标包含了加载过程的三个阶段:
- 导航阶段
- 主要是处理响应头的数据,并执行一些老页面退出之前的清理操作
- 解析 HTML 文件阶段
- 主要是解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOM 和 CSSOM
- 生成位图阶段
- 主要是将生成的 DOM 和 CSSOM 合并,包括了布局 (Layout)、分层、绘制、合成等一系列操作
Lighthouse面板(Google)
Performance 面板最大的优点就是各种数据信息非常的全,但这也是它最大的缺点,数据信息庞大到需要自行过滤,对于不熟悉的开发者来说,还是需要一定的学习成本的。
相反,Lighthouse 面板中的信息就相对简洁一些,除了检测结果以外,还会提供对应的改进方案,主要检测五个方面的内容:
- Performance(性能)
- Accessibility(可访问性)
- Best practice(最佳实践)
- SEO(搜索引擎优化)
- Progressive Web App(渐进式 Web 应用)
还提供了对应的诊断结果,建议优化点:
如Performance的优化建议:
可访问性Accessibility
Performance API
浏览器端的全局对象window上有一个名为 performance 的属性,在浏览器中用于记录页面加载和解析过程中关键时间点的机制,内置了一些前端需要的性能参数。
其中performance.timing 当前页面期间发生的各种事件的性能计时信息,我们可以通过这些时间节点来简单的计算出需要的性能指标数据(每个参数含义详见PerformanceTiming - Web API 接口参考 | MDN),计算方式如下:
const {
domainLookupStart,
domainLookupEnd,
navigationStart,
loadEventEnd,
responseStart,
responseEnd,
connectStart,
connectEnd,
redirectStart,
redirectEnd,
domContentLoadedEventEnd,
domComplete,
} = performance.timing
// DNS 查询时间
DNS = domainLookupEnd - domainLookupStart
// TCP 建立连接时间
TCP = connectEnd - connectStart
// 页面重定向时间
Redirect = redirectEnd - redirectStart
// 首字节到底时间
TTFB = responseStart - navigationStart
// 首次渲染时间
FP = responseStart - navigationStart
// DOM 解析时间
DOM = domComplete - responseEnd
// 首屏时间
LCP = loadEventEnd - navigationStart
web指标
简介
Web 指标是Google 开创的一项新计划,旨在为网络质量信号提供统一指导,这些信号对于提供出色的网络用户体验至关重要。
Google 提供了许多性能测量和性能报告工具。一些开发者对这些工具的使用十分在行,而另一些开发者则发现大量的工具和指标令人应接不暇。
我们想了解提供给用户的体验质量,并非需要成为性能专家。 Web 指标计划旨在简化场景,帮助网站专注于最重要的指标,即核心 Web 指标
核心 Web 指标
核心 Web 指标是适用于所有网页的 Web 指标子集,每位网站所有者都应该测量这些指标,并且这些指标还将显示在所有 Google 工具中。每项核心 Web 指标代表用户体验的一个不同方面,能够进行实际测量,并且反映出以用户为中心的关键结果的真实体验。
核心 Web 指标的构成指标会随着时间的推移而发展 。当前针对 2020 年的指标构成侧重于用户体验的三个方面——加载性能、交互性和视觉稳定性——并包括以下指标(及各指标相应的阈值):
- Largest Contentful Paint (LCP) :最大内容绘制,测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的2.5 秒内发生。
- First Input Delay (FID) :首次输入延迟,测量交互性。为了提供良好的用户体验,页面的 FID 应为100 毫秒或更短。
- Cumulative Layout Shift (CLS) :累积布局偏移,测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应保持在 0.1. 或更少。
为了确保能够在大部分用户的访问期间达成建议目标值,对于上述每项指标,一个良好的测量阈值为页面加载的第 75 个百分位数,且该阈值同时适用于移动和桌面设备。
如果一个页面满足上述全部三项指标建议目标值的第 75 个百分位数,那么评估核心 Web 指标合规性的工具应评判该页面为通过。
可视化性能指标分析中,经常会用到分位统计。比如FCP 8分位的值为1s: 意即80%的用户FCP为1s
在JavaScript中测量核心Web指标
测量所有核心 Web 指标,最简单的方法是使用web-vitalsJavaScript 库,这是一个围绕底层网页 API 的小型的、生产就绪的封装器,通过准确匹配每项指标在上方列出的所有 Google 工具中的报告方式来进行指标测量
通过使用web-vitals库,测量每项指标就像调用单个函数一样简单(有关完整用法和API详情,请参阅文档):
import {getCLS, getFID, getLCP} from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', {body, method: 'POST', keepalive: true});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
资源优化
CDN加速
- 因为浏览器对同一时间同一域名下资源请求数量有限制,可以通过cdn增加并发
- 设置 DNS预解析(一次DNS查询的时间大约是60~120ms之间或者更长,而TCP的三次握手时间大概也是几十毫秒或者更长)
<link rel="dns-prefetch" href="//example.com">
预下载
- preload浏览器会在遇到如下link标签时,立刻开始下载app.0f9424a8.js(不阻塞parser),并放在内存中,但不会执行其中的JS语句。只有当遇到script标签加载的也是app.0f9424a8.js的时候,浏览器才会执行预先加载的JS
<head>
<link href=/js/app.0f9424a8.js rel=preload as=script>
<link href=/js/chunk-vendors.bf703c67.js rel=preload as=script>
</head>
<body>
<script src=/js/chunk-vendors.bf703c67.js></script>
<script src=/js/app.0f9424a8.js></script>
</body>
- prefetch(一般多用于模块化,提前加载模块js、css等)浏览器会在空闲的时候,下载chunk-497522e7.f0d8c357.js, 并缓存到disk。当有页面使用的时候,直接从disk缓存中读取
<link href=/js/chunk-497522e7.f0d8c357.js rel=prefetch>
<link href=/js/chunk-c6a461ce.04d8fa41.js rel=prefetch>
- 如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能,不要在当前页面马上就要用的资源上用prefetch,要用preload
- defer和asyncdefer的执行时间是在所有元素解析完成之后,DOMContentLoaded 事件触发之前。async的执行时间是在当前JS脚本下载完成后,所以多个async script的执行顺序是不固定的
写了个检测当前网站是否使用preload和prefetch的google插件可以参考下:prefetch-preload-Checker
减少http请求
- 合并js、css、小图片
- 小图片通过构建工具转化为base64编码(一般几KB的转,过大会加大资源大小)
- 合并资源和转化base64会增大单文件大小要和资源大小做平衡
减小资源大小
- 图片压缩,不必要的png转jpg
- html、js、css压缩
- 开启服务器gzip压缩
- 模块化、分拆大体积文件
缓存
- 通过浏览器和服务器设置文件缓存时长,减少请求
- 本地storage缓存,减少不必要的数据请求等
动态页面静态化
占位图和图片懒加载
代码优化
严格模式
优化代码
- 模块化复用精简代码
- 避免js操作dom
- 缓存获取dom,减少获取dom次数
减少不必要的循环和逻辑嵌套
- 避免长数组循环,拆分为子数组。减少阻塞时间
缓存数据,减少不必要的请求
减少回流和重绘
Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小),以下操作会触发回流:
添加或删除可见的DOM元素
元素的位置发生变化
元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
页面一开始渲染的时候(这肯定避免不了)
浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素。
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color
复杂动画启用GPU加速
设置will-change: transform;
优势
使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
缺点
如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
GPU渲染字体会导致抗锯齿无效,这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。