浏览器工作原理
浏览器结构
用户界面用于展示除标签页窗口之外的其他用户界面内容
渲染引擎负责渲染用户请求的页面内容。
浏览器引擎,用于在用户界面和渲染引擎之间传递数据。
渲染引擎下面有很多小的功能模块,比如负责网络请求的网络模块,用于解释和执行JS的JS解释器。
还有数据存储持久层,帮助浏览器存储各种数据,如cookie等等。
渲染引擎可以说是一个浏览器的核心,我们往往会把渲染引擎称为浏览器的内核。
不同浏览器使用的内核也不一样。
IE使用的是Trident
Firefox使用的是Gecko
Safari使用的是Webkit 并将其开源。
Chrome使用的是基于Webkit改造优化的Blink渲染引擎,也将其开源。
Opera和Edge使用的Blink。
浏览器进程
每个应用程序必须至少启动一个进程来执行其功能,每个程序往往运行很多任务,进程就会创建一些线程来帮助它去执行这些小任务。
进程是操作系统进行资源分配和调度的基本单元,可以申请和拥有计算机资源,进程是程序的基本执行实体。
线程是操作系统能够运行调度的最小单位,一个进程可以并发多个线程,每条线程并行执行不同的任务。
当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间。该应用程序的状态都保存在该内存空间里。
当应用关闭时,该内存空间就会被回收,进程可以启动更多的进程来执行任务。
由于每个进程分配的内存空间是独立的,如果两个进程间需要传递某些数据,则需要通过进程间的通讯管道IPC来传递。
很多应用程序都是多进程结构,这样是为了避免某一个进程卡死,由于进程间相互独立,不会影响到整个应用程序。
进程可以将任务分成更多更细小的任务。然后通过创建多个线程并执行不同任务,同一进程下的线程之间可以直接通信共享数据的。
早期的浏览器并不是多进程的结构,而是一个单进程结构,这样有很多弊端。
这样的结构并不稳定,其中一个线程卡死可能会导致整个进程出现问题。
比如,浏览器的多个标签中的一个卡死,会导致浏览器无法正常运行。不安全,浏览器之间是可以共享数据的,那JS线程就可以随意访问浏览器进程内的所有数据。
不流畅,一个进程需要负责太多事情,会导致运行效率问题。
为了解决以上问题,现在采用了多进程浏览器结构,根据进程功能的不同来拆卸浏览器。
其中浏览器进程负责控制Chrome浏览器除标签页外的用户界面,包括地址栏、书签、后退和前进按钮,以及负责与浏览器的其他进程协调工作。
网络进程负责发起接受网络请求、GPU负责整个浏览器界面的渲染、插件进程负责控制网站使用的所有插件。
例如:flash(并不是指chrome的扩展)
渲染进程用来控制显示tab标签内的所有内容,浏览器在默认情况下可能会为每个标签页都创建一个进程。
这个和你启动chrome时选择的进程模型有关。
chrome官方文档说明了chrome一共有四种进程模型。
process-per-site-instance,即访问不同站点和同一站点的不同页面都会创建新的进程。(默认)
process-per-site,表示同一站点使用同一进程。
process-per-tab,表示tab里的所有站点使用一个进程。
single process,让浏览器引擎和渲染引擎共用一个进程。
其中,process-per-site-instance是最安全的。
浏览器工作流程
当你在地址栏输入地址时,浏览器进程的UI线程会捕捉你的输入内容,如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着开始连接服务器获取数据。
如果你输入的不是网址而是一串关键词,浏览器就知道你是要搜索,于是就会使用默认配置的搜索引擎来查询。
我们主要来看看网络线程获取到数据之后会发生什么。
当网络线程获取到数据后会通过SafeBrowsing来检查站点,是否是恶意站点。
如果是,就会提示个警告页面,浏览器会阻止你访问,当然你可以强行继续访问。
SafeBrowsing是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全,比如通过查看该站点的IP是否在谷歌的黑名单之内,当返回数据准备完毕并且安全校验通过时,网络线程会通知UI线程准备完毕。
然后UI线程会创建一个渲染器进程(Render Thread)来渲染页面,浏览器进程通过IPC管道将数据传递给渲染器进程,正式进入渲染流程。
渲染器进程接到的数据也就是HTML,渲染器进程的核心任务就是把HTML、css、js、image等资源渲染成用户可以交互的web页面,渲染器进程的主线程将html解析,构造DOM数据结构。
DOM也就是文档对象模型,是浏览器对页面在其内部的表示形式,是web开发程序员可以通过js与之交互的数据结构API
html首先经过tokeniser标记化,通过词法分析将输入的html内容,解析成多个标记,根据识别后的标记进行DOM树构造,在DOM树构造过程中会创建document对象,然后以document的为根节点的DOM树不断进行修改,向其中添加各种元素。HTML代码中往往会引入一些额外的资源,比如图片、css、js脚本等。
图片和css这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为他们不会影响DOM的生成。但当HTML解析过程中遇到script标签,会停止HTML解析流程,转而去加载解析并执行JS。
为什么不直接加载完HTML再去解析JS?
浏览器并不知道JS执行是否会改变当前页面的HTML结构,如果JS代码用了document.write方法来修改html,那之前的html解析就没有意义了。
因此建议把script标签放在合适的位置,或者使用async或defer属性来异步加载执行JS。
1 样式(Style)
HTML解析完成后,我们会获得一个DOM Tree,但我们还不知道它上面是什么样子,主线程需要解析css并确定每个DOM节点的计算样式,即使没有提供自定义的css样式,浏览器会有自己默认的样式表。
2 布局(Layout)
接下来需要知道每个节点需要放在哪个位置,也就是节点的坐标以及该节点需要占用多大的区域。
主线程通过遍历DOM和计算好的样式来生成Layout Tree,上面的每一个节点都记录了x,y坐标和边框尺寸。
这里需要主要的一点是DOM Tree和Layout Tree并不是一一对应的。
设置了display:none的节点不会出现在Layout Tree上,而在before伪类中添加了content值的元素,content里的内容会出现在Layout Tree上,不会出现再DOM树里,这是因为DOM是通过HTML解析获得的,并不关系样式。而Layout Tree是根据DOM Tree和计算好的样式来生成,Layout Tree是和最后展示在屏幕上的节点是对应的。
3 绘制(Paint)
节点存在绘制顺序(paint)。
举个例子:z-index属性会影响节点绘制的层级关系,如果我们按照dom的层级结构来绘制页面则会导致错误的渲染。
为了保证在屏幕上展示正确的层级,主线程遍历Layout Tree创建一个绘制记录表(Paint Record),该表记录了绘制的顺序这个阶段被称为绘制(paint)。
4 栅格化(Rastering)
将信息转化为像素点显示在屏幕上。
Chrome最早使用了一种很简单的方式,指栅格化用户可视区域(Viewport)的内容。
当用户滚动页面时,再栅格化更多的内容来填充缺失的部分,这种行为会导致展示延迟。
随着优化升级Chrome使用更为复杂的栅格化流程,叫做合成(Composting)。
合成是一种将页面的各个部分分成多个图层,分别对其栅格化并在合成器线程(Composting Thread)的技术中单独进行合成页面,简单来说就是页面所有的元素按照某种规则进行分图层并把图层都栅格化好了,然后只把可视区的内容组合成一帧展示给用户。
主线程遍历Layout Tree生成Layer(图层) Tree,当Layer Tree生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化。
由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为许多图块(tiles)。
然后将每个图块发送给栅格化线程(Raster Thread),栅格化线程栅格化每个图块并将它们存储在GPU内存中,当图块栅格画完成后,合成器线程将收集称为”draw quads”的图块信息。
这些信息记录了图块在内存中的位置,和在页面的哪个位置绘制图块的信息。根据这些信息合成器线程生成了一个合成器帧(Compositor Frame),然后合成器Frame 通过IPC传输给浏览器进程,接着浏览器进程将合成器帧传送到GPU,然后GPU渲染展示到屏幕上。
每当你滚动页面时,都会生成一个合成器帧,新的帧传给GPU,然后再渲染到屏幕上。
总结:
浏览器进程中的网络线程请求获取到HTML数据后,通过IPC将数据传给渲染器进程的主线程。
主线程将HTML解析构造DOM Tree,然后进行样式计算,根据DOM树和生成好的样式生成Layout Tree,通过遍历Layout Tree生成绘制循序表(paint)接着遍历了Layout Tree生成Layer Tree,然后主线程将Layer Tree和绘制顺序信息一起传给合成器线程。
合成器线程按规则进行分图层并把图层分为更小的图块(tiles)传给栅格化线程进行栅格化,栅格化完成后,合成器线程会获得栅格线程传过来的“draw quads”图块信息。
根据这些信息合成器线程上合成了一个合成器帧,然后将该合成器帧通过IPC传回给浏览器进程,浏览器进程再传到GPU进行渲染,之后就展示到你的屏幕上。
重排和重绘以及优化手段
当我们改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style) 布局(Layout)绘制(Paint)以及后面的所有流程,这种行为为我们称为重排。
当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这个就是重绘。
我们可以发现重排和重绘都会占用主线程,还有另外一个东西也是运行在主线程上 ———— JavaScript。
他们都是主线程运行,就会出现抢占执行时间的问题,如果你写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作。
当页面以每秒60帧的刷新率时(也是每帧16ms)才不会让用户感觉到页面卡顿。
如果你在运行动画时还有大量的JS任务需要执行,因为布局、绘制、和JS执行都是在主线程运行的。
当在一帧的时间内布局和绘制结束后还有剩余时间,JS就会拿到主线程的使用权,如果JS执行时间过长就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿。
优化手段:
- requestAnimationFrame();
这个方法会在每一帧被调用,通过API的回调,然后我们可以把JS运行分成一些更小的任务块(分到每一帧),在每一帧时间用完钱暂停JS执行,归还主线程这样的话在下一帧开始时主线程就可以按时执行布局和绘制。
React最新的渲染引擎React Fiber就是用到了API来做了很多优化。
- 通过刚才流程我们知道栅格化的整个流程不占用主线程的,只在合成器线程和栅格线程中运行,这就意味着它无需和JS抢夺主线程。
我们刚才提到如果反复进行重绘和重排,可能会导致掉帧,这是因为有可能JS执行阻塞了主线程,而CSS中有个动画属性叫transform,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格线程,所以不会受到主线程中JS执行的影响。
更重要的是通过transform实现的动画由于不需要经过布局、绘制、样式计算等操作,所以节省了很多的运算时间(方便实现负责的动画)。
比如:位置变化、宽高变化(旋转、3D) 都可以使用transform代替。
文案来源:浏览器是如何运作的?
相关参考资料:
《Inside look at modern web browser1-4》
《webkit技术内幕》