小谢第67问:用户输入一个URL到页面渲染完成具体发生了什么?
一、前言
当用户输入一个URL到页面渲染完成具体发生了什么?
先回顾一下之前写的一篇文章输入url到页面渲染全链路分析,主要分析了浏览器从加载ULR到服务器返回资源的过程,如果不太了解可以看看这篇(又增加了更详细的内容)
当浏览器发出请求,服务器返回对应的资源后,浏览器又做了哪些工作将字节流转化成漂亮的页面?
针对这个问题,需要对浏览器的工作原理进行深入的研究,才能清楚的知道浏览器在这个过程中,做了哪些操作,就能帮助我们在前端开发或前端性能优化的时候,针对某个环节采取不同的方法进行优化,而不是只会背诵减少http请求,合并背景图等。
上篇文章我们知道了,浏览器会通过网络进程请求网络资源,然后网络进程和渲染进程建立"管道",放入消息队列等待渲染线程进行处理。
二、渲染进程下的各个线程
现代浏览器是多进程架构,其中进程中又包含了多个线程,而处理html解析和页面渲染主要就发生在渲染进程中,就是我们常说的浏览器内核,渲染进程主要包括了GUI渲染线程、js引擎线程、事件触发线程、定时触发线程、合成线程、IO线程等
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引擎执行。
当渲染线程拿到返回的资源,会发生如下整个过程,下面我们来一一说明,具体的渲染流程图
- DOM解析
- 字节流词牌解析
- 转化Token
- DOM树构建
- CSSOM解析
- CSS令牌
- CSS格式化
- CSSOM构建
- 布局树
- DOM和CSSOM计算布局树
- 计算DOM节点的坐标,构建布局树
- 图层树
- 根据布局树构建图层树
- 绘制列表、合成
- 渲染引擎根据图层树,生成绘制列表,交给合成线程
- 合成线程将绘制列表生成图块
- 光栅化
- 进行光栅化操作,将图块合成位图,放入光栅化线程池
- 生成位图,优先生成视口附近的位图
- 显示
- 然后交给浏览器进程,通过浏览器组件绘制成图片到内存,显示出来
三、构建DOM树
这里通过浏览器开发者工具中的Performance
面板录制了页面加载过程中,浏览器执行的任务,可以清晰的看到有个解析HTML的任务,事件的加载和JS先解析再编译等。
其实后面有很多任务,但截图太多没法显示信息,就截图了一部分,可以自行测试。
这一步渲染线程获取资源后会获取返回头信息,如果头部信息存在标识content_type: html
,网络进程会和渲染线程建立通道,就像流水线一样,渲染引擎的HTML模块解析器,首先通过词解析,生成对应的Token,就是标记<StartTag>
和<EndTag>
,然后压入它维护的栈中,同时生成对应的Node节点,通过不断的对比开始节点和结束节点,最终构建出DOM树。
在词解析的过程中,如果发现了style和script的引用文件,浏览器另开启线程进行预下载。当DOM树构建完成,开始解析预下载的CSS。
在解析DOM的过程中,如果有JS脚本,渲染线程会停止工作,等待JS引擎的执行,所以说在文档头部引用JS会导致页面渲染卡顿。
上面这个是打印出来的DOM结构,这个结构给我们通过JS操作DOM提供了可能。
四、构建styleSheet样式表
这一步主要发生了:CSS解析、CSS标准化、styleSheets构建(CSSOM)
CSS主要有四个来源:
- 1、内联样式
- 2、style标签嵌入
- 3、link引入
- 4、js引入
在解析DOM的过程中,不管遇到哪种样式,渲染引擎都会在将CSS标准化完成后,加入到如下的StyleSheetList表中,这样就为后面我们操作CSS提供了便利,在控制台Console中可以打印出来document.styleSheets
其中CSS标准化就是将一些浏览器不能识别的语法标准化成可以识别的。比如
.demo {
font-size: 2em;
color: #000;
}
复制代码
转化成
.demo {
font-size: 28px;
color: rgb(0, 0, 0);
}
复制代码
五、布局树,计算样式和Node节点坐标
有了DOM树和StyleSheetss,就可以开始构建布局树了。具体就是遍历DOM节点,为每个节点计算样式,过程中隐藏的和不可见的元素是不会加入布局树的,这个过程包括:计算布局树和渲染布局树。
我们可以在Elements这里看到每个节点计算的样式。
这个过程中,我们发现构建布局树需要DOM和CSS样式表(这里可以理解成CSSOM)。
如果CSS下载时间过长,导致CSSOM没有下载或解析完成,渲染线程就会停下来等待CSS处理。
所以如果CSS文件比较大或者网络差,就会导致页面最终的渲染时间增加。
如果在加载DOM的过程中执行了JS代码,JS中又包含CSS,那么渲染线程也会等待CSS下载,这样也会影响渲染的时间。
六、图层树
构建完成了布局树,此时还不会进行绘制,渲染引擎会通过布局树构建图层树。
就像PS一样,每张图片都是由若干个图层覆盖,最后显示出来一副图片,图层树就是这样,将页面处理成为一个一个层级,每个节点都有所在的层级,如果没有就归属于父节点。
如图所示,在浏览器layers栏可以看到浏览器分层的效果
点击下面的绘制列表,拖动绘制步骤就可以重现绘制过程
常见的可以引起分层的样式有z-index
,DIV内容大于宽度出现的裁剪或出现滚动条
,Fix
,3D渲染
等
七、合成绘制列表、光栅化操作
构建完成图层树,渲染引擎会将图层转化成绘制列表,如上图所示,这个列表只有绘制指令。
接着渲染引擎会将绘制列表交给合成线程,合成线程会将绘制列表绘制成图块,然后执行光栅化操作,就是大页面分割成256*256
或512*512
的大小,然后生成位图,优先渲染页面的可视区域,这也是浏览器在渲染上做的优化。
渲染引擎会会维护一个光栅化线程池,一旦光栅化完成,就会将绘制指令交给浏览器进程。如果这一步操作使用GUI加速,那么后续操作也会在GUI进程中操作,这样就不会影响渲染进程,提高了绘制的速度。
八、绘制
合成线程将绘制位图的指令提交给浏览器进程后,浏览器进程会调用它的viz 的组件根据绘制指令将他们绘制到内存中,然后在页面显示出来。
这样整个过程就完成了。
九、重排和重绘
重排和重绘是面试中经常问到的一个知识点,如果开发中做动画比较多,那了解这方面的内容可以优化动画效果。
重排,就是当页面元素的几何属性改变时,会导致页面重新计算DOM树,然后引发后续的一系列操作。
重绘,就是当页面的颜色等属性变化,只会重新计算样式,然后执行合成操作,省略了DOM树计算的过程,就减少了渲染引擎的压力。
当然,如果你的修改即没有触发重排也没有触发重绘,那不就更快了吗?
对,所以CSS动画实现可以使用transform,只会在合成线程阶段处理,如果使用了GUI加速,也不会影响主进程,渲染就更快了。
下面是一些减少重排重绘的方法:
- 使用class批量处理样式,而不是频繁操作style
- 避免使用table布局
- 使用框架,框架使用虚拟DOM,通过算法减少操作DOM的频率
- 使用transform处理动画等