浏览器是如何运作的-页面渲染

777 阅读13分钟

写这个文章有一个重要的原因是在面试的时候,被问到 div是怎么绘制到页面的,只回答上了render tree,然后…………。
更多面试记录,查看我的专栏--面试记录

浏览器发展史

  • 1991年Berners Lee建立了第一代网络浏览器WorldWideWeb,只支持显示文本、图片

  • 1993年Mosaic问世,可以同时显示文本和图片,一经推出就受到全球用户的欢迎

  • 1994年网景浏览器发布,它是曾经参与开发Mosaic的人共同创建,只能显示静态的html,没有js和css。同时也出现的opera

  • 1995年微软发布了IE1.0和IE2.0。第一次浏览器大战打响

  • 1996年发布了IE3.0和windows系统集成了在一起。网景公司市场份额86%。

  • 1998年Mozilla成立基金会

  • 1999年,IE在windows系统的帮助下,发行的四年时间内,市场份额达到99%

  • 2003年苹果发布了safari浏览器,并被包含在所有苹果的操作系统中

  • 2004年Mozilla发布FireFox1.0版本,第二次浏览器大战的序幕

  • 2005年苹果开源了Safari浏览器的内核webkit

  • 2008年谷歌以webkit作为内核创建了一个新的项目Chromiun。在这个基础上谷歌发布了自己的浏览器产品Chrome。目前是最受欢迎的浏览器

  • (由于IE性能和体验问题 ,IE逐渐掉队)2015年,微软放弃IE推出了基于webkit内核的Edge浏览器作为IE的替代品。

简化浏览器结构

  • 用户界面。用于展示除标签页窗口之外的其他用户界面内容
  • 浏览器引擎。 用户在用户界面和渲染引擎之间传递数据。
  • 渲染引擎。负责渲染用户请求的页面内容。渲染引擎下面还有很多小的功能模块。例如:网络模块(负责网络请求)、 js解析器(用于解析和执行js )、数据存储持久层

浏览器内核(渲染引擎)

浏览器内核
IETrident
FireFoxGecko
SafariWebkit
Chrome/Operda/EdgeBlink

进程和线程

进程:总系统进行资源分配和调度的基本单元,可以申请和拥有计算机资源,进程是程序的基本执行实体。
线程:操作系统能狗进行运算调度的最小单位,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存中。应用关闭时,该内存空间就会被回收。进程可以启动更多的进程来执行任务。每个进程分配的内存空间是独立的。如果两个进程之间需要进行通信,则需要通过进程通信管道IPC来传递
很多程序都是多进程的,进程间是独立的,这样可以避免某个进程卡死,这样不会影响到整个应用程序。
进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的任务。同一进程下的线程是可以通信共享数据的。

浏览器进程

早期浏览器是单进程的,浏览器包括了 页面线程、js线程、其他线程。带来的问题有

  1. 不稳定。一个页面卡死会导致整个浏览器崩溃
  2. 不安全。 线程间是可以共享数据的,不同网站可以共享数据
  3. 不流畅。一个进程需要负责的事情过多

现在的浏览器也是多进程的结构,根据进程功能不同来拆卸浏览器,可讲浏览器分为浏览器进程、缓存进程、网络进程、GPU进程、插件进程 image

  • 浏览器进程,负责控制Chrome浏览器除标签也外的用户界面。包括地址栏、书签、后退和前进按钮,以及负责与浏览器其他进程协调工作。
  • 网络进程。负责发起接受网络请求
  • GPU进程。负责整个浏览器界面的渲染
  • 插件进程。负责控制网站使用的所有插件。例如falsh。插件并不是指的Chrome市场里安装的扩展
  • 渲染器进程。程用来控制显示tab标签内的所有内容
  • 缓存进程

image

浏览器在默认情况下会为每个标签页都创建一个进程。

Chrome浏览器进程的模式

  • Process-per-site-instance。每个实例创建一个渲染器进程,确保来自不同站点的页面是独立呈现的
  • Pcocess-per-site。同一站点使用同一进程
  • Process-per-tab。一个tab里所有站点使用一个进程
  • Single Process。让浏览器引擎和渲染引擎共享一个进程

浏览器是如何运行的

以谷歌浏览器为例。

浏览器进程的工作

在地址栏输入地址会发生什么
当我们在浏览器地址栏输入地址时,

  • 浏览器进程的UI线程会捕捉你的输入内容
  • 如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着开始连接服务器获取数据
  • 如果输入不是网址而是一串关键词,浏览器就知道是要搜索,就会使用默认配置的搜索引擎来查询。

网络线程

网络进程获取到数据之后会通过SafeBrowsing来检查站点是否是恶意站点。如果是,则会提示一个警告页面,告诉我们这个站点有安全问题。浏览器会阻止我们的访问,当然我们也可以强行继续访问。 当返回数据准备完毕并且通过安全校验时,网络线程会通知UI线程

SafeBrowsing是谷歌内部的一套站点安全系统,通过该站点的数据来判断是否安全。比如通过查看该站点的IP是否在谷歌的黑名单内。

UI线程

UI线程会创建一个渲染器进程(Render Thread)来渲染页面。浏览器进程通过IPC管道将数据传递给渲染器进程

渲染器进程

正式进入渲染流程,渲染器进程接到的数据也就是html,渲染器进程的核心任务就是把html、css、js、image等资源渲染成用户可以交互的web页面。

渲染器主线程

1. 构建DOM树
渲染器的主线程将html进行解析,构造dom数据结构。
html经过tokeniser标记化,分析输入的html内容,解析为多个标记,根据识别后的标记进行DOM树构造。
在DOM树构造过程中会创建document对象,然后以document为根节点的DOM树不断进行修改,向其中添加各种元素。

html代码中往往会引入一些其他资源比如image、css、js脚本等。图片和CSS这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为他们不会影响DOM的生成。

但html解析过程中遇到script标签,就会停止html解析过程,转而去加载解析并执行js。因为浏览器并不知道JS是否会改变当前页面的HTML结构,例如JS中直接调用document.write,那么之前的html解析就没有任何意义了。这也就是为什么我们一直说要把Script标签放在合适的位置,或者使用async和defer属性来异步加载js。

在html解析完成之后就会获得DOM Tree

DOM:文档对象模型,是浏览器对页面在其内部的表示形式,是web开发程序员可以通过JS与之机哦阿虎的数据结构和API

2. 构建CSSOM
主线程需要解析CSS,并确定每个DOM节点的计算样式,及时我们没有提供自定的CSS样式,浏览器也会提供默认的样式表,比如h2的字体会比h3的大。

3. Layout布局
知道DOM结构和每个dom的样式后,解下来我们需要知道每个节点在页面上的那个位置,也就是节点的坐标以及该元素需要占用多大的区域,这个阶段就是layout 布局
主线程通过遍历dom和计算好的样式来生成layout tree,树的每个节点都记录了x,y坐标和边框尺寸。

需要注意的一点是layout tree和DOM树的节点并不是一一对应的,设置了display: none的节点不会出现在layout tree上。而在before伪类中添加了content值的元素,content的内容会出现在Layout tree上不会出现在DOM树里,这是因为DOm是通过HTML解析获得,并不关心样式。而Layout tree是根据DOM和计算好的样式生成,Layout tree是跟最后展示在屏幕上的节点对应的。

4. 绘制
虽然知道元素的位置和大小,但是我们还需要知道以什么顺序来绘制节点,比如z-index这个属性会影响绘制大的层级关系。如果我们按照dom的层级结构来绘制页面,则会导致错误的渲染。 为了展现正确的层级,主线程遍历layout tree创建一个绘制记录表(Paint Record),该表记录了绘制的顺序,这个阶段就被称为绘制(Paint)

5. 分层
主线程遍历layout tree生成layer tree。

合成器线程

当layer tree生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化

6. 栅格化
现在知道了文档的绘制的顺序,终于到了该吧这些信息转化成像素点显示在屏幕上的时候了,这种行为被称为栅格化(Rastering) 由于一层可能向页面的整个长度一样大,因此合成器线程将他们切分为许多图块儿(tiles)将每个图块发给栅格化线程(Raster Thread)

chrome最早使用了一种最简单的方式,只栅格化用户可是区域Viewport)的内容,当用户滚动页面内容是,再栅格化更多的内容来填充缺失的部分,这种做法的缺点也是显而易见的,会导致内容显示的延迟,随着不断的优化升级。

现在chrome使用了一种更为复杂的栅格化流程叫做合成(Composting),合成将页面各个部分分层多个图层,分别对其进行栅格化,并在合成器线程(Compositor Thread)单独进行合成页面技术。
简单来说就是页面所有的元素按照某种规则进行分图层,并把图层都进栅格化好了,然后只需呀吧可是去的内容组合成一帧展示给用户即可。

栅格化线程

栅格化线程将栅格化每个图块,并将他们存储在GPU内存中。

合成器线程

7.合成
当所有图块都栅格化完成之后,合成器线程将收集成为“draw quads”的图块信息,这些信息记录了图块在内存中的位置和在页面的那个位置绘制图块的信息。根据这些信息合成器线程生成了一个合成器帧(Compositor Frame),这个合成器帧通过IPC传给浏览器进程

浏览器进程

8.渲染
浏览器进程将合成器帧传送给GPU,然后CPU渲染展示到屏幕上。这时候我们终于看到了页面的内容。
当滚动页面,都会生成一个新的合成器帧,新的帧在传给GPU,然后再次渲染到屏幕上。

总结

  • 浏览器进程中的网络线程请求获取到html数据后,通过IPC将数据传递给渲染器进程的主线程
  • 主线程将html解析构造dom树,然后进行样式计算,根据dom树和生成好的样式生成Layout tree
  • 通过遍历Layout tree 生成绘制顺序表,遍历Layout tree生成Layer tree
  • 然后主线程将Layer tree和绘制顺序一起传给合成器线程
  • 合成器线程按照规则进行分图层,并把图层分为更小的图块(tiles)传给栅格线程
  • 栅格线程进行栅格化,栅格化完成后
  • 合成器线程会获得栅格化线程传过来的draw quads图块信息,根据这些信息合成了一个合成器帧
  • 然后将该合成器帧通过IPC传回给浏览器进程
  • 浏览器进程在传给GPU进行绘制,最后就展示到了我们屏幕 image.png

重排 和 重绘

重排:当我们改变元素height等样式,导致重新布局以及后续的所有流程,这种行为称之为重排
重绘:当我们改变元素颜色属性时,不会触发布局,但还是会触发样式计算和绘制

如何优化动画卡顿

重排和重绘都会占用主线程,还有另外一个东西也是运行在主线上,那就是js。
他们都会在主线程运行就会出现抢占执行时间的问题,如果我们写了一个不断重排和重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作。当页面以每秒60帧的刷新时,才不会让用户感到页面卡顿,如果我们在运行动画是有大量的js任务需要执行。
当在一帧的时间内布局和绘制结束后,还有剩余时间,js就会拿到主线成的使用权。如果js执行时间过长,就会导致在下一帧开始是js没有及时归还主线程,就会导致下一帧动画没有按时渲染,就会主线页面动画卡顿,如何优化呢?

  • 利用requestAnimationdFrame,会在每一帧被调用,相当于把js分成更小的任务块(分到每一帧中)在每一帧时间用完之前暂停js执行,归还主线程,这样的话在下一帧开始时主线程就可以按时执行布局和绘制
  • transfrom,在上面渲染流程中,我们知道栅格化是不占用 主线程的,只在合成器线程和栅格化线程中进行。transform实现的动画不会经过布局和绘制,直接运行在合成器线程和栅格线程中,所以不收到js的影响。

image.png

参考资料

【干货】浏览器是如何运作的?(B站)
《Inside look at modern web browser》
《High Performance Animations》
《Process Models》
《How Browsers Work》