现代浏览器网页渲染原理简介
0.为什么要理解浏览器的工作原理
- 为了写出更好的代码和提供更好的用户体验
简化的浏览器结构图:
- 用户界面
- 用于展示除标签页窗口之外的其他用户界面的内容
- 浏览器引擎
- 用于再用户界面和渲染引擎之间传递数据
- 渲染引擎(重点,常称为浏览器的内核)
- 负责渲染用户请求的页面内容
- 下面还有很多小的功能模块
内核使用:
1.目前的浏览器是个多进程结构
- 浏览器进程:控制除标签页外的用户界面,包括地址,书签,后退,前进按钮等,以及负责与浏览器其他进程负责协调工作
- 缓存进程:Cookie、sessionStorage。。。
- 网络进程:发起网络请求
- 渲染器进程:控制每个Tab标签内的显示内容。默认情况下浏览器可能会为每个标签页都创建一个进程
- 可能:是与打开Chrome时选择的进程模型有关。chromium的官方文档中说明了chrome一共有4种进程模型
- GPU进程:渲染
- 插件进程:内置插件
早期的浏览器是一个单进程结构:
如一个浏览器进程中:
- 页面线程负责页面
- JS线程负责JS代码
- 以及其他线程
因此会出现很多问题,如:
- 不稳定:一个线程的卡死可能导致整个进程出现问题。如打开多个标签页,其中一个标签页卡死,可能会导致整个浏览器无法正常运行
- 不安全:浏览器之间的线程是可以互通数据的,可能会通过JS访问其他线程的数据
- 不流畅:运行效率低
2.前端层面输入一个地址然后按回车经历的过程
(1)数据请求部分
(2)数据处理部分
(chrome)当网络线程获取到数据后,会通过SafeBrowsing(是谷歌内部的一套站点安全系统,通过检测该站点的数据是否安全,如看该IP是否在谷歌的黑名单内)来检查站点是否是恶意站点。如果是则显示警告页面,但还是可以继续访问。
- 当返回数据下载完毕并且安全校验通过时,网络线程会通知UI线程已经准备好了。
- UI线程会创建一个:
- 渲染器进程(Renderer Thread)来渲染页面:把html,css,js,image等资源渲染成用户可以交互的web页面。
- 浏览器进程通过IPC管道将数据传递给渲染器进程,进入渲染流程:
- 渲染器进程的主线程将html进行解析,构造DOM数据结构。
- html首先经过Tokeniser标记化,通过词法分析,将输入的html内容解析成多个标记,根据解析后的标记进行DOM树构造
- 在DOM树构造过程中会创建document对象,以document为根节点的DOM树不断修改,向其中添加各种元素。
- html代码中往往会引入其他资源,如图片、CSS、js等:
- 图片和CSS资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为它们不会影响DOM的生成
- 但是当HTML解析过程中遇到script标签就会停止html解析流程,转而去加载解析并执行JS(因为JS可能会改变当前页面的HTML结构)
- 渲染器进程(Renderer Thread)来渲染页面:把html,css,js,image等资源渲染成用户可以交互的web页面。
- Layout布局:在知道DOM节点和每个节点的样式后,接下来需要知道每个节点需要放到页面上的哪个位置(坐标及占用区域)。这个阶段称为Layout布局:
- 主线程通过遍历dom和计算好的样式来生成Layout Tree。Layout Tree上的每个节点都记录了x,y坐标和边框尺寸
-
- 绘制(paint):(绘制记录表)
- 我们需要知道以什么样的顺序绘制。为了保证在屏幕上展示正确的层级。
- 主线程遍历layout tree创建一个绘制记录表。该表记录了绘制的顺序。这个阶段被称为绘制
- 合成(Composting)
- 知道了绘制顺序,可以把这些信息转化为像素点显示在屏幕上的时候了
- 早期的chrome栅格化会导致展示延迟
- 目前的处理方案是合成。
- 合成是一种将页面上各个部分分成多个图层,分别对其栅格化,并在合成器线程(Compositor Thread)中单独进行页面合成的技术。
- 即:把页面所有的元素按照某种既定的规则进行分图层。并把图层都栅格化好了。然后只需要把可视区的内容组合成一帧展示给用户即可。
- 绘制(paint):(绘制记录表)
-
-
-
- 由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为多个图块(tiles)。
- 再将每个图块发送给栅格化线程,栅格线程栅格每个图块,并将其存储在GPU内存中
- 当图片栅格化完成后,合成器线程将收集称为draw quads的图块信息(记录了图块在内存中的位置和在页面中的哪个位置绘制图块的信息)
- 通过图块信息,合成器线程生成了一个合成器帧,然后合成器帧通过IPC传送给浏览器进程
- 浏览器进程将合成器帧传送到GPU,GPU渲染展示到屏幕上
- 此时如果页面发生变化,如滚动了页面,都会生成一个新的合成器帧
- 新的帧再传给GPU,然后再次渲染到屏幕上
-
-
- 注意:
- Layout Tree和Dom tree并不是一一对应的
- 如设置了display:none。的节点不会出现在Layout tree 上,而在before伪类中添加了content值的元素,content里的内容会出现在Layout tree上,不会出现在DOM树里。
- 这是因为DOM是通过HTML解析获得,并不关心样式。而layout tree是通过DOM树和计算好的样式来生成。layout tree是和最后展示在屏幕上的节点是对应的
3.汇总:
- 浏览器进程中的网络线程请求获取到html数据后,通过IPC(进程间通信管道)将数据传给渲染器进程的主线程
- 主线程将html解析构造DOM树,然后进行样式计算。根据DOM树和生成好的样式生成Layout Tree
- 通过遍历Layout Tree生成绘制顺序表,接着生成Layer tree。
- 然后主线程将Layer Tree和绘制顺序信息一起传给合成器线程。
- 合成器线程按规则进行分图层,并把图层分为更小的图块(tiles)传给栅格线程进行栅格化
- 栅格化完成后,合成器线程会获得栅格线程传过来的"draw quads"图块信息
- 根据这些信息,合成器线程上合成了一个合成器帧,然后将该合成器帧通过IPC传回给浏览器进程
- 浏览器进程再传到GPU进行渲染。这样就展示到屏幕上了
- 重排:当我们改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style),布局(Layout)绘制(Paint)以及后面的所有流程
- 重绘:当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制。
- 说明:
- 重排和重绘都会占用主线程,JS也会运行在主线程上,因此会出现抢占执行时间的问题
- 当页面以每秒60帧的刷新率才不会感觉到卡顿。如果在运行动画时还有大量的JS任务需要执行。因为布局、绘制和JS执行都是在主线程运行的
- 当在一帧的时间内布局和绘制结束后,还有剩余时间,JS就会拿到主线程的使用权,如果JS执行时间过长就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现卡顿
- 处理:
- requestAnimationFrame()
- 使用Transfrom:通过Transform实现的动画不需要经过布局绘制、样式计算等操作。所以实现了很多运算时间
4.动画卡顿的原因
例如用JS写了一个不断重排重绘的动画,浏览器需要在每一帧都运行样式计算布局和绘制的操作
- 如果在运行动画时还有大量的JS任务需要执行,由于布局、绘制、JS执行都是在主线程执行的
- 当在一帧的时间内布局和绘制结束后还有剩余时间,JS就会拿到主线程的使用权
- 如果JS执行的时间过长,就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会导致卡顿
- 解决:
- requestAnimationFrame():会在每一帧被调用,可以把JS任务分成一些更小的任务快,在每一帧时间用完前暂停JS执行归还主线程——React Fiber
- 栅格化的过程是不占用主线程的,只在合成器线程和栅格线程中运行——transform实现的动画不会通过布局和绘制,而是直接运行在合成器线程和栅格化线程中,不会影响主线程JS的执行
(1)影响回流的操作
- 添加/删除元素
- 操作styles
- display:none
- offsetLeft,scrollTop,clientWidth
- 移动元素的位置
- 修改浏览器的大小,字体大小
(2)避免layout thrashing——布局抖动
回流的时候可能会出现layout thrashing问题:因为强制不断的回流的发生
- 避免回流
- CSS方面:如用translate实现位移等
- 减少回流:如React的virtual DOM减少回流的发生
- 读写分离