# 浏览器渲染
Chrome 浏览器运行原理你了解多少 (opens new window)
# 进程
# 定义
- 狭义定义:进程是正在运行的程序的实例。
- 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。
# 特征
- 结构特征:进程由程序、数据和进程控制块三部分组成。
- 行为特征:
行为特征 | 描述 |
---|---|
动态性 | 进程的实质是程序在 多道程序系统 (opens new window) 中的一次执行过程,进程是动态产生,动态消亡的。 |
独立性 | 进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位; |
异步性 | 由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进 |
并发性 | 任何进程都可以同其他进程一起并发执行 |
# 线程
# 定义
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
# 特征
特征 | 描述 |
---|---|
轻型实体 | 线程中的实体基本上不拥有 系统资源 (opens new window),只是有一点必不可少的、能保证独立运行的资源。 |
独立调度和独立运行的基本单位 | 在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。 |
可并发执行 | 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行 |
共享进程资源 | 在同一进程中的各个线程,都可以共享该进程所拥有的资源 |
# 多线程
# 定义
多线程就是指一个进程中同时有多个线程正在执行
# 为什么要使用多线程
- 在一个程序中,有很多的操作是非常耗时的,如数据库读写操作,IO操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程,可以在将耗时任务放在后台继续执行的同时,同时执行其他操作。
- 可以提高程序的效率。
# 缺点
- 使用太多线程,是很耗系统资源,因为线程需要开辟内存。更多线程需要更多内存。
- 影响系统性能,因为操作系统需要在线程之间来回切换。
- 需要考虑线程操作对程序的影响,如线程挂起,中止等操作对程序的影响。
注意
多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换(因为系统切换的速度非常的快,所以给我们在同时运行的错觉)
# 同一进程中线程的关系
- 在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。
- 同一个进程内的线程之间互相通信不必调用内核,因为同一个进程内的线程共享文件和内存,
- 同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)
# 浏览器多进程
通用浏览器架构它可以是一个单进程多线程架构,也可以是具有几个通过 IPC 进行通信的多个线程的进程,即多进程架构。chrome 采用多进程架构。
多进程模型优缺点:
优点:
- 稳定性,一个页面崩溃不会影响到其他页面,因为使用了不同渲染进程
- 安全性和沙箱处理,浏览器可以从某些功能中沙漏某些进程。例如,Chrome浏览器限制了处理诸如渲染器进程之类的任意用户输入的进程的任意文件访问。
缺点:
- 更高的资源占用。因为每个进程都会包含公共基础结构的副本(例如V8,这是Chrome的JavaScript引擎),这就意味着浏览器会消耗更多的内存资源。
- 更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题
# 浏览器的多线程
浏览器是多进程,最重要的是渲染进程。而渲染进程下有多个线程,这就是我们常说的浏览器内核。
下面具体分析浏览器进程以及浏览器渲染原理:
# 浏览器进程
通常情况浏览器包括以下几个进程:
浏览器进程(主进程)
主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
渲染进程
核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中。默认情况下,Chrome 浏览器会为每个 tab 标签创建一个渲染进程,但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。
GPU进程
GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
网络进程
主要负责页面的网络资源加载。
插件进程
主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
# 浏览器渲染进程
渲染进程是多线程,主要包含以下 6 个线程:
GUI 渲染线程
负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意
GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
为什么说 js 是单线程,而不是多线程
JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程。如果 JS 是多线程的方式来操作这些 UI DOM,则可能出现UI操作的冲突;如果在多线程的交互下,处于 UI中的 DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果,当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JS 在最初就选择了单线程执行。
js引擎线程
JS 引擎线程(例如V8引擎)负责解析 Javascript 脚本,运行代码。JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。
注意
GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程
归属于浏览器而不是 JS 引擎,用来控制事件循环。当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。
注意
由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
定时触发线程
传说中的setInterval与setTimeout所在线程。浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确),因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)。
注意
W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。(也就是0ms也算4ms)
异步http请求线程
在XMLHttpRequest连接后是通过浏览器新开一个线程请求。将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
合成线程
在GUI渲染线程后执行,将GUI渲染线程生成的待绘制列表转换为位图。
I/O线程
用来和其他进程进行通信
# 浏览器渲染过程
- Html解析:DOM树。解码,预解析,符号化,构建树,浏览器容错,事件
- Css解析:CSS规则树按从右向左。
- Js编译执行
- Render渲染树:DOM树与CSSOM规则树合并过程
- 布局与绘制
- 合并渲染层
- 回流与重绘
渲染进程将HTML解析成能读懂的DOM树,渲染引擎将CSS样式表转化为浏览器可理解的styleSheets,计算出DOM节点的样式,即解析成CSSOM CSS规则树。创建布局树,计算元素布局信息。对布局树分层,生成分层树。为每个图层生成绘制列表,并将其提交给合成线程,合成线程将图层分块,并栅格化图块转换为位图。合成线程发送绘制图块命令给浏览器进程,浏览器进程根据指令生成页面,显示。以上过程也称关键渲染路径
# 解析HTML生成DOM树
DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。DOM 树的根节点就是 document 对象。
DOM 树的生成过程中可能会被 CSS 和 JS 的加载执行阻塞。
当 HTML 文档解析过程完毕后,浏览器继续进行标记为 deferred 模式的脚本加载,然后就是整个解析过程的实际结束触发 DOMContentLoaded 事件,并在 async 文档执行完之后触发 load 事件。
HTML 解析
- 解码(encoding) 将http传输来的二进制字节数据解码成浏览器指定的编码格式转化成字符串,也就是html代码。
- 预解析(pre-parsing) 提前加载资源,减少处理时间,识别一些类似img中src的请求资源属性,并将其加入到请求队列中
- 符号化(tokenization) 符号化是词法分析过程,将HTML解析成符号
- 构建树(tree) 符号化和构建树是并行操作的,只要解析到一个开始标签就创建一个DOM节点
- 浏览器容错机制
- 事件 解析中浏览器会通过DOMContentLoaded事件通知DOM解析完成
# 样式计算
解析 HTML 生成 DOM 树的同时会生成样式结构体 CSSOM(CSS Object Model)Tree(或者叫 CSS 规则树),再根据 CSSOM 和 DOM 树构造渲染树 Render Tree(呈现树)。
Render Tree 的构建其实就是 DOM Tree 和 CSSOM Attach 的过程。
# (1) 将CSS 文本转换为浏览器可以理解的结构-styleSheets(即CSSOM树)
由于浏览器也无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行转换操作。styleSheets同时具备了查询和修改功能,这会为后面的样式操作提供基础。
# (2) 转换样式表中的属性值,使其标准化
如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,则会转换。将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
# (3) 计算出 DOM 树中每个节点的具体样式(即结合DOM树和CSSOM树形成渲染树)
样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的 继承 和 层叠 两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。
CSS 继承与层叠
- CSS 继承就是每个 DOM 节点都包含有父节点的样式
- CSS 层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
display 等于 none 的也不会被显示在这棵树里头,但是 visibility 等于 hidden 的元素是会显示在这棵树里头的。
那些添加了 float 或者 position:absolute 的元素,因为它们脱离了正常的文档流,构造 Render 树的时候会针对它们实际的位置进行构造。
CSS 解析
HTML可以逐步解析,它不需要等待所有DOM都构建完毕后再去构建CSSOM,而是在解析HTML构建DOM时,若遇见CSS会立刻构建CSSOM,它们可以同时进行。 然而CSS需要完整的构建,不完整的CSS是无法使用的,因为CSS的每个属性都可以改变CSSOM,所以会存在这样一个问题:假设前面几个字节的CSS将字体大小设置为16px,后面又将字体大小设置为14px,那么如果不把整个CSSOM构建完整,最终得到的CSSOM其实是不准确的。
渲染阻塞资源(CSS)
因而必须等CSSOM构建完毕才能进入到下一个阶段,哪怕DOM已经构建完,它也得等CSSOM,然后才能进入下一个阶段。所以CSS也被认为是“渲染阻塞资源”
解析器阻塞资源(JS)
浏览器解析文档,当遇到<script>
标签的时候,会立即交给 JS 引擎解析脚本,停止解析DOM与CSSOM(因为JS可能会改动DOM和CSS,所以继续解析会造成浪费)。如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。现在可以在script标签上增加属性 defer或者async。脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM Tree和Style Rules上。
JS对关键路径渲染的影响不只是阻塞DOM构建,也会导致CSSOM阻塞DOM构建。如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。
# 布局 Layout
上面确定了 renderer 的样式规则后,然后就是重要的显示元素布局了。当 renderer 构造出来并添加到 Render 树上之后,它并没有位置跟大小信息,所以接下来交给布局(layout)。
布局阶段输出的结果称为box盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位。
确定渲染树所有的几何属性,如位置,大小等,最后输出一个盒子模型,盒子模型可精准的捕获到每个元素在屏幕上的准确位置与大小,然后遍历渲染树,调用渲染器的paint()方法显示。
# (1) 创建布局树
创建一棵只包含可见元素布局树。浏览器会做如下操作: 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;而不可见的节点会被布局树忽略掉。
# (2) 布局计算
计算布局树节点的坐标位置。在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。
# 分层
为了更方便的实现页面中的复杂的效果,如3D 变换、页面滚动等,渲染引擎会为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。即将多个图层叠加在一起构成最终的页面图像。(打开 Chrome 的“开发者工具”,选择“Layers”标签即可看到页面的分层)。
注意
- 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
- 拥有层叠上下文属性(定位属性、透明属性、CSS 滤镜、z-index 等)的元素会被提升为单独的一层。
- 需要剪裁(clip)的地方也会被创建为图层
# 图层绘制 painting
构建完图层树之后,渲染引擎会对图层树中的每个图层进行绘制。 渲染引擎实会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。(点击开发者工具”的“Layers”标签,选择“document”层,显示的则是绘制列表)
重排(回流,reflow)与重绘(repaint)
过程中还有 重排(回流,reflow)与重绘(repaint),具体见文章最后部分
# 栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
渲染主线程与合成线程的关系
- 合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。
视口
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口
划分图块的原因
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
- 合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的(图块是栅格化执行的最小单位)
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
注意
GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。即渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中
# 合成与显示
一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
# 浏览器渲染完整流程 总 结
渲染阻塞
当遇到一个<script>
DOM构建会暂停,直到脚本执行完毕,继续构建DOM树,如果JS依赖CSS样式,而它还没有被下载和构建,浏览器就会延迟脚本的执行,直到脚本所依赖的CSS 规则树被构建,所以:CSS会阻塞JS的执行,但不会阻塞JS的解析,JS会阻塞后面DOM的解析。为此应避免:CSS资源需排在JS资源前面,JS应该放在HTML底部</body>
前,另外可使用defer,async改变阻塞方式
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树 结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets ,计算出 DOM 节点的样式。
- 创建 布局树 ,并计算元素的布局信息。
- 对布局树进行分层,并生成 分层树 。
- 为每个图层生成 绘制列表 ,并将其提交到合成线程。
- 合成线程将图层分成 图块 ,并在 光栅化 (栅格化) 线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息 生成页面 ,并 显示 到显示器上。
# 重排(回流,reflow)重绘(repaint)与合成 补 充
# 重排
定义
当通过JS或者 CSS 修改元素的几何属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
图示
触发条件
- 添加或者删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变
- 元素内容改变(例: 一个文本被另一个不同尺寸的图片替代)
- 页面渲染初始化(无法避免)
- 浏览器窗口尺寸改变
优化方案
- 尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新)。
- 合并多次DOM操作。比如用class来改变多个样式。
- 避免使用table。
- 使用fragment元素(createDocumentFragment)
- 让元素脱离文档流。即让当前元素有自己的图层。
- 多次修改时把dom 离线 ,修改完再显示。(display:none)
- 使用采用虚拟DOM的库,如Vue,React
- will-change: transform 启用硬件加速
# 重绘
定义
当通过JS或者 CSS 修改元素的绘制属性,例如改变元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段(即生成待绘制列表),然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
图示
触发条件
- background属性(background, background-color, background-image, background-position,background-repeat, background-size)
- outline属性(outline, outline-color, outline-style)
- box-shadow属性
- border属性(border-style, border-radius)
- visibility
优化方案
- 合并多次操作
# 合成
定义
更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。比如我们使用了 CSS 的 transform、opacity、filter这些属性来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率(常说的GPU加速)。
图示
触发条件
- will-change
- transform属性改变
- 整个图层的几何变换,透明度变换,阴影。
优化方案
- 使用will-change提前声明,使得渲染引擎将该元素单独实现一帧。(空间换时间)。
注意
每当渲染引擎为一个元素准备一个独立层的时候,它占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,这些都需要额外的内存,所以你需要恰当地使用 will-change。
GPU加速的原因
在合成的情况下,会直接跳过布局和绘制流程,直接进入非主线程处理的部分,即直接交给合成线程处理。交给它处理有两大好处:
- 能够充分发挥GPU的优势。合成线程生成位图的过程中会调用线程池,并在其中使用GPU进行加速生成,而GPU 是擅长处理位图数据的。
- 没有占用主线程的资源,即使主线程卡住了,效果依然能够流畅地展示。
# 关键渲染路径与阻塞渲染 补 充
关键渲染路径:就是浏览器拿到资源,解析HTML,CSS,JS字节数据并对其进行解析和转变成像素的渲染过程的过程。通过优化关键渲染路径即可以缩短浏览器渲染页面的时间。
CSS 被视为渲染阻塞资源(包括JS),这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕,才会进行下一阶段。
JavaScript 被认为是解释器阻塞资源,HTML解析会被JS阻塞,它不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。
存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。另外:
- 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
- JavaScript 可以查询和修改 DOM 与 CSSOM。
- CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。
# CSS的阻塞
关于CSS加载的阻塞情况:
- css加载不会阻塞DOM树的解析
- css加载会阻塞DOM树的渲染
- css加载会阻塞后面js语句的执行
没有js的理想情况下,html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)
# JS的阻塞
如果没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的HTML元素之前,也就是说不等待后续载入的HTML元素,读到就加载并执行。
解析过程中无论遇到的JavaScript是内联还是外链,只要浏览器遇到 script 标记,唤醒JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕,才去执行js脚本。因为脚本中可能会操作DOM元素,而如果在加载执行脚本的时候DOM元素并没有被解析,脚本就会因为DOM元素没有生成取不到响应元素,所以实际工程中,我们常常将资源放到文档底部。
# defer 与 async
defer 与 async 可以改变JS的阻塞。这两个属性都会使 script 异步加载,但执行的时机是不一样的。
注意
async 与 defer 属性对于 inline-script 都是无效的
defer
延迟执行引入的 js。即 js 加载时 HTML 未停止解析,两个过程是并行的。 整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,再触发 DOMContentLoaded(初始的 HTML 文档被完全加载和解析完成之后触发,无需等待样式表图像和子框架的完成加载) 事件。
- 载入 JavaScript 文件时不阻塞 HTML 的解析
- 执行阶段被放到 HTML 标签解析完成之后
async
异步执行引入的 js。如果已经加载好,就会开始执行,无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发(HTML解析完成事件)之后。
- 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
- 多个 async-script 的执行顺序是不确定的,谁先加载完谁执行。值得注意的是,向 document 动态添加 script 标签时,async 属性默认是 true。
- 使用 document.createElement("script") 创建的 script 默认是异步的。所以,通过动态添加 script 标签引入 JavaScript 文件默认是不会阻塞页面的。
图解 script 标签中的 async 和 defer 属性 (opens new window)
# 优化渲染性能
- 优化JS的执行效率
- 降低样式计算的范围和复杂度
- 避免大规模、复杂的布局
- 简化绘制的复杂度、减少绘制区域
- 优先使用渲染层合并属性、控制层数量
- 对用户输入事件的处理函数去抖动(移动设备)
- 避免频繁使用 style,而是采用修改class的方式。
- 使用createDocumentFragment进行批量的 DOM 操作。
- 对于 resize、scroll 等进行防抖/节流处理。
- 添加 will-change: tranform ,让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。当然这个变化不限于tranform, 任何可以实现合成效果的 CSS 属性都能用will-change来声明。
# JS 解析与运行 补 充
# 词法分析
JS脚本加载完会进入语法分析阶段,首先会分析代码块语法是否正确,不正确抛出语法错误。
- 分词:分成词法单元
- 解析:将词法单元转成抽象语法树AST
- 代码生成:将抽象语法树转机器指令
# 预编译
JS三种运行环境:全局环境,函数环境,evel。进入不同的环境创建对应的执行上下文,根据不同的上下文环境,形成一个函数调用栈,栈底永远式全局上下文。栈顶永远式当前执行上下文。
# 创建执行上下文
- 创建变量对象(参数,函数,变量)
- 建立作用域(确定指定当前执行环境是否能访问变量)
- 确定this指向
# JS 执行
JS单线程,但实际上参与工作的线程共4个:一个主线程,其余3个辅助。
JS 引擎线程
JS内核,负责解析JS脚本程序的主线程,例如V8
事件触发线程
属于浏览器内核线程,主要控制事件调度,当事件触发时,将事件的处理回调推进事件队列,等待JS引擎线程执行。
定时器触发线程
控制setInterval和setTimeout,计时完毕把回调函数推进事件队列,等JS引擎执行。
HTTP异步请求线程
通过XMLHttpRequest连接后,通过浏览器新开一个线程,监控readyState状态改变,状态变更时,将回调函数推进事件队列,等JS引擎执行。
# 参考资料
浏览器原理系列-浏览器渲染流程详解 (opens new window)