在我刚开始学习Web开发的时候,始终有个疑难——我写出的代码到底是在什么时候产生作用的呢?是不是每次我批改代码网页都随之变动了?当然,当初来看这必定是一个谬误的想法,通过一段时间的工作和学习后,代码到页面转换的门路在我的脑海里愈发清晰,尽管“输出URL到网页显示之间产生了什么?”是个陈词滥调的问题,但我还是想按本人的了解来阐明一遍。
浏览器架构
首先从咱们最相熟的敌人开始说起,Web开发离不开浏览器,我在查资料的时候有开很多选项卡的习惯,每次关上工作管理器都能看到Chrome浏览器在内存占用方面一枝独秀,另外还能看到利用名称前面的括号里有个数字,如下图所示,但我关上的标签页是不到23的,那么剩下的过程是什么呢?
咱们来看一张经典的图,它描述了Chrome浏览器中四种过程的地位和作用:
- 浏览器过程 (Browser Process):负责浏览器的TAB的后退、后退、地址栏、书签栏的工作和解决浏览器的一些不可见的底层操作,比方网络申请和文件拜访。
- 渲染过程 (Renderer Process):负责一个Tab内的显示相干的工作,也称渲染引擎。
- 插件过程 (Plugin Process):负责管制网页应用到的插件
- GPU过程 (GPU Process):负责解决整个应用程序的GPU工作
渲染过程较为非凡,每个选项卡里都须要一个渲染过程,它也是网页渲染的外围,咱们在下一节具体阐明,对于这些过程,能够在浏览器自带的过程管理器中具体查看:
因为常常要做多浏览器兼容,常常同时关上几个浏览器,即便没有认真比照还是能够发现Chrome浏览器在内存占用方面算是绝对比拟高的,而Firefox则绝对要低很多,这是因为Firefox的Tab过程和IE的Tab过程都采纳了相似的策略:有多个Tab过程,但都不肯定是一个页面一个Tab过程,一个Tab过程可能会负责多个页面的渲染。而作为比照,Chrome是以一个页面一个渲染过程,加上站点隔离的策略来进行的。尽管内存占用的确比拟高,然而这种多过程架构也有独特的劣势:
- 更高的容错性。当今WEB利用中,HTML,JavaScript和CSS日益简单,这些跑在渲染引擎的代码,频繁的呈现BUG,而有些BUG会间接导致渲染引擎解体,多过程架构使得每一个渲染引擎运行在各自的过程中,相互之间不受影响,也就是说,当其中一个页面解体挂掉之后,其余页面还能够失常的运行不收影响。
- 更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是歹意的代码,它们会利用这些破绽在你的电脑上装置歹意的软件,针对这一问题,浏览器对不同过程限度了不同的权限,并为其提供沙盒运行环境,使其更平安更牢靠
- 更高的响应速度。在单过程的架构中,各个工作相互竞争争夺CPU资源,使得浏览器响应速度变慢,而多过程架构正好躲避了这一毛病。
网页渲染
大抵来说,输出URL后要通过五个步骤网页才会渲染实现:
- DNS 查问
- TCP 连贯
- HTTP 申请即响应
- 服务器响应
- 客户端渲染
首先,如果输出的是域名,浏览器会先从hosts文件中查找有没有对应的设置,如果没有则拜访左近的DNS服务器进行DNS查问获取正确的IP地址,之后进行TCP连贯,通过三次握手建设连贯后开始解决HTTP申请,服务器端收到申请返回响应文档,拿到响应文档的浏览器开始应用渲染引擎进行页面渲染。
这里说到的渲染引擎就是咱们常常说到的浏览器内容,例如Webkit、Gecko这些。
渲染引擎
浏览器内核是多线程,在内核管制下各线程相互配合以放弃同步,一个浏览器通常由以下常驻线程组成:
- GUI 渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面须要重绘(Repaint)或因为某种操作引发回流(reflow)时,该线程就会执行
- GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被解冻了),GUI更新会被保留在一个队列中等到JS引擎闲暇时立刻被执行。
- JavaScript引擎线程
- 也称为JS内核,负责解决Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎始终期待着工作队列中工作的到来,而后加以解决,一个Tab页(renderer过程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样留神,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的工夫过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 定时触发器线程
- 传说中的setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的精确)
- 因而通过独自线程来计时并触发定时(计时结束后,增加到事件队列中,期待JS引擎闲暇后执行)
- 事件触发线程
- 归属于浏览器而不是JS引擎,用来管制事件循环(能够了解,JS引擎本人都忙不过来,须要浏览器另开线程帮助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其余线程,如鼠标点击、AJAX异步申请等),会将对应工作增加到事件线程中
- 当对应的事件合乎触发条件被触发时,该线程会把事件增加到待处理队列的队尾,期待JS引擎的解决
- 留神,因为JS的单线程关系,所以这些待处理队列中的事件都得排队期待JS引擎解决(当JS引擎闲暇时才会去执行)
- 异步http申请线程
- 在XMLHttpRequest在连贯后是通过浏览器新开一个线程申请
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
这五个线程各司其职,但咱们这里还是将眼光放到GUI渲染上:
渲染流程
- 解决 HTML 标记并构建 DOM 树。
- 解决 CSS 标记并构建 CSSOM 树
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 依据渲染树来布局,以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
1. DOMTree的构建(Document Object Model)
第一步(解析):从网络或者磁盘下读取的HTML原始字节码,通过设置的charset编码,转换成字符
第二步(token化):通过词法分析器,将字符串解析成Token,Token中会标注出以后的Token是开始标签,还是完结标签,或者文本标签等。
第三步(生成Nodes并构建DOM树):浏览器会依据Tokens里记录的开始标签,完结标签,将Tokens之间互相串联起来(带有完结标签的Token不会生成Node)。
2. CSSOMTree的构建(CSS Object Model)
当HTML代码遇见<link>标签时,浏览器会发送申请取得该标签中标记的CSS文件(应用内联CSS能够省略申请的步骤进步速度,但没有必要为了这点速度而失落了模块化与可维护性),style.css中的内容见下图:
浏览器取得内部CSS文件的数据后,就会像构建DOM树一样开始构建CSSOM树,这个过程没有什么特地的差异。
从图中能够看出,最开始body有一个款式规定是font-size:16px,之后,在body这个款式根底上每个子节点还会增加本人独自的款式规定,比方span又增加了一个款式规定color:red。正是因为款式这种相似于继承的个性,浏览器设定了一条规定:CSSOMTree须要等到齐全构建后才能够被应用,因为前面的属性可能会笼罩掉后面的设置。比方在下面的css代码根底上再增加一行代码p {font-size:12px},那么之前设置的16px将会被笼罩成12px。
看到这里,感觉如同少了什么?咱们的页面不会只蕴含HTML和CSS,JavaScript通常也在页面中占有很大的比重,并且JavaScript也是引发性能问题的重要因素,这里通过解答上面几个问题来阐明JavaScript在页面渲染中的状况。
问题:渲染过程中遇到JS文件怎么解决?
因为JavaScript是可操纵DOM的,如果在批改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后取得的元素数据就可能不统一了。
因而为了避免渲染呈现不可预期的后果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起, GUI更新则会被保留在一个队列中等到JS引擎
线程闲暇时立刻被执行。
也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行结束,浏览器再从中断的中央复原DOM构建。
问题:为什么有时在js中拜访DOM时浏览器会报错?
因为在解析的过程中,如果碰到了script或者link标签,就会依据src对应的地址去加载资源,在script标签没有设置async/defer属性时,这个加载过程是下载并执行齐全部的代码,此时,DOM树还没有齐全创立结束,这个时候如果js希图拜访script标签前面的DOM元素,浏览器就会抛出找不到该DOM元素的谬误。
问题:平时谈及页面性能优化,常常会强调css文件应该放在html文档中的后面引入,js文件应该放在前面引入,这么做的起因是什么呢?
原本,DOM构建和CSSOM构建是两个过程,井水不犯河水。假如DOM构建实现须要1s,CSSOM构建也须要1s,在DOM构建了0.2s时发现了一个link标签,此时实现这个操作须要的工夫大略是1.2s,如下图所示:
但JS也能够批改CSS款式,影响CSSOMTree最终的后果,而咱们后面提到,不残缺的CSSOMTree是不能够被应用的。
问题:如果JS试图在浏览器还未实现CSSOMTree的下载和构建时去操作CSS款式,会产生什么?
咱们在HTML文档的两头插中入了一段JS代码,在DOM构建两头的过程中发现了这个script标签,假如这段JS代码只须要执行0.0001s,那么实现这个操作须要的工夫就会变成:
那如果咱们把css放到后面,js放到最初引入时,构建工夫会变成:
由此可见,尽管只是插入了小小的一段只运行0.0001s的js代码,不同的引入机会也会重大影响DOMTree的构建速度。
简而言之,如果在DOM,CSSOM和JavaScript执行之间引入大量的依赖关系,可能会导致浏览器在解决渲染资源时呈现大幅度提早:
- 当浏览器遇到一个script标签时,DOMTree的构建将被暂停,直至脚本执行结束
- JavaScript能够查问和批改DOMTree与CSSOMTree
- 直至CSSOM构建结束,JavaScript才会执行
- 脚本在文档中的地位很重要
3. 渲染树的构建
当咱们生成 DOM 树和 CSSOM 树当前,就须要将这两棵树组合为渲染树。
- Render 树上的每一个节点被称为:RenderObject。
- RenderObject跟 DOM 节点简直是一一对应的,当一个可见的 DOM 节点被增加到 DOM 树上时,内核就会为它生成对应的 RenderOject 增加到 Render 树上。
其中,可见的DOM节点不包含:
- 一些不会体现在渲染输入中的节点(
<html><script><link>
….),会间接被疏忽掉。 - 通过CSS暗藏的节点。例如上图中的span节点,因为有一个CSS显式规定在该节点上设置了display:none属性,那么它在生成RenderObject时会被间接疏忽掉。
- 一些不会体现在渲染输入中的节点(
- Render 树是连接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输入,渲染引擎的输出。
4. 布局
到目前为止,浏览器计算出了哪些节点是可见的以及它的信息和款式,接下来就须要计算这些节点在设施视口内的确切地位和大小,这个过程咱们称之为“布局”。
布局最初的输入是一个“盒模型”:将所有绝对测量值都转换成屏幕上的相对像素。
5. 渲染
当Layout布局事件实现后,浏览器会立刻收回Paint Setup与Paint事件,开始将渲染树绘制成像素,绘制所需的工夫跟CSS款式的复杂度成正比,绘制实现后,用户就能够看到页面的最终出现成果了。
总结
咱们对一个网页发送申请并取得渲染后的页面可能也就通过了1~2秒,但浏览器其实曾经做了上述所讲的十分多的工作,总结一下浏览器要害渲染门路的整个过程:
- 解决HTML标记数据并生成DOM树。
- 解决CSS标记数据并生成CSSOM树。
- 将DOM树与CSSOM树合并在一起生成渲染树。
- 遍历渲染树开始布局,计算每个节点的地位信息。
- 将每个节点绘制到屏幕。
相干问题
defer 和 async
下面咱们还提到一个小知识点:在script标签没有设置async/defer属性时,这个加载过程是下载并执行齐全部的代码。如果有设置这两个属性会有什么不同呢?
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
- 状况1
<script src="script.js"></script>
没有 defer 或 async,浏览器会立刻加载并执行指定的脚本,也就是说不期待后续载入的文档元素,读到就加载并执行。
- 状况2
<script async src="script.js"></script>
(异步下载)
async 属性示意异步执行引入的 JavaScript,与 defer 的区别在于,如果曾经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。须要留神的是,这种形式加载的 JavaScript 仍然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但肯定在 load 触发之前执行。
- 状况3
<script defer src="script.js"></script>
(提早执行)
defer 属性示意提早执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未进行解析,这两个过程是并行的。整个 document 解析结束且 defer-script 也加载实现之后(这两件事件的程序无关),会执行所有由 defer-script 加载的 JavaScript 代码,而后触发 DOMContentLoaded 事件。
defer 与相比一般 script,有两点区别:
- 载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析实现之后。
- 在加载多个JS脚本的时候,async是无程序的加载,而defer是有程序的加载。
回流(reflow)和重绘(repaint)
咱们晓得,当网页生成的时候,至多会渲染一次。在用户拜访的过程中,还会一直从新渲染。从新渲染会反复上图中的第四步(回流)+第五步(重绘)或者只有第五个步(重绘)。
- 重绘:当render tree中的一些元素须要更新属性,而这些属性只是影响元素的外观、格调,而不会影响布局的,比方background-color。
- 回流:当render tree中的一部分(或全副)因为元素的规模尺寸、布局、暗藏等扭转而须要从新构建
回流必定会产生重绘,重绘不肯定会引发回流。重绘和回流会在咱们设置节点款式时频繁呈现,同时也会很大水平上影响性能。回流所需的老本比重绘高的多,扭转父节点里的子节点很可能会导致父节点的一系列回流。
常见引起回流属性和办法
任何会扭转元素几何信息(元素的地位和尺寸大小)的操作,都会触发回流,增加或者删除可见的DOM元素:
- 元素尺寸扭转——边距、填充、边框、宽度和高度
- 内容变动,比方用户在input框中输出文字
- 浏览器窗口尺寸扭转——resize事件产生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
如何缩小回流、重绘
- 应用 transform 代替 top
- 应用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(扭转了布局)
- 不要应用 table 布局,可能很小的一个小改变会造成整个 table 的从新布局
- 动画实现的速度的抉择,动画速度越快,回流次数越多,也能够抉择应用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,防止节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层可能阻止该节点的渲染行为影响别的节点。比方对于 video 标签来说,浏览器会主动将该节点变为图层。
为什么操作 DOM 慢
因为 DOM 是属于渲染引擎中的货色,而 JS 又是 JS 引擎中的货色。当咱们通过 JS 操作 DOM 的时候,其实这个操作波及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于始终在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的状况,所以也就导致了性能上的问题。
这也就是为什么咱们在应用Vue.js框架时会感觉晦涩水平显著高于传统的页面,因为Vue.js应用的是虚构DOM,若一次操作中有10次更新DOM的动作,虚构DOM不会立刻操作DOM,而是将这10次更新的diff内容保留到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,防止大量无谓的计算量。