迄今为止最全- 前端性能优化

简介

当我们说前端性能优化的时候,指的可能是不同场景的性能优化。前端涉及性能优化的场景主要有:

  1. 项目构建性能优化
  2. 页面性能优化
    1. 加载时性能优化
    2. 运行时性能优化

构建性能主要指构建速度,优化方法和打包工具直接相关,主要思路是缓存 + 并行

页面性能主要指页面加载速度和流畅度,很明显页面性能指标都是从用户体验角度出发的,

页面性能优化目标是尽可能快地展示出页面内容,尽可能快地使功能可用,减少页面卡顿,提升用户体验。

【注意页面性能优化的工作需要在构建、部署和编码各阶段进行处理,例如构建时候代码压缩,部署时候资源打散及CDN部署,编码节点注意DOM操作减少卡顿。】

性能优化应该从这几个方面来掌握
  1. 评价指标
  2. 优化方法
  3. 瓶颈分析(方法及工具)
性能优化的步骤通常是
  1. 确认目标,从用户角度出发,明确希望给用户什么样的体验(比如首屏展现、流畅性等)
  2. 设立影响用户体验的关键性能指标(卡顿率、秒开率)
  3. 梳理链路、拆解指标
  4. 瓶颈分析(可能通过工具分析,也可能需要根据埋点数据分析)
  5. 针对性能瓶颈优化
  6. 监控指标,观察优化效果
  7. 继续优化

页面性能优化

在了解页面性能优化之前,需要知道浏览器工作原理

浏览器原理: 从输入url到看到界面的过程

1. 检查缓存

如果浏览器有本地的静态资源缓存,并且未过期,则直接从缓存中读取,而不会发送网络请求。

2. DNS解析

将输入的url对应的域名解析成ip,DNS解析过程根据 本机的hosts文件->本地DNS缓存->本地域名服务器的优先级解析域名,在域名解析过程还可能涉及和顶级域名服务器、根域名服务器、根域名服务器的交互。

3. 发送HTTP请求

建立TCP连接,如果是HTTPS还需要建立TLS连接,然后发送HTTP请求,并接收响应,如果响应状态码为301/302还需要进行重定向。

4. 将响应数据提交给渲染进程处理

渲染进程解析HTML,这个过程中,会提前解析外链(link和script)并提前下载。

5. 构建DOM

将HTML数据转换为DOM。

6. 样式计算

将CSS转换为CSSOM(document.styleSheets),并使用CSSOM,根据继承规则和层叠规则,计算每个DOM节点的具体样式,得到ComputedStyle树。

7. 布局

根据DOM树和ComputedStyle,计算所有可见元素的坐标,生成一个新的树:布局树。

8. 分层

根据布局树生成不同的图层,得到分层树(LayerTree)。

9. 绘制

对每个层生成绘制指令。然后渲染引擎将绘制指令提交给合成线程处理。

10. 分块

合成线程会先将每个图层分块(tile),优先渲染视口附近的块。合成线程把tile提供给栅格化线程。

11. 栅格化

栅格化线程把每个块转成位图,并写到显存中。一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。DrawQuad是一系列的指令,这些指令引用了显存中的tile位图。

12. 合成

浏览器进程接收到DrawQuad,并用指令将tile合成完整的帧,然后调用GPU进程将合成后的帧绘制到屏幕上。

性能优化原理

加载阶段

加载阶段可以划分为两个大的阶段,URL请求阶段和渲染阶段。上述从URL请求到看到界面的过程简述了整个过程。下面详细说明过程中的细节。

URL请求阶段

URL请求阶段主要有缓存、DNS检查、发送HTTP请求,这个阶段通常是服务器的响应时间占用大部分时间。对于静态资源,服务器的响应时间一般不会成为瓶颈;对于服务端渲染则需要进行缓存等优化处理。   

针对这个阶段,有一些常用优化手段。     

  1. 使用httpDNS
  2. 减少重定向
  3. 静态资源服务器gzip
  4. 静态资源使用CDN
  5. 服务器升级带宽
  6. 升级服务器配置
  7. 数据库优化(对于服务端渲染的页面)
  8. 缓存动态网页(对于服务端渲染的页面)
  9. 还有可能是服务器被攻击导致缓慢,更换高防服务器
  10. 善用缓存
渲染阶段

从上述“从输入url到看到界面的过程”,我们知道渲染阶段有解析HTML构建DOM、样式计算、布局、分层、绘制、分块、栅格化、合成这几个步骤。这其中耗时较长的,最容易造成瓶颈的是构建DOM、样式计算这2个步骤。

在这两个步骤中,存在几个关键的操作:解析HTML构建DOM、下载CSS、解析CSS、下载JavaScript、解析并执行JavaScript。

这两个步骤的更详细的操作流程是

  1. 预解析HTML,预加载link和script外链。现代浏览器对外链加载做了优化,会在渲染进程接收到HTML数据字节流时候就开始预解析HTML,预解析会找到HTML中的外链并交给下载进程提前加载。
  2. 解析HTML,构建DOM。
  3. 遇到style标签或者link外链加载好后,解析CSS,构建CSSOM。
  4. 遇到script标签,停止解析HTML,等待外链CSS加载并解析完成、内联CSS解析完成后,再执行JavaScript。执行完JavaScript再开始解析HTML。如果外链的script标签有defer/async属性,则该script标签的下载和执行时候不会停止解析HTML。若存在defer属性,JavaScript会等DOMContentLoaded事件触发后再开始执行;若存在async属性,JavaScript会等下载完再执行。动态创建的标签也会在下载完成后再执行。

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

从上面操作流程可以得到各操作阻塞的总结

  • css解析会阻塞渲染。因为构造渲染树需要CSSOM,因此CSS解析完成是后续工作的先决条件。
  • css下载会阻塞js执行,不会阻塞html解析。
  • js下载和执行会阻塞html。
  • 在执行 JavaScript 脚本之前,会先加载并解析页面中的CSS样式(包括link标签和style标签)(如果存在)。也就是说 CSS 在部分情况下也会阻塞 DOM 的生成。
  • defer的js会异步下载执行,不阻塞HTML解析。
  • async的js会异步下载,下载完执行,即下载不阻塞HTML解析,但执行阻塞HTML解析。

由于解析HTML、加载CSS、解析CSS、加载JavaScript、解析并执行JavaScript之间会有相互阻塞,因此为了尽快到达构造渲染树的阶段,有两个原则:

  1. 尽量避免阻塞
  2. 缩短阻塞时间

从上面对关键资源阻塞规律的总结可以知道,CSS的下载和解析对于首次构建布局树是必要的步骤,没有办法避免。JavaScript的下载、解析和执行则不应该阻塞HTML解析,为什么呢?首先,如果JavaScript没有操作DOM,那么首次构建布局树不需要JavaScript;如果JavaScript操作了DOM,也应该在整个HTML解析完,基本的DOM树构建好了之后再开始执行JavaScript操作DOM。

因此样式资源放在head标签中,这样并不会造成不必要的阻塞,并且代码会更规整;JavaScript应该放在body底部或者加上defer/async属性或者动态创建script标签(动态创建的script标签外链会异步加载)避免JavaScript的下载执行阻塞HTML的解析。

如何缩短阻塞时间呢?有2个原则:尽可能少、尽可能早。

资源量尽可能少(压缩、雪碧图、按需加载)、请求次数尽可能少(打包)、让无依赖关系的资源尽可能早加载而不是等待排队(域名打散、分包,即并行)、请求链路尽可能少(CDN)

尽可能早解析DNS(DNS预解析)

加载阶段性能优化方案

“尽可能少”、“尽可能早”的原则实现的优化手段。

1. 减少需要请求的资源尺寸:资源压缩、删除冗余代码和其他资源,或者使用尺寸更小的资源
  • 代码压缩,包括js/css/html都应该压缩。
  • 服务器开启gzip。
  • iconfont代替图片。
  • 使用webp图片,在质量相同的情况下,WebP 格式图像的体积要比 JPEG 格式图像小 40%。
  • 删除无用代码(摇树js和css、删除console.log)。
  • 模块按需加载(antd、lodash、moment等常用的第三方库,不用的模块不打包进项目)
2. 减少请求数量
  • 合并请求,由于每次请求时候,实际传输的内容只占整个请求过程的较少一部分时间,因此合并内容让多个请求变成一个可以节约请求中建立连接、排队等待等耗时。
  • 雪碧图,图片合成,避免每个图片都要发一次请求。
  • 内联较小的js css、图片(转成base64)等资源,避免再发一次请求获取资源。
3. 缓存
  • 使用强缓存,文件名加hash后缀,这样只要文件内容不变,就会读缓存内容。
  • 文件分包,更好地利用缓存,不常改变的资源分离出来。
  • 使用cdn,注意要避免html被cdn缓存,可以在cdn服务配置不缓存html资源,也可以把html部署在自己的服务器。
4. 并行请求
  • 域名打散(针对http1)。
  • 使用http2。(当然http2还有二进制等好处)。
5. 按需加载
  • 图片按需加载,只下载可视区附近的图片。
  • 组件懒加载,路由懒加载,其实路由懒加载本质也是组件懒加载。
  • 其他资源懒加载,避免由于模块引用关系不当,导致首屏页面加载了首屏用不到的CSS、字体图标、图片等资源。
6. 预加载

可以通过<meta>或者<link>提前加载资源或者连接

本页面资源预处理,用于加载资源之前做一些预处理,增加资源加载速度

<!-- DNS预解析 -->
<meta http-equiv="x-dns-prefetch-control" content="on" />
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
<!-- 预连接,包括DNS、TCP和HTTP连接 -->
<link rel="preconnect" href="https://cdn-s1.somecdnsite.com">
<!-- 预加载资源 -->
<link rel="preload" href="./main.css" as="style">

下个页面资源预处理,用于在下个页面加载资源之前做一些处理

<!-- 提前拉取资源,下个页面可以直接使用 -->

<link rel="prefetch" href="lib/jquery.min.js" as="script">

<!-- 提前渲染下个用户可能访问的页面,用户跳转时候可以直接展示 -->

<link rel="prerender" href="https://www.apple.com" as="script">
7. 注意事项

异步加载js(async、defer、放到body底部、动态创建script标签)。
不使用CSS @import,CSS的@import会造成额外的请求 用代替@import 。
避免空的src和href 留意具有这两个属性的标签如link,script,img,iframe等;

8. 服务端渲染和预渲染。

主流框架都支持SSR,并有开箱即用的框架,服务端渲染有优异的首屏性能,并且对SEO更友好。
预渲染目的提升首屏性能,预渲染就是在构建阶段,启用无头浏览器,加载项目的路由,并渲染出首屏页面(也可以配置其他路由),然后生成静态页面,输出到指定的目录。
prerender-spa-plugin

preender-spa-plugin: 使用这个插件相当于在构建阶段就渲染好了首屏页面,极大地提升了首屏性能。

指标

对于加载阶段,一个常用指标是TTFBTime To First Byte,首字节响应时间,指从发送请求到收到首个字节。大多数服务器的 TTFB 时间都在100ms 以内,这个时间就是我们优化时候可以追求的时间。

这里的TTFB指的是HTML资源的TTFB,因为浏览器收到HTML时候才会开始构建DOM,进入渲染阶段。

使用performance API计算方法

performance.timing.responseStart - performance.timing.requestStart

对于渲染阶段,主要的指标是首屏和白屏。

白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素

首屏时间 = 地址栏输入网址后回车 - 浏览器第一屏渲染完成

实际上浏览器没有API返回白屏和首屏时间,我们需要用其他API近似计算。

网上有些文章提到在HTML中加script标签计算白屏和首屏

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>白屏</title>
  <script type="text/javascript">
    // 不兼容performance.timing 的浏览器,如IE8
    window.pageStartTime = Date.now();
  </script>
  <!-- 页面 CSS 资源 -->
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
  <script type="text/javascript">
    // 白屏时间结束点
    window.firstPaint = Date.now();
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>
白屏时间 = firstPaint - performance.timing.navigationStart || pageStartTime。

这些方法在没有performance API的时代很常用,但是它们有较大的问题。这些方法一方面容易受其他script标签位置影响,比如如果在body前部有个耗时长的script标签,那么body的渲染将会被延后,渲染之前将一直是白屏。因此这样计算白屏并不准确。另一方面这些方法代码侵入性较强,不通用。

也有使用performance.timing.domloading事件作为白屏结束时间点。这也不准确,因为该事件表示渲染引擎开始解析HTML,如果有script执行block,渲染同样会延

目前计算白屏和首屏比较常用的方法是:

白屏结束时间 = FP事件触发时间(Frist Paint)

首屏结束时间 = FCP事件触发时间(Frist Content Paint)

业界(Core Web Vitals)新的标准更关注最大内容的渲染时间LCP(largest contentful paint),认为LCP能够更好地衡量用户关注的主要内容的加载速度。

前端页面性能指标
1. 简介

最初,评价前端页面加载性能有两个指标:DOMContentLoaded和load事件,分别代表DOM构造完成和首屏资源加载完成。

对于之前的页面和现代的服务端渲染的页面,这两个指标都可以很好地衡量首屏内容展示时间。但对于现代复杂的单页应用,都是通过JS操作DOM向页面添加主要内容,对于这种场景,DOMContentLoaded和load事件就不能很好地衡量首屏显示时间了。

于是有FP、FCP、FMP被提出来,它们关注的不是“加载”,而是“渲染”,因此能更好地表现用户看到的情况。

FP、FCP这两个指标虽然表达了渲染的事件,但对“用户关注的内容”没有体现,比如首屏渲染出来一个背景,或者一个loading,可能对于用户来说和白屏区别不大。FMP虽然体现了“关键内容”的要素,但它是复杂的、模糊的,甚至是错误的,并不能准确识别页面主要内容的加载时机。

后来LCP指标被提出来,表示“用于度量视口中最大的内容元素何时可见”,它用来代替FMP,表征页面的关键元素何时可以被用户看到。

除了加载性能,还有可交互时间、稳定性指标、流畅性指标,在不同的业务场景都可以被监控用来作为提升用户体验的依据。

谷歌一直十分重视网站的用户体验,移动友好性,页面加载速度和HTTPS是Google已经使用的页面排名因素,而2020年,谷歌将Core Web Vitals新纳入的用户体验指标。其中核心的3个就是LCP、FID、CLS。后面会详细说明。

2. 加载性能指标

我们知道我们使用浏览器访问页面时候,浏览器将页面从网络下载到本地后,主要做几个事情:解析HTML,创建DOM,同时加载依赖的资源:CSS、图片等(加载资源的过程不会阻塞DOM解析),然后调用渲染进程渲染到界面上。

这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了猜测预加载。当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。

在整个加载和渲染过程中会触发多个事件

关键事件
  • load(Onload Event),它代表页面中依赖的所有资源加载完的事件。
  • DCL(DOMContentLoaded),DOM解析完毕。
  • FP(First Paint),表示渲染出第一个像素点。FP一般在HTML解析完成或者解析一部分时候触发。
  • FCP(First Contentful Paint),表示渲染出第一个内容,这里的“内容”可以是文本、图片、canvas。
  • FMP(First Meaningful Paint),首次渲染有意义的内容的时间,“有意义”没有一个标准的定义,FMP的计算方法也很复杂。
  • LCP(largest contentful Paint),最大内容渲染时间。

下面对各个事件和特点详细说明

白屏和首屏

白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素

首屏时间 = 地址栏输入网址后回车 - 浏览器第一屏渲染完成

根据白屏和首屏的定义,我们可以用FP和FCP来计算白屏和首屏。

白屏结束时间 = FP事件触发时间

首屏结束时间 = FCP事件触发时间

当然FCP代表第一个内容被渲染出来,有些业务中希望用更关键的内容的渲染来表示首屏,这时候可以用FMP或者LCP来作为首屏的计算指标。

load事件

Onload Event代表页面中依赖的所有资源:DOM、图片、CSS、Flash等都加载完,window.onload注册的回调就会在load事件触发时候被调用。

有时候FCP比Onload Event先触发,因为渲染第一个内容时候可能不包括图片的展示,只有文本内容。

所有依赖的资源包括异步加载的资源,但不包括延时加载的资源。

<html>
<head>
    <title>demo</title>
</head>
<body>
    <script>
        // 该资源算在Onload Event加载的资源中
        const img = document.createElement('img');
        img.src = 'https://domain/path/image.png';
        document.appendChild(img);

        // 该资源算在Onload Event加载的资源中
        setTimeout(() => {
            const img = document.createElement('img');
            img.src = 'https://domain/path/image.png';
            document.appendChild(img);
        }, 0);

        // 该资源不会算在Onload Event加载的资源中
        setTimeout(() => {
            const img = document.createElement('img');
            img.src = 'https://domain/path/image.png';
            document.appendChild(img);
        }, 5000);
    </script>
</body>
</html>
DCL

DOMContentLoaded事件,当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载。

可以通过注册回调监听该事件

  document.addeventListener('DOMContentLoaded', function() {}, false);

document.onloaddocument.body.onload也是这个事件的回调。

DCL和load的先后顺序

如果需要渲染的内容不多,DCL在load之前,如果需要渲染的内容很多,那么DCL会在load之后。

FP和FCP的关系

浏览器渲染的界面可能是“内容”,例如文本,也可能不是“内容”,比如一个背景为红色的div标签。FCP事件指渲染出第一个内容的事件,而FP指渲染出第一个像素点,渲染出的东西可能是内容,也可能不是。

有节点不一定有渲染,如果没有任何样式,是没有界面的,也不需要渲染。下面代码就没有FP事件

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>no FP</title>

</head>

<body>

<div></div>

</body>

</html>

下面代码,会渲染界面,因此会触发FP事件,但是不会触发FCP,因为没有内容

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>has FP, no FCP</title>

<style>

div {

width: 1px;

height: 1px;

background-color: red;

}

</style>

</head>

<body>

<div></div>

</body>

</html>

注意:渲染的操作一定是发生在视口内的,对于视口外不可见的内容,不会触发“Paint”操作,比如下面代码,不会触发FP事件。

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<title>no FP</title>

<style>

div {

position: absolute;

left: -99999px;

width: 1px;

height: 1px;

background-color: red;

float: left;

}

</style>

</head>

<body>

<div></div>

</body>

</html>

通过上面对FP和FCP的介绍,可以知道,如果html本身有内容(文本、图片)或者js脚本很快能创建内容,那么FP和FCP会一起触发。否则FP比FCP提前触发。FP肯定不会在FCP后面出现,因为渲染出内容,一定也渲染出了像素。

FP和DCL的先后顺序

浏览器不一定等到所有的DOM都解析完再开始渲染,如果DOM节点少,浏览器会加载完再渲染,但是如果节点很多,浏览器解析一部分节点后就会开始渲染(这时候就会触发FP)。也就是说,当需要渲染的节点数少的时候,DCL会在FP前面;当需要渲染的节点数很多时候,DCL会在FP后面。

例如下面的代码,只有一个div,会先触发DCL然后再触发FP。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>FP after DCL</title>
    <style>
        div {
            width: 1px;
            height: 1px;
            background-color: red;
            float: left;
        }
    </style>
</head>
<body>
    <div></div>
</body>
</html>

而下面的代码,有10000个div,FP会在DCL前面。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>FP before DCL</title>
    <style>
        div {
            width: 1px;
            height: 1px;
            background-color: red;
            float: left;
        }
    </style>
</head>
<body>
    <!-- 10000个div... -->
</body>
</html>
各指标的计算

FP、FCP和L时间都可以通过performance API计算

// load
// loadEventStart是load事件发送给文档,也即load回调函数开始执行的时间
// loadEventEnd是load回调函数执行完成的时间
const loadTime = performance.timing.loadEventStart - performance.timing.fetchStart


// DCL
const dcl = performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart

// FP
const fp = performance.getEntries('paint').filter(entry => entry.name == 'first-paint')[0].startTime;

// FCP
const fcp = performance.getEntries('paint').filter(entry => entry.name == 'first-contentful-paint')[0].startTime;

// Onload Event
const l = performance.timing.loadEventEnd - performance.timing.navigationStart;

// LCP
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

// LCP也可以通过web-vitals计算
import {getLCP, getFID, getCLS} from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);
 3.可交互时间
首次可交互时间

文档首次可交互时间可以用来衡量页面可交互的性能。

首次可交互,即DOM加载并解析完成后,界面上的元素可以交互(如输入框可以输入、按钮可以点击、超长元素可以滚动)。其时间用performance.timing.domInteractive计算。

performance.timing.domInteractive:当前网页DOM结构结束解析、开始加载内嵌资源的时间,document.readyState 变成interactive,并将抛出"readyStateChange"事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源)

const timeToInteractive = 
performance.timing.domInteractive - 
performance.timing.fetchStart, //首次可交互时间

TTI
用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点
深入浅出前端监控

这里定义一下什么是完全可交互状态的页面:
1、页面已经显示有用内容。
2、页面上的可见元素关联的事件响应函数已经完成注册。
3、事件响应函数可以在事件发生后的 50ms 内开始执行(主线程无 Long Task)。

计算方法描述如下:
1、从 FCP 时间开始,向前搜索一个不小于 5s 的静默窗口期。(静默窗口期定义:窗口所对应的时间内没有 Long Task,且进行中的网络请求数不超过 2 个)
2、找到静默窗口期后,从静默窗口期向后搜索到最近的一个 Long Task,Long Task 的结束时间即为 TTI。
3、如果一直找到 FCP 时刻仍然没有找到 Long Task,以 FCP 时间作为 TTI。
其实现需要支持 Long Tasks API 和 Resource Timing API。

longTask
Long Task:阻塞主线程达 50 毫秒或以上的任务,可以通过PerformanceObserver获取。

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

console.log('longtask candidate: ', entry.startTime);

}

});



observer.observe({entryTypes: ['longtask']});
FID

FID(First Input Delay) 用于度量用户第一次与页面交互的延迟时间,是用户第一次与页面交互到浏览器真正能够开始处理事件处理程序以响应该交互的时间。

相对于TTI,FID表示实际的用户操作的延时,更能从用户角度反映网页的交互性能。

其计算使用简洁的 PerformanceEventTiming API 即可,回调的触发时机是用户首次与页面发生交互并得到浏览器响应(点击链接、输入文字等)。

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('fid', entry.processingStart - entry.startTime);
  }
});

observer.observe({type: 'first-input', buffer: true});
4. 稳定性指标 CLS

CLS(Cumulative Layout Shift)是对在页面的整个生命周期中发生的每一次意外布局变化的最大布局变化得分的度量,布局变化得分越小证明你的页面越稳定。
听起来有点复杂,这里做一个简单的解释:
●不稳定元素:一个非用户操作但发生较大偏移的可见元素称为不稳定元素。
●布局变化得分:元素从原始位置偏移到当前位置影响的页面比例 * 元素偏移距离比例。

网站应努力使 CLS 分数小于 0.1 。

可以通过web-vitals获取CLS。
import {getCLS} from 'web-vitals';

getCLS(console.log);

减少CLS的方法,参考文章:页面视觉稳定性之CLS

5. 流畅性指标

FPS
Chrome DevTool 中有一栏 Rendering 中包含 FPS 指标,但目前浏览器标准中暂时没有提供相应 API ,只能手动实现。这里需要借助 requestAnimationFrame 方法模拟实现,浏览器会在下一次重绘之前执行 rAF 的回调,因此可以通过计算每秒内 rAF 的执行次数来计算当前页面的 FPS。
FPS过低会让用户感觉卡顿,因此这个计算可以用来监控页面卡顿情况。
longTask
长任务监听,PerformanceObserver 监听,参考上面TTI一节中长任务的监听。

6. Core Web Vitals

Core Web Vitals是谷歌提出的,衡量用户体验的新指标,指标将被纳入谷歌搜索引擎的网页排名。
Core Web Vitals是用户体验和SEO的重要指标。
关键的指标包括
LCP,用来衡量页面加载性能,为了提供良好的用户体验,LCP应该在页面首次开始加载后的2.5秒内发生
FID,衡量交互性能,为了提供良好的用户体验,页面的FID应该小于100毫秒。
CLS,测量视觉稳定性。为了提供良好的用户体验,页面应该保持CLS小于0.1。
Chrome提供了web-vitals库获取3个关键指标的数值。

import {getLCP, getFID, getCLS} from 'web-vitals';

getCLS(console.log);

getFID(console.log);

getLCP(console.log);
瓶颈分析

对于TTFB指标,如果是客户端渲染,那么通常服务器响应时间不会成为网页加载性能的瓶颈,因为静态资源服务器在网络请求时候使用的资源很小,不会有很大压力。如果是服务端渲染网页,就需要服务器做好并发优化、数据库优化、缓存策略等工作,并做好监控以保证响应时间不会过长影响页面加载。

对于白屏和首屏,评估网页性能瓶颈需要区分客户端渲染和服务端渲染。

客户端渲染一般只有一个root节点用于js挂载dom。因此应该尽早加载用于操作DOM生成首屏DOM树的JavaScript代码。服务端渲染项目返回给浏览器的是处理好的完整HTML,解析完HTML、load完依赖资源就可以开始渲染了,因此服务端渲染应该更关注如何避免HTML解析被阻塞。

客户端渲染主要关注压缩文件、按需加载。服务端渲染复杂一些,需要考虑避免关键渲染路径上的阻塞。加载阶段瓶颈分析可以使用现成的网页性能测试工具,如

webpagetest

pagespeed

lighthouse(可以使用chrome的lighthouse devtool,也可以用nodejs API)

hiper(基于node的性能分析工具)

注意一些网页性能测试工具可能需要科学上网。

这些网页性能测试工具都会从各个维度对网页性能打分,并给出优化建议。

也可以使用chrome的performance和network工具观察网页加载渲染行为,从而找出性能瓶颈。

使用performance工具关注主要的事件(FCP),看触发之前有哪些操作是不必要的或者过长的(如是否加载了不必要的资源、资源加载事件是否过长,资源是否过大等)。

加载阶段:network查看是否关键资源尽快下载完、是否有过大的文件而未压缩、是否有并行下载过多的情况。

监控

监控通常使用performance API采集关键事件点上报,根据上面对指标的介绍可以总结上报的主要指标如下

const ttfb = performance.timing.responseStart - performance.timing.requestStart;

// FP
const fp = performance.getEntries('paint').filter(entry => entry.name == 'first-paint')[0].startTime;

// FCP
const fcp = performance.getEntries('paint').filter(entry => entry.name == 'first-contentful-paint')[0].startTime;

// Onload Event
const l = performance.timing.loadEventEnd - performance.timing.navigationStart;

数据上报之后,通常使用fp数据表示白屏时间,使用fcp/Max(fcp, l)时间表示首屏时间。一般会通过95分位/98分位的数据来评估网页性能。

运行阶段

渲染过程

首次加载的渲染过程

在上面已经讨论过,渲染过程的关键操作是

  1. html转成dom
  2. 计算style
  3. 生成布局树
  4. 分层,生成分层树
  5. 主线程给每个图层生成绘制列表,交给合成线程处理
  6. 合成线程将图层分块
  7. 合成线程在光栅化线程池中将图块转成位图
  8. 合成线程发送绘制图块的命令drawquad给浏览器进程
  9. 浏览器根据命令绘制,并显示在显示器上
重排和重绘

如果JavaScript做了修改DOM元素的几何属性(位置、尺寸)等操作,将会重新计算style,并且需要更新布局树,然后执行后面的渲染操作,即从1~9的步骤需要重新执行一遍。这个过程叫“重排”。

如果JavaScript修改了DOM的非几何属性,如修改了元素的背景颜色,不需要更新布局树和重新构建分层树,只需要重新绘制,即省略了3、4两个阶段。

在页面运行中,应该尽量避免重排和重绘,以提升渲染性能。

什么情况会触发重排

除了首次加载,还有一些其他的操作会引起重排

  • DOM元素移动、增加、删除和内容改变会触发回流。
  • 当DOM元素的几何属性(width / height / padding / margin /border)发生变化就会触发回流。
  • 读写元素的offset / scroll / client等属性会触发回流。
  • 调用window.getComputedStyle会触发回流。

注意,多次修改样式并不一定触发多次回流,例如

document.getElementById('root').stlye.width = '100px';
document.getElementById('root').stlye.height = '100px';
document.getElementById('root').stlye.top = '10px';
document.getElementById('root').stlye.left = '10px';

上面代码只会触发一次回流,这是因为浏览器自身有优化机制。

但是获取offset等元素属性,每获取一次都会触发一次回流,这是因为offset等属性,要回流完才能获取到最准确的值。

如何减少重排

在更新界面时候,应该尽量避免重排。

1、避免元素影响到所在文档流

用绝对定位(position: absolute;)使元素脱离文档流,这样元素的变化不会导致其他元素的布局变化,也就不会引起重排。

如果使用CSS的transform属性实现动画,则不需要重排和重绘,直接在合成线程合成动画操作,即省略了3、4、5三个阶段。由于没有占用主线程资源,并且跳过重排和重绘阶段,因此这样性能最高。

2、读写分离

当JS对DOM样式进行读写时候,浏览器会如何操作呢?

浏览器对写操作会采用渲染队列机制,将写操作放入异步渲染队列,异步批量执行。当JS遇到读操作时候(offset / scroll / client),会把异步队列中相关的操作提前执行,以便获取到准确的值。

下面通过几个示例理解这个过程。

当使用JS修改样式时候,可能触发重排,例如

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

上面代码执行后,浏览器并不会触发4次重排,而是会将3个操作放入一个渲染队列中,异步批量执行,因此可能只会触发一次重排。

当遇到读操作时候,则立刻执行渲染队列中相关操作,从而马上触发重排。

div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);

上面每个console.log()都会让浏览器取出异步渲染队列中的写操作执行,然后返回重排后的值。

对于上面的情况,为了避免重排,应该进行读写分离

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

上面代码在执行console.log()的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。因为渲染队列是空的,所以后面的读操作并没有触发重排,仅仅取值而已。

如果需要根据当前的样式设置新样式,应该先缓存当前样式,然后批量更新样式。

// bad 强制刷新,触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';

// good 缓存布局信息,读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
3、集中改变样式

虽然浏览器有异步渲染队列的机制,但是异步flush的时机我们没有办法控制,为了保证性能,还是应该集中改变样式。

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// good 
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
4、离线改变DOM

如果需要进行多个DOM操作(添加、删除、修改),不要在当前的DOM中连续操作(如循环插入li)。

  • 在要操作DOM之前,通过display隐藏DOM,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘。
dom.display = 'none';

// 执行DOM操作...

dom.display = 'block';
  • 通过使用DocumentFragment创建一个dom碎片,在它上面批量操作DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。
  • 复制节点,在副本上操作,然后替换原节点。
瓶颈分析

使用chrome的performance查看main部分火焰图,检查是否有过长时间的js block,避免大的js循环和大列表渲染,控制循环和列表的上限。导致页面卡顿,如果js执行超过几百ms就需要警惕了。

  • 31
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

勒布朗-前端

请多多支持,留点爱心早餐

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值