Performance的使用

Performance的使用

浏览器多进程架构

进程 负责的工作 包含的线程
浏览器进程 负责浏览器的导航栏、书签、前进、后退按钮等,还会控制我们看不见的部分,包括文件的读写等 UI线程等
渲染进程 渲染进程负责网页展示相关的所有工作,主要任务是将HTML,CSS,以及JavaScript转变为我们可以进程交互的网页内容。每一个tab页、页面内的每一个iframe都会分配一个单独的渲染进程 主线程:渲染主线程的任务执行过程;
Compositor: 合成线程,将绘制列表合成页面;
Chrome_ChildIOThread线程: IO线程;
Raster: 光栅化线程池等
GPU进程 负责独立于其他进程的GPU任务,处理来自不同tab的渲染请求,并把他们在界面上画出来
网络进程 负责页面的网络资源加载
Plugin进程 负责网页使用的所有插件
多进程架构的优点
  1. 容错性:每个tab都有独立的渲染进程,当一个tab也崩溃时,关闭这个tab,不影响其他tab页
  2. 安全性和沙盒性:操作系统提供方法限制每个进程拥有的能力,所以浏览器可以让某些进程不具备某些特定的功能,比如tab渲染进程会处理来自用户的输入,所以chrome限制了他们对系统文件随机读写的能力
多进程架构的缺点
  1. 内存消耗:每个进程都有独立的内存空间,不像一个进程中的所有线程可以公用内存空间,这会造成一些基础架构会在不同进程的内存空间同时存在(比如V8 JavaScript引擎),这些重复内容会消耗更多的内存。为了节省内存,chrome会限制启动的进程数目,当进程数达到一定的界限后,chrome会将访问同一个站点的tab都放在一个进程里面跑

    同一站点的定义为:根域名+协议

性能指标简介

Performance面板中的各项指标就记录了浏览器多个进程之间配合完成页面渲染的流程,先简单介绍一下各个指标的基本内容。

Network指标

Network指标主要是记录页面中每个网络请求在网络进程中所消耗的时长

  • HTML: 蓝色
  • CSS: 紫色
  • JS: 黄色
  • Images: 绿色

截屏2022-02-18 上午8.53.27

上图每个请求的柱状图对应Network的Timing面板:

截屏2022-02-18 上午8.54.10

左侧的灰线表示请求排队时间(Queueing)发起连接(Connection Start)这一组操作共花费的时间。

排队时间(Queueing)请求正在排队指的是:

  1. 请求被渲染引擎推迟,因为该请求的优先级低于其他关键资源的优先级
  2. 请求被暂停,等待将要释放的TCP套接字
  3. 请求被暂停,因为有“队头阻塞”, 浏览器仅允许每个域名同时最多发起6个请求(http1.0/http1.1)
  4. 生成磁盘缓存条目所用的时间

发起连接(Connection Start)包括阻塞时间(Stalled)、DNS Lookup、建立TCP连接(TCP握手、协商SSL)等;

浅色代表发送请求(Request sent)和等待首字节响应(TTFB)的时间,这部分包括服务器处理的时间,网络延迟时间等;

深色代表下载内容,即从接收到第一个字节到最后一个字节接收完毕花费的时间;

右侧的灰线表示等待渲染主线程响应的时间;

Timings指标

记录关键的时间节点产生的数据信息,FP,FCP,LCP等

截屏2022-02-03 下午2.09.28

指标 触发事件 触发时间
DCL DOMContentLoaded 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完全加载。
L load 当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load事件
FP First Paint 首次绘制(First Paint)渲染进程确认要渲染当前的请求后,渲染进程会创建一个空白页面,我们把创建空白页面的这个时间点称为FP
FCP First Contentful Paint 首次有内容的绘制,第一个dom元素绘制完成的时间点
LCP Largest Contentful Paint 最大内容绘制,报告从首次开始加载的时间开始,到可视区域内可见的最大图像或文本块完成渲染的相对时间,最大元素完成渲染的时间点
Frame指标

记录浏览器生成每帧的记录

鼠标移到某个帧上,会展示渲染当前帧花费的时间和fps(帧每秒)

截屏2022-02-02 下午3.24.04

60fps就是60帧每秒,屏幕每秒会展示60帧静态图像,1000ms / 60 ≈ 16.7ms,所以只有在16.7毫秒内完成一次帧绘制,页面看起来才会更流畅,一帧绘制的时间超过16.7ms,就会出现掉帧的情况,页面会表现出卡顿的现象

Layout Shift由Layout Instability API定义,每当视口中两次渲染帧之间的可视元素改变了其起始位置时都会触发layout-shift entries,改变了起始位置的元素被认为是不稳定元素。由于LS只会发生在改变了初始位置的已有元素上,只要新加入的元素并不会造成其他可见元素改变位置,它将不会被当成是LS元素。

Main指标

记录渲染进程中主线程的执行记录,渲染进程负责标签(tab)内发生的所有事情,在渲染进程里面,主线程(main thread)处理了绝大多数你发送给用户的代码。

Event: beforeunload

当浏览器窗口关闭或者刷新时,会触发beforeunload事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。如果实现了beforeunload事件,就会在这一阶段被触发。

截屏2022-02-03 下午5.25.19

Event: pagehide

当浏览器在显示与会话历史记录不同的页面的过程中隐藏当前页面时, pagehide(页面隐藏)事件会被发送到一个Window 。例如,当用户单击浏览器的“后退”按钮时,当前页面在显示上一页之前会收到一个pagehide(页面隐藏)事件。

Event: visibilitychange

当其选项卡的内容变得可见或被隐藏时,会在文档上触发 visibilitychange (能见度更改)事件

Event: webkitvisibilitychange

当前网页在可见和不可见之间变换的时候调用

Event: unload

当文档或一个子资源正在被卸载时, 触发 unload事件。

Send Request

表示网络请求已发送

Receive Response

表示已经接收到HTTP响应头

commitNavigationEnd

导航结束

Receive Data

接收数据,如果数据量大会存在多个Recive Data过程

Finish Loading

所有数据接收完成后执行,表示网络请求已经完成

Parse HTML

构建DOM树,以内渲染引擎无法直接理解html内容,需要将其解析为DOM树结构,DOM树表示了页面上所有节点的内容和节点之间的关系。

DOM construction process

如果遇到script标签,就暂停DOM树构建,如果是内联script,直接执行代码,如果是外部js文件,在过去会触发Send Request事件,网络进程开始请求外部js文件,请求完成后触发Finish Loading事件, 之后触发Evaluate Script事件,开始执行js代码,执行完成后再继续执行Parse HTML构建DOM树,如果遇到如.css等子资源,还是会触发Send Request事件,网络进程请求资源,但是不会阻塞DOM树的构建。

现在chrome已做预解析优化,在渲染主线程接收到HTML的字节流后,会开启预解析线程,分析HTML中包含的JS、CSS等资源,解析到这些资源链接后,预解析线程会让网络进程提前下载这些文件,

资源接收的流程:

Send Request

Receive Response

Receive Data

Receive Data

….

Receive Data

Parse Stylesheet/Parse HTML

Receive Data

….

Finish Loading

js文件是先Finish Loading后再Evaluate Script。

js下载:js下载会阻塞其他资源的下载,chrome做了预加载优化,会提前下载资源

js执行:script标签会中断DOM树构建

Parse Stylesheet

构建CSSOM,跟html一样,渲染引擎也无法直接理解css内容,所以需要将 CSS 文本转换为渲染引擎可以理解的结构CSSOM,转换为CSSOM之后,js可以直接操作样式,也为下一步合成布局树提供样式信息。可以在控制台通过styleSheets查看:

img

Evaluate Script

执行js代码

Recalculate Style

计算 DOM 树中每个节点的样式,涉及css继承规则和层叠规则,这个阶段最终输出的内容是每个 DOM 节点的样式,可以在devtools的 ComputedStyle 内查看

CSSOM tree

Layout

生成可见节点的布局树,这个布局树中只包含DOM树中的可见节点,并计算出每个DOM 元素的几何位置信息。

截屏2022-02-04 下午6.13.59

Update Layer Tree

构建渲染图层树,什么情况下会为节点创建新图层:

  1. 页面的根元素

  2. 拥有层叠上下文属性的元素会被提升为单独的一层。

    明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。

  3. 需要剪裁(clip)的地方也会被创建为图层

    剪裁区域占一层,内容占一层,如果有滚动条,滚动单独占一层

<body>
  <p style="position:fixed;"></span>hello, <span>web performance</span> students</p>
  <div style="height: 100px;overflow: scroll;">
    <img src="https://cdn.ne-smalltown.com/453.jpg--ljg4NsMmh4S3skxKXaOaZsdjXjn0?imageView2/2/w/327/h/312/interlace/1"/>
  </div>
</body>

截屏2022-02-05 下午4.58.32

Paint

生成绘制列表,绘制列表用来记录绘制顺序和绘制指令,从下图可以看到图层p的绘制列表:

截屏2022-02-05 下午5.11.26

Composite Layers

将绘制列表提交给渲染进程中的合成线程

Compositor指标

记录渲染进程中合成线程的执行记录,合成线程主要负责将图层划分为图块,然后会向光栅化线程发送指令,由光栅化线程池来生成位图

Raster指标

记录渲染进程光栅化线程池中每个光栅化线程的执行记录,光栅化负责将图块转为位图,最小的执行单位是图块,光栅化线程回和GPU进程通信,利用GPU进程来实现快速栅格化。

GPU指标

执行raster指标中的快速光栅化任务生成位图,并保存在GPU内存中

Chrome_ChildIOThread指标

记录用户输入事件、网络事件、设备相关等事件

Interactions指标

记录用户的交互操作

一些例子

在Performance面板看css加载阻塞
script会阻塞DOM树构建,那css会阻塞DOM树构建吗?

因为js可以更改DOM,所以js代码一定会阻塞DOM树构建:

  1. 内部script代码:

    Parse HTML时遇到内部script代码时,会暂停DOM树构建,先执行js代码,执行完成之后再继续进行DOM树构建

  2. 外部script代码
    先通过网络进程下载script代码,下载后执行js代码,执行完成之后再继续进行DOM树构建

因为js也可以更改样式,所以css在某些情况下会阻塞js资源下载和执行,相当于阻塞了DOM树构建:

  1. 通过外部引入css资源,后面跟有嵌入js代码时:等待css资源下载和Parse Stylesheet,然后才能执行嵌入的js代码
  2. 通过外部引入css资源,后面根据有js外部引入资源时:因为css不会阻塞DOM树构建,所以会同时发起css和js资源请求,但无论请求完成的快慢,js资源请求都需要等待css资源下载并且在主线程完成Parse Stylesheet后,再执行js代码
帧优化

https://googlechrome.github.io/devtools-samples/jank/

层爆炸
长任务优化

因为页面渲染和 JS的 执行都在主线程中执行,会相互阻塞,如果 JS 有长时间(超过50ms)执行的 Task,就会长期占据主线程,阻塞渲染,导致页面卡顿。可以通过Performance面板找出长任务,优化长任务。

有两种常见的造成长任务的方式:

  1. 一段同步代码被递归调用或者处理一段复杂的逻辑导致耗时较长

    long task:

    let total = 100;
    function a() {
      let s = 0;
      for (let i = 0; i < total; i++) {
        let div = document.createElement('div');
        let span = document.createElement('span')
        div.innerHTML = `hello`;
        span.innerHTML = i
        div.appendChild(span)
        document.body.appendChild(div);
      }
    }
    a();
    

    截屏2022-02-07 下午5.04.18

    优化:

    let current = 0;
    let total = 100;
    let count = 10;
    function a() {
      let s = 0;
      for (let i = 0; i < count; i++) {
        let div = document.createElement('div');
        let span = document.createElement('span')
        div.innerHTML = `hello`;
        span.innerHTML = current++
        div.appendChild(span)
        document.body.appendChild(div);
      }
    }
    for(let j = 0; j < total; j+=count) {
      setTimeout(a);
    }
    

    截屏2022-02-07 下午5.01.48

  1. 一个Promise具有多个.then处理程序,由于Promise的then处理程序是在MicroTask队列中排队,并且将MicroTask队列中的任务连续执行,直到队列为空,它可能会导致当前任务执行超过50ms:

    long task:

    let current = 0;
    let count = 10;
    function a() {
      let s = 0;
      for (let i = 0; i < count; i++) {
        let div = document.createElement('div');
        let span = document.createElement('span');
        div.innerHTML = `hello`;
        span.innerHTML = current++;
        div.appendChild(span);
        document.body.appendChild(div);
      }
    }
    Promise.resolve()
      .then(a)
      .then(a)
      .then(a)
      .then(a)
      .then(a)
      .then(a)
      .then(a)
      .then(a)
      .then(a)
      .then(a);
    

    截屏2022-02-07 下午5.09.33

    优化:

    function a() {
      let s = 0;
      for (let i = 0; i < count; i++) {
        let div = document.createElement('div');
        let span = document.createElement('span');
        div.innerHTML = `hello`;
        span.innerHTML = current++;
        div.appendChild(span);
        document.body.appendChild(div);
      }
    }
    function b() {
      setTimeout(a, 0)
    }
    Promise.resolve()
      .then(b)
      .then(b)
      .then(b)
      .then(b)
      .then(b)
      .then(b)
      .then(b)
      .then(b)
      .then(b)
      .then(b);
    

截屏2022-02-07 下午5.14.59

使用webworker进行优化

参考

示例: https://googlechrome.github.io/devtools-samples/jank/

示例2: https://www.ne-smalltown.com/

  1. https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit#

  2. https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=en

  3. https://developer.chrome.com/docs/devtools/evaluate-performance/performance-reference/

  4. https://time.geekbang.org/column/article/118826

  5. https://w3c.github.io/longtasks/

  6. https://web.dev/rail/#response:-process-events-in-under-50ms

  7. https://javascript.plainenglish.io/how-to-optimize-long-tasks-blocking-javascript-in-browsers-d49508f72c9

父元素设置了filter,子元素设置定位属性,将根据父元素来定位

.container {
  display: inline-block;
  width: 200px;
  height: 200vh;
  border: 1px solid;
}

.container>div {
  position: fixed;
  width: 100px;
  height: 100px;
  background: red;
  color: #fff;
}
<div class="container">
  <div>I am fixed on scroll</div>
</div>

<div class="container" style="filter:grayscale(1);">
  <div>I move with the scroll</div>
</div>

下面属性都应用:

  • filter
  • transform ref
  • backdrop-filter ref
  • perspective ref
  • contain ref
  • transform-style ref
  • content-visibility ref
  • will-change when used with one of the above values