渲染主线程都在干什么

渲染主线程的task都安排了什么任务

  1. 还没拿到html的时候已经分配了任务给主线程,这个任务是先执行一下unload事件,然后在开始navigationStart发起导航任务

  2. 在请求过程中执行完了commitNavigationEnd任务后,就开始执行domloading了,渲染进程接收浏览器进程提交的HTML数据开始dom解析

  3. 在整个HTML解析的过程中也分配了performance.timing中关键时间点记录的任务

性能指标

  1. Main指标是记录了渲染主线程的执行记录
  2. Compositer指标记录了合成线程的执行记录
  3. Raster指标,光栅化线程池,用来让GPU执行光栅化的任务
  4. GPU指标,执行raster指标中的任务
  5. Chrome_ChildIOThread指标,记录用户输入事件,网络事件,设备相关等事件
  6. Network指标,页面中每个网络请求所消耗的时长
  7. Timings指标,用来记录关键的时间节点产生的数据信息FP,FCP,LCP等
  8. Frames指标,浏览器生成每帧的记录,
  9. Interactions指标,用来记录用户的交互操作

页面加载过程

  1. 导航阶段,该阶段主要从网络进程接收HTML响应头和HTML响应体
  2. 解析HTML阶段,该阶段主要是将接收到的HTML数据转换为DOM和CSSOM
  3. 生成可显示的位图阶段,该阶段主要是利用DOM和CSSOM,经过计算布局,生成层树(LayerTree)生成绘制列表(Paint)完成合成等操作,生成最终的图片

导航流程

用户发出URL请求到页面解析的过程,叫做导航

  1. 浏览器进程接收到用户的URL请求,浏览器进程便将该URL转发给网络进程
  2. 网络发起URL,解析完响应头数据,并将数据转发给浏览器进程
  3. 浏览器进程接收到网络进程的响应头数据后,发送提交导航CommitNavigation消息到渲染进程
  4. 渲染进程接收到提交导航的信息后,便开始接收HTML数据,接收数据的方式是直接和网络进程建立数据管道
  5. 最后渲染进程会想浏览器进程确认提交,这是告诉浏览器进程已经准备好接收和解析页面数据了
  6. 浏览器进程接收到渲染进程提交的消息后,便开始移除之前的旧文档,然后更新浏览器进程中的页面状态

页面准备

重定向:在header定义了重定向才会有这个过程,如果没有重定向,不会产生这个过程。

app cache:会先检查这个域名是否有缓存,如果有缓存就不需要DNS解析域名。这里的app是值应用程序application,不指手机app。

DNS解析:把域名解析成IP,如果直接用ip地址访问,不产生这个过程。

TCP连接:http协议是经过TCP来传输的,所以产生一个http请求就会有TCP connect,但是依赖于长连接,不会产生这个过程。

request header:请求头信息。

request body:请求体信息,比如get请求是没有请求体信息的,所以没有这个过程,这就是为什么把头跟体分开写的原因。

response header:响应头信息。

response body:响应体信息。
解析HTML结构

加载外部脚本和样式表文件:正常来说JS、css都是外部加载的,当然有不正常的人啊,比如我。
解析并执行脚本代码

构建与解析HTML DOM树:这个过程可以去了解下DOM树是怎样的就明白啦。
加载外部图片
页面加载完成,显示出来啦
1. 准备新页面耗时:fetchStart - navigationStart
2. 重定向时间:redirectEnd - redirectStart
3. App Cache时间:domainLookupStart - fetchStart
4. DNS解析时间:domainLookupEnd - domainLookupStart
5. TCP连接时间:connectEnd - connectStart
6. request时间:responseEnd - requestStart
7. 请求完毕到DOM树加载:domInteractive -responseEnd
8.构建与解析DOM树,加载资源时间:domCompleter -domInteractive
8. load时间:loadEventEnd - loadEventStart
9. 整个页面加载时间:loadEventEnd -navigationStart
10. 白屏时间:responseStart-navigationStart

let timing = performance.timing

// DNS 解析耗时
timing.domainLookupEnd - timing.domainLookupStart

// TCP 连接耗时
timing.connectEnd - timing.connectStart

// SSL 安全连接耗时
timing.connectEnd - timing.secureConnectionStart

// 网络请求耗时
timing.responseStart - timing.requestStart

// 数据传输耗时
timing.responseEnd - timing.responseStart

// DOM 解析耗时
timing.domInteractive - timing.responseEnd

// 资源加载耗时
timing.loadEventStart - timing.domContentLoadedEventEnd 

/* 关键性能指标 */

// 首包时间
timing.responseStart - timing.domainLookupStart

// 白屏时间
timing.responseStart - timing.navigationStart 

// 首次可交互时间
timing.domInteractive - timing.requestStart 

// HTML 加载完成时间, 即 DOM Ready 时间
timing.domContentLoadedEventEnd - timing.navigationStart

// 页面完全加载时间
timing.loadEventStart - timing.navigationStart

导航过程中分配的任务

  1. Send request表示网络请求已发送
  2. Receive Response,表示已经接收到HTTP响应头
  3. 执行DOM事件,pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。
  4. Recive Data,其他事件处理完成之后,接下来接收HTML数据,Recive Data表示数据已经被接收,如果数据量大会存在多个Recive Data过程
  5. Finish Load,所有数据接收完成后执行,该过程表示网络请求已经完成

解析HTML数据阶段

  1. 解析HTML,遇到js,先编译js,执行js,执行全局代码时,v8会先构造annoymous过程,annoymous执行中执行js中的函数,由于修改了dom又强制执行了parseHTML过程又生成新的dom
  2. DOM生成完毕后,会触发相关的DOM事件
  3. DOM生成完毕后,计算样式表,这就是生成CSSOM的过程

生成可显示位图阶段

  1. 生成DOM和CSSOM之后,渲染主线程首先执行了一些dom事件,
  2. 执行布局,layout
  3. 然后更新层树(Layer Tree),这个过程对应Update LayerTree
  4. 有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程叫做paint
  5. 准备每层的绘制列表之后,就需要利用绘制列表生成相应的位图,这个过程叫做Composite Layers
  6. Composite Layers之后主线程任务完全交给合成线程来执行
首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。

微任务检查点

  1. 在执行全局js代码时,创建了anonymous执行环境,然后运行了微任务队列任务

<body>
    <script>
        let p = new Promise(function (resolve, reject) {
            resolve("成功!"); 
        });


        p.then(function (successMessage) {
            console.log("p! " + successMessage);
        })


        let p1 = new Promise(function (resolve, reject) {
            resolve("成功!"); 
        });


        p1.then(function (successMessage) {
            console.log("p1! " + successMessage);
        })
    </script>
</bod>

  1. 多个脚本执行微任务会影响解析时间

<body>
    <script>
        let p = new Promise(function (resolve, reject) {
            resolve("成功!");
        });


        p.then(function (successMessage) {
            console.log("p! " + successMessage);
        })
    </script>
    <script>
        let p1 = new Promise(function (resolve, reject) {
            resolve("成功!");
        });


        p1.then(function (successMessage) {
            console.log("p1! " + successMessage);
        })
    </script>
</body>

  1. 在同步任务之后,开始检查微任务
       let p = new Promise(function (resolve, reject) { 
            resolve("成功!"); 
        }); 
        p.then(function (successMessage) {
             console.log("p! " + successMessage); 
        })
        for(let i = 0; i < 10000; i++){
            console.log(i)
        }
     

audit 性能指标

  1. 首次绘制(First Paint)渲染进程确认要渲染当前的请求后,渲染进程会创建一个空白页面,我们把创建空白页面的这个时间点称为FP
  2. 首次有效绘制(First Meaningfull Paint)使用FCP来代替,因此js是关键资源,关键资源会阻塞渲染,当页面绘制第一个元素时,这个时候称为FCP
  3. 首屏时间(Speed Index)就是指LCP,继续执行js脚本,当首屏内容完全绘制时,这个时间点称为LCP,首屏时间值越大,那么加载速度越慢
  4. 首次CPU空闲时间(First CPU Idle)也叫First Interactive表示页面达到最小化可交互时间,也就是说并不需要等到页面上所有元素可交互,只要可以堆大部分用户输入做出响应即可,缩短CPU空闲时间,就需要尽可能的加载完关键资源,尽可能快的渲染出首屏内容,因此和FCP优化是一样的
  5. 完全可交互时间(Time To Interactive)简称 TTI,它表示页面中所有元素都达到了可交互的时长。简单理解就这时候页面的内容已经完全显示出来了,所有的 JavaScript 事件已经注册完成,页面能够对用户的交互做出快速响应,通常满足响应速度在 50 毫秒以内。如果要解决 TTI 时间过久的问题,我们可以推迟执行一些和生成页面无关的 JavaScript 工作。
  6. 最大估计输入延时(Max Potential First Input Delay)这个指标是估计你的 Web 页面在加载最繁忙的阶段, 窗口中响应用户输入所需的时间,为了改善该指标,我们可以使用 WebWorker 来执行一些计算,从而释放主线程。另一个有用的措施是重构 CSS 选择器,以确保它们执行较少的计算。

页面生命周期

  1. 加载阶段,是指发出请求到渲染完整页面的过程,影响这个阶段的主要因素有网络和js脚本
  2. 交互阶段,页面加载完成到用户交互的整合过程,影响这个阶段的主要因素是js脚本
  3. 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作

加载阶段

html,css,js在网页加载过程都会阻塞首次渲染,能阻塞网页首次渲染的资源称为关键资源

  1. 关键资源个数
  2. 关键资源大小
  3. 请求关键资源需要多少个RTT
如何减少关键资源的个数?一种方式是可以将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。另一种方式,如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 async 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。当 JavaScript 标签加上了 async 或者 defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了。如何减少关键资源的大小?可以压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过前面讲的取消 CSS 或者 JavaScript 中关键资源的方式。如何减少关键资源 RTT 的次数?可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。

交互阶段

js通过修改DOM或者CSSOM都会触发生成一个新的帧,css修改也会触发生成新的帧。如果在计算样式阶段发现有布局信息的修改,那么就会触发重排操作,然后触发后续渲染流水线的一系列操作,这个代价是非常大的。同样如果在计算样式阶段没有发现有布局信息的修改,只是修改了颜色一类的信息,那么就不会涉及到布局相关的调整,所以可以跳过布局阶段,直接进入绘制阶段,这个过程叫重绘。不过重绘阶段的代价也是不小的。还有另外一种情况,通过 CSS 实现一些变形、渐变、动画等特效,这是由 CSS 触发的,并且是在合成线程上执行的,这个过程称为合成。因为它不会触发重排或者重绘,而且合成操作本身的速度就非常快,所以执行合成是效率最高的方

  1. 减少js脚本执行时间,如果js函数执行时间过长,这就严重霸占了执行其他渲染任务的时间,将一次执行的函数分解为多个任务,使得每次执行的时间不要过久

  2. 避免强制同步布局,通过DOM接口执行添加元素或者删除元素操作后,需要重新计算样式和布局正常情况


<html>
<body>
    <div id="mian_div">
        <li id="time_li">time</li>
        <li>geekbang</li>
    </div>

    <p id="demo">强制布局demo</p>
    <button onclick="foo()">添加新元素</button>

    <script>
        function foo() {
            let main_div = document.getElementById("mian_div")      
            let new_node = document.createElement("li")
            let textnode = document.createTextNode("time.geekbang")
            new_node.appendChild(textnode);
            document.getElementById("mian_div").appendChild(new_node);
        }
    </script>
</body>
</html>

在当前任务,在执行完foo后执行了立即执行了重绘重排,这就是强制布局

    function foo() {
            let main_div = document.getElementById("mian_div")
            let new_node = document.createElement("li")
            let textnode = document.createTextNode("time.geekbang")
            new_node.appendChild(textnode);
            document.getElementById("mian_div").appendChild(new_node);
            console.log(main_div.offsetHeight)
        }

避免强制同步布局


function foo() {
    let main_div = document.getElementById("mian_div")
    //为了避免强制同步布局,在修改DOM之前查询相关值
    console.log(main_div.offsetHeight)
    let new_node = document.createElement("li")
    let textnode = document.createTextNode("time.geekbang")
    new_node.appendChild(textnode);
    document.getElementById("mian_div").appendChild(new_node);
    
}
  1. 避免布局抖动,在一次js执行过程中,多次执行强制布局和抖动操作,尽量不要在修改dom结构时再去查询一些相关的值

function foo() {
    let time_li = document.getElementById("time_li")
    for (let i = 0; i < 100; i++) {
        let main_div = document.getElementById("mian_div")
        let new_node = document.createElement("li")
        let textnode = document.createTextNode("time.geekbang")
        new_node.appendChild(textnode);
        new_node.offsetHeight = time_li.offsetHeight;
        document.getElementById("mian_div").appendChild(new_node);
    }
}
  1. 合理利用css合成动画
    合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript 或者一些布局任务占用,CSS 动画依然能继续执行。所以要尽量利用好 CSS 合成动画,如果能让 CSS 处理动画,就尽量交给 CSS 来操作。另外,如果能提前知道对某个元素执行动画操作,那就最好将其标记为 will-change,这是告诉渲染引擎需要将该元素单独生成一个图层。
.slide {
  will-change: transform;
}
  1. 避免频繁的垃圾回收

JavaScript 使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。这样当垃圾回收操作发生时,就会占用主线程,从而影响到其他任务的执行,严重的话还会让用户产生掉帧、不流畅的感觉。

  1. 频繁垃圾回收不仅霸占大量的主线程的时间,而且还占用大量的内存,严重阻塞了后续页面的渲染

function strToArray(str) {
  let i = 0
  const len = str.length
  let arr = new Uint16Array(str.length)
  for (; i < len; ++i) {
    arr[i] = str.charCodeAt(i)
  }
  return arr;
}


function foo() {
  let i = 0
  let str = 'test V8 GC'
  while (i++ < 1e5) {
    strToArray(str);
  }
}


foo()
posted @ 2021-06-29 16:58  pluscat  阅读(589)  评论(0编辑  收藏  举报