关于浏览器渲染

浏览器组成

  • 用户界面 :展示除标签页窗口之外的其他用户界面内容

  • 浏览器引擎:在用户界面和渲染引擎之间传递数据,具有数据存储持久层(存储cookie等各种数据)

  • 渲染引擎(内核):负责渲染用户请求的页面内容,包括许多小的模块,如网络模块、js解释器


主流浏览器内核

IE Firefox Safari Chrome Opera
Trident Gecko Webkit Webkit-->Blink Blink

进程(process)与线程(thread)

  1. 当启动一个程序时,就会创建一个进程来执行任务代码,同时为该进程分配内存空间,该应用程序的状态都保存在该内存空间里,应用程序关闭则回收内存空间。可以启用多个进程来执行任务,每个进程相互独立,一个进程出现故障,不会影响其他进程,数据通过进程间通信管道IPC来传递。

  2. 进程可以将任务再分为更小的线程,多个线程并行执行不同的任务,同一进程下的多个线程是可以通信共享数据的。

  • 多进程结构
    • 浏览器进程
      ​ 控制浏览器除标签页外的用户界面,包括地址栏、书签栏、前进后退按钮,以及负责与浏览器的其他进程协调工作

    • 缓存进程

    • 网络进程
      发起接受网络请求

    • GPU进程
      负责整个浏览器页面的渲染

    • 插件进程
      负责网站使用的所有插件,如flash(并不是浏览器商店安装的扩展)

    • 渲染器进程(Render Thread)
      控制显示tab标签内所有内容,浏览器默认情况下会为每个标签页都创建一个进程(由进程模式决定,可修改),核心任务就是把html、css、js、image等资源渲染为用户可以交互的web页面


HTML是如何渲染到页面上的(chrome为例)

后面有整个过程的简单总结

1.渲染前阶段

当你在浏览器地址栏输入地址时,浏览器进程的UI线程会捕捉输入内容,若输入为网址,UI线程则会启动一个网络线程发送请求进行 DNS域名解解,开始连接服务器,服务器将数据返回。

网络线程获取到数据后,(会经过谷歌内部的一套站点安全系统SafeBrowsing来检查站点是否为恶意站点)当返回数据准备完毕时,网络线程会通知UI线程已经准备好。然后UI线程会创建一个渲染器进程来渲染页面,浏览器进程通过IPC管道将数据传递给渲染器进程,进入渲染流程。


2.渲染阶段

2.1. 生成DOM结构树

渲染器进程接收到html数据,在内存中开辟一块栈内存(stack)来给代码的执行提供环境。渲染器进程解析执行html数据,构造DOM数据结构,HTML首先经过Tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记,进行DOM树构造,构造过程中创建document对象,然后以document为根节点的DOM树不断进行修改,向其中添加各种元素。

html代码中往往会引入一些额外资源,图片、css这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为其不会影响DOM的生成,但当遇到script标签,将停止html解析流程,转而去加载解析并执行js,因为js有可能会改变当前的页面结构(所以script标签要放在合适的位置,或使用async或defer属性来异步加载执行)。

2.2 生成CSS样式树

主线程再去解析css并确定每个DOM节点的计算样式生成CSS样式树,即使没有提供自定义css样式,浏览器也会有自己默认的样式表。

2.3 Layout布局,生成Render Tree

在得到dom树结构和节点样式之后,还需要知道每个节点要放置在页面上的哪个位置,即坐标和边框尺寸。主线程通过遍历dom树和css样式树生成Layout Tree(Render Tree)

DOM Tree 和 Layout Tree并不是一一对应的,如:设置了display:none的节点不会显示在Layout Tree上,而在before属性添加了content的元素,content中内容会出现在Layout Tree上,而不会出现在DOM Tree中,因为DOM是通过html解析获得,并不关系样式,Layout Tree则是根据DOM和计算好的样式生成,Layout Tree和最后显示在屏幕上的节点是对应的。

2.4 绘制

经过前面步骤后,我们已经知道的每个节点的位置、大小和样式,下面还需要指定以什么样的顺序绘制(paint)节点,比如z-index属性会影响节点的绘制层级关系,若我们按照DOM的层级结构绘制,则会导致错误的渲染。因此主线程遍历Layout Tree创建一个绘制记录表(paint record),记录节点绘制的顺序,此阶段成为绘制。

主线程遍历Layout Tree生成Layer Tree并确定绘制顺序后,将这些信息传递给合成器线程,合成器线程按照某种规则分图层,将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将它们切分为许多图块(titles)发送给栅格化线程(raster thread)进行栅格化,之后存储在GPU内存中。

当图块栅格化完成后,合成器线程将收集“draw quads”图块信息,这些信息里记录了图块在内存中的位置和在页面上的绘制位置,根据这些信息合成器线程生成了一个合成器帧,然后合成器帧通过IPC传送给浏览器进程,接着浏览器进程将合成器帧传送给GPU渲染展示到屏幕上

2.5 当页面发生变化时
  • 页面滚动时,则会生成一个新的合成器帧传给GPU,再次渲染到屏幕上。

  • 当改变尺寸位置属性时,会重新进行样式计算,布局绘制以及后面的所有流程,这种行为称为重排(回流)

  • 当改变颜色等其他属性时,不会重新触发布局,但依旧会触发计算样式,称为重绘

总结:

  1. 浏览器进程中的网络线程获取到html数据后,通过IPC通信管道传递给渲染进程的主线程。
  2. 主线程:
    解析html文档构造DOM树,然后进行样式计算,根据DOM树和计算好的样式生成layout tree,通过遍历layout tree生成绘制顺序表,接着遍历layout tree生成layer tree,然后主线程将layer tree和绘制信息一起传递给合成器线程。
  3. 合成器线程:
    按规则进行分图层,并把图层分为更小的图块(tiles)传递给栅格化线程。
  4. 栅格化线程:
    将图块进行栅格化,栅格化完成后获得“draw quads”图块信息,传回给合成器线程。
  5. 合成器线程:
    根据图块信息合成了一个合成器帧,将该合成器帧通过IPC传回给浏览器进程。
  6. 浏览器进程再将合成器帧传递给GPU进行渲染,最后展示到页面上。

性能优化

重排和重绘都会占用主线程,同时js也运行在主线程,就会出现抢占时间的问题
当屏幕刷新率为每秒60帧(16ms)时才不会感到卡顿,若运行动画时还有大量的js任务要执行,当在一帧的时间内布局和绘制结束后还有剩余时间,js就会拿到主线程的使用权,而当js执行时间过长时就会导致在下一帧开始js没有及时归还主线程,导致下一帧动画没有按时渲染出现卡顿。

setInterval定时器动画的缺点:

  1. 任务被放在异步队列,只有当主线程 任务执行完后才会执行异步队列中的任务,因此执行事件总比设置的事件要晚。
  2. 定时器设定的事件间隔不一定与屏幕刷新率相同,若屏幕刷新率为每秒60帧,而定时器每秒执行60次以上,那么必然会丢帧,引起卡顿。
1. requestAnimationFrame()

此方法要求浏览器在下次重绘之前调用指定的回调函数更新动画,刷新频率为每秒60帧,会在每一帧准时调用。通过api的回调,可以把JS运行任务分割成一些更小的任务块,在每一帧时间用完前暂停js执行,归还主线程。
使用:

        function move(){
            var box = document.querySelector('.box');
            box.style.left = box.offsetLeft + 20 + 'px';
            if(box.offsetLeft < 700){
                window.requestAnimationFrame(move);
            }
        }
        move();

返回值为一个整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给window.cancelAnimationFrame()以取消回调函数。

2. CSS3硬件(GPU)加速

transform/opcity/filter...属性会触发硬件加速,不会引发重排和重绘。
通过transform属性实现的动画直接运行在合成器线程和栅格化线程,不会受到主线程中js执行的影响。更重要的是,transform动画不经过重绘重排和样式计算,所以节省了很多运算时间,方便实现复杂的动画。
(GPU加速 标准方法:will-change
will-change: transforn; 提前告诉浏览器将关于transform的属性在单独的层变换)

3. 分离读写操作(浏览器的渲染队列机制)
        box.style.width = '200px';
        box.style.height = '200px';
        box.style.margin = '10px';
        console.log(box.clientWidth);

上述代码只触发一次回流,但若将console.log(box.clientWidth);插入到写入样式之间,则会触发两次,会触发刷新渲染队列的还有offTop、offWidth、clientTop、clientWidth、scrollTop、scrollWidth、getComputedStyle、currentStyle...

4.样式集中改变
        box.style.cssText = 'width:20px;height:20px';
        box.className = 'box';
5. 元素批量添加
  • 使用文档片段:内存中临时保存多个平级子元素的虚拟父元素,通过createDocumentFragment()创建,其并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流
        var frag = document.createDocumentFragment();
        for(var i = 0;i < 5;i++){
            var li = document.createElement('li');
            frag.appendChild(li);
        }
        box.appendChild(frag);
  • 模板字符串拼接
        var str = '';
        for(var i = 0;i < 5;i++){
            str += '
  • '; } box.innerHTML = `
    ${str}
    `
    6. 避免table布局和css的JavaScript表达式

    参考视频:《浏览器是如何运作的?》

    你可能感兴趣的:(关于浏览器渲染)