浏览器渲染原理解读和实践(二)

1,165 阅读9分钟

一、前言

当用户输入一个URL到页面渲染完成具体发生了什么?

先回顾一下之前写的一篇文章输入url到页面渲染全链路分析,主要分析了浏览器从加载ULR到服务器返回资源的过程,如果不太了解可以看看这篇(又增加了更详细的内容)

当浏览器发出请求,服务器返回对应的资源后,浏览器又做了哪些工作将字节流转化成漂亮的页面?

针对这个问题,需要对浏览器的工作原理进行深入的研究,才能清楚的知道浏览器在这个过程中,做了哪些操作,就能帮助我们在前端开发或前端性能优化的时候,针对某个环节采取不同的方法进行优化,而不是只会背诵减少http请求,合并背景图等。

上篇文章我们知道了,浏览器会通过网络进程请求网络资源,然后网络进程和渲染进程建立"管道",放入消息队列等待渲染线程进行处理。

二、渲染进程下的各个线程

现代浏览器是多进程架构,其中进程中又包含了多个线程,而处理html解析和页面渲染主要就发生在渲染进程中,就是我们常说的浏览器内核,渲染进程主要包括了GUI渲染线程、js引擎线程、事件触发线程、定时触发线程、合成线程、IO线程等

image.png

1、渲染线程

渲染线程主要负责页面的渲染工作,解析html、css、构建布局树、绘制图层等操作

2、JS引擎线程

JS引擎线程是浏览器用来执行js的解释器,常见的一种实现方式就是V8引擎。

JS引擎线程和渲染线程互斥,即同时只能有一个线程在执行。

这是因为JS可以对DOM节点进行增删改,所以如果在渲染的过程中,JS同时修改了DOM,就会不断的重复渲染,

所以JS引擎在设计之初就设计了两者互斥,当JS引擎工作的时候,渲染线程就挂起等待,

这就导致了我们经常遇到的一个问题,当js执行时间过长会造成页面卡顿。

3、事件触发线程

我们知道JS是靠事件驱动的语言,它是单线程,异步执行的。主要处理浏览器的各种事件,比如点击事件,移动事件,然后将事件放入任务队列末尾等待JS引擎执行。

4、定时触发线程

主要负责处理JS中的定时器,因为JS引擎是单线程的,可能存在阻塞的情况,所以再开一个线程负责可以保证定时器的准确性。

当然我们通常通过回调函数执行定时的事件,而回调事件触发后会被加入任务队列末尾等待执行,所以如果JS引擎阻塞后,具体的执行时间还是会有误差。

5、异步请求线程

当有XMLHttpRequest请求时,会新开线程发出请求,等返回状态变更时,会触发回调事件,将事件放入任务队列等待JS引擎执行。

当渲染线程拿到返回的资源,会发生如下整个过程,下面我们来一一说明,具体的渲染流程图 image.png

  • DOM解析
    • 字节流词牌解析
    • 转化Token
    • DOM树构建
  • CSSOM解析
    • CSS令牌
    • CSS格式化
    • CSSOM构建
  • 布局树
    • DOM和CSSOM计算布局树
    • 计算DOM节点的坐标,构建布局树
  • 图层树
    • 根据布局树构建图层树
  • 绘制列表、合成
    • 渲染引擎根据图层树,生成绘制列表,交给合成线程
    • 合成线程将绘制列表生成图块
  • 光栅化
    • 进行光栅化操作,将图块合成位图,放入光栅化线程池
    • 生成位图,优先生成视口附近的位图
  • 显示
    • 然后交给浏览器进程,通过浏览器组件绘制成图片到内存,显示出来

三、构建DOM树

image.png

这里通过浏览器开发者工具中的Performance面板录制了页面加载过程中,浏览器执行的任务,可以清晰的看到有个解析HTML的任务,事件的加载和JS先解析再编译等。

其实后面有很多任务,但截图太多没法显示信息,就截图了一部分,可以自行测试。

image.png

这一步渲染线程获取资源后会获取返回头信息,如果头部信息存在标识content_type: html,网络进程会和渲染线程建立通道,就像流水线一样,渲染引擎的HTML模块解析器,首先通过词解析,生成对应的Token,就是标记<StartTag><EndTag>,然后压入它维护的栈中,同时生成对应的Node节点,通过不断的对比开始节点和结束节点,最终构建出DOM树。

在词解析的过程中,如果发现了style和script的引用文件,浏览器另开启线程进行预下载。当DOM树构建完成,开始解析预下载的CSS。

在解析DOM的过程中,如果有JS脚本,渲染线程会停止工作,等待JS引擎的执行,所以说在文档头部引用JS会导致页面渲染卡顿。

image.png

上面这个是打印出来的DOM结构,这个结构给我们通过JS操作DOM提供了可能。

四、构建styleSheet样式表

这一步主要发生了:CSS解析、CSS标准化、styleSheets构建(CSSOM)

CSS主要有四个来源:

  • 1、内联样式
  • 2、style标签嵌入
  • 3、link引入
  • 4、js引入

在解析DOM的过程中,不管遇到哪种样式,渲染引擎都会在将CSS标准化完成后,加入到如下的StyleSheetList表中,这样就为后面我们操作CSS提供了便利,在控制台Console中可以打印出来document.styleSheets

image.png

其中CSS标准化就是将一些浏览器不能识别的语法标准化成可以识别的。比如

.demo {
    font-size: 2em;
    color: #000;
}

转化成

.demo {
    font-size: 28px;
    color: rgb(0, 0, 0);
}

五、布局树,计算样式和Node节点坐标

有了DOM树和StyleSheetss,就可以开始构建布局树了。具体就是遍历DOM节点,为每个节点计算样式,过程中隐藏的和不可见的元素是不会加入布局树的,这个过程包括:计算布局树和渲染布局树。

image.png

我们可以在Elements这里看到每个节点计算的样式。

这个过程中,我们发现构建布局树需要DOM和CSS样式表(这里可以理解成CSSOM)。

如果CSS下载时间过长,导致CSSOM没有下载或解析完成,渲染线程就会停下来等待CSS处理。

所以如果CSS文件比较大或者网络差,就会导致页面最终的渲染时间增加。

如果在加载DOM的过程中执行了JS代码,JS中又包含CSS,那么渲染线程也会等待CSS下载,这样也会影响渲染的时间。

六、图层树

构建完成了布局树,此时还不会进行绘制,渲染引擎会通过布局树构建图层树。

就像PS一样,每张图片都是由若干个图层覆盖,最后显示出来一副图片,图层树就是这样,将页面处理成为一个一个层级,每个节点都有所在的层级,如果没有就归属于父节点。

如图所示,在浏览器layers栏可以看到浏览器分层的效果

image.png

点击下面的绘制列表,拖动绘制步骤就可以重现绘制过程

常见的可以引起分层的样式有z-index,DIV内容大于宽度出现的裁剪或出现滚动条,Fix,3D渲染

七、合成绘制列表、光栅化操作

构建完成图层树,渲染引擎会将图层转化成绘制列表,如上图所示,这个列表只有绘制指令。

接着渲染引擎会将绘制列表交给合成线程,合成线程会将绘制列表绘制成图块,然后执行光栅化操作,就是大页面分割成256*256512*512的大小,然后生成位图,优先渲染页面的可视区域,这也是浏览器在渲染上做的优化。

渲染引擎会会维护一个光栅化线程池,一旦光栅化完成,就会将绘制指令交给浏览器进程。如果这一步操作使用GUI加速,那么后续操作也会在GUI进程中操作,这样就不会影响渲染进程,提高了绘制的速度。

八、绘制

合成线程将绘制位图的指令提交给浏览器进程后,浏览器进程会调用它的viz 的组件根据绘制指令将他们绘制到内存中,然后在页面显示出来。

这样整个过程就完成了。

九、重排和重绘

重排和重绘是面试中经常问到的一个知识点,如果开发中做动画比较多,那了解这方面的内容可以优化动画效果。

重排,就是当页面元素的几何属性改变时,会导致页面重新计算DOM树,然后引发后续的一系列操作。

重绘,就是当页面的颜色等属性变化,只会重新计算样式,然后执行合成操作,省略了DOM树计算的过程,就减少了渲染引擎的压力。

当然,如果你的修改即没有触发重排也没有触发重绘,那不就更快了吗?

对,所以CSS动画实现可以使用transform,只会在合成线程阶段处理,如果使用了GUI加速,也不会影响主进程,渲染就更快了。

下面是一些减少重排重绘的方法:

  • 使用class批量处理样式,而不是频繁操作style
  • 避免使用table布局
  • 使用框架,框架使用虚拟DOM,通过算法减少操作DOM的频率
  • 使用transform处理动画等

3、优化方法