共计 6926 个字符,预计需要花费 18 分钟才能阅读完成。
在我刚开始学习 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 树上,再进行后续操作,防止大量无谓的计算量。