本文来自于我的github
0. 前言
身为前端,打交道最多的就是浏览器和node了,也是我们必须熟悉的。接下来我们讲一下浏览器工作原理和工作过程。从url到页面的过程,......,我们直接来到收到服务器返回内容部分开始。
先上很多人都见过的一幅图:
还有一幅图:
浏览器主要组成部分:
- 浏览器引擎:在用户界面和呈现引擎之间传送指令。
- 渲染引擎:负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络:用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
- JavaScript 解释器:用于解析和执行 JavaScript 代码。
- 数据存储:浏览器需要在硬盘上保存各种数据,例如 Cookie、storage、indexdb。
过程:(重要)
- 解析过程
- CSS样式计算
- 构建Render Tree
- layout:布局。定位坐标和大小,是否换行,position, overflow之类的属性。确定了每个DOM元素的样式规则后,计算每个DOM元素最终在屏幕上显示的大小和位置。Web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如
body
元素的width变化会影响其后代元素的宽度。因此,布局过程是经常发生的。 - paint:绘制文字、颜色、图像、边框和阴影等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
- composite:渲染层合并。页面中DOM元素的绘制是在多个层上进行的,在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后在屏幕上呈现。
1. 解析过程
- 获取请求文档的内容后,呈现引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点。
- 解析外部 CSS以及style元素中的样式数据形成呈现树。呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。呈现树构建完毕之后,进入“布局”处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。
- 解析script标签时,解析完毕马上执行,并且阻塞页面。
- 绘制 - 呈现引擎会遍历呈现树,由用户界面后端层将每个节点绘制出来。
1.1 词法、语法分析与编译
词法分析器将输入内容分解成一个个有效标记,解析器负责根据语言的语法规则分析文档的结构来构建解析树。词法分析器知道如何将无关的字符(空格、换行符等)分离出来,所以我们平时写一些空格也不会影响大局。
在语法分析的过程中,解析器会向词法分析器请求一个标记(就是前面分解出来的标记),并尝试将其与某条语法规则(比如标签要闭合、正确嵌套)进行匹配。如果发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,然后继续请求下一个标记。
如果没有规则可以匹配,解析器就会将标记存储到内部,并继续请求标记,直至找到可与所有内部存储的标记匹配的规则(如div多层嵌套的情况,这样子能找到div闭合部分)。如果找不到任何匹配规则,解析器就会引发一个异常。这意味着文档无效,包含语法错误。
解析器类型有两种:
- 自上而下解析器:从语法的高层结构出发,尝试从中找到匹配的结构。
- 自下而上解析器:从低层规则出发,将输入内容逐步转化为语法规则,直至满足高层规则。将扫描输入内容,找到匹配的规则后,将匹配的输入内容替换成规则。如此继续替换,直到输入内容的结尾。部分匹配的表达式保存在解析器的堆栈中。
编译:将源代码编译成机器代码,源代码先走完解析的过程形成成解析树,解析树被翻译成机器代码文档,完成编译的过程
1.2 DTD
特殊的是,恰好html不能用上面两种解析方法。有一种可以定义 HTML 的正规格式:DTD,但它不是与上下文无关的语法,html明显是和上下文关系紧密的。我们知道 HTML 是有点“随意”的,对于不闭合的或者不正确嵌套标签有可能不报错,并且尝试解释成正确的样子,具有一定的容错机性,因此可以达到简化网络开发的效果。另一方面,这使得它很难编写正式的语法。概括地说,HTML 无法很容易地通过常规解析器解析(因为它的语法不是与上下文无关的语法),所以采用了 DTD 格式。
1.3 解析为dom过程
解析器解析html文档的解析树是由 DOM 元素和属性节点构成的树结构。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的api,其根节点是document。上面已经说到,不能使用常规的解析技术解释html,浏览器就创建了自定义的解析器来解析 。对于HTML/SVG/XHTML这三种文档,Webkit有三个C++的类对应这三种文档,并产生一个DOM Tree。解释html成dom的过程,由两个阶段组成:标记化和树构建。
1.3.1 标记化算法
对于一段html:
<html>
<body>
hi
</body>
</html>
该算法使用状态机来表示。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。
初始状态是数据状态。遇到字符 < 时,状态更改为“标记打开状态”。接收一个字母会创建“起始标记”,状态更改为“标记名称状态”。这个状态会一直保持到接收 > 字符,接收到将会进入“标记打开状态”。在此期间接收的每个字符都会附加到新的标记名称上。
-
比如我们先写html标签,先遇到<,进入“标记打开状态”,遇到html四个字母进入“标记名称状态”,接着接收到了>字符,会发送当前的标记,状态改回“数据状态”
-
<body>
标记也会进行同样的处理。现在 html 和 body 标记均已发出,而且目前是“数据状态”。接收到 hi中的 h 字符时,将创建并发送字符标记,直到接收</body>
中的 <。我们将为hi的每个字符都发送一个字符标记。 -
回到“标记打开状态”。接收下一个输入字符 / 时,会创建闭合标签token,并改为“标记名称状态”。我们会再次保持这个状态,直到接收 >。然后将发送新的标记,并回到“数据状态”。最后,
</html>
输入也会进行同样的处理。
1.3.2 树构建过程
在创建解析器的同时也会创建 document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。
-
树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下重新处理此标记。这样会创建一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。
-
状态改为“before head”。此时我们接收“body”标记。由于容错性,就算我们的没head标签,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。
-
进入了“in head”模式,然后转入“after head”模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为“in body”。
-
接收由“hi”字符串生成的一系列字符标记。接收第一个字符时会创建并插入文本节点,而其他字符也将附加到该节点。当然还有其他节点,比如属性节点、换行节点。我们实际场景还有外部资源以及其他各种各样的复杂标签嵌套和内容结构,不过原理都类似。对于中间这个过程,遇到外部资源如何处理,顺序是怎样的,后面再讲。
-
接收 body 结束标记会触发“after body”模式。现在我们将接收 HTML 结束标记,然后进入“after after body”模式。接收到文件结束标记后,解析过程就此结束,dom树已经建立完毕(不是加载完毕,在DOMContentLoaded之前,document.readyState = ‘interactive ’)。
结束后,此时文档被标注为交互状态,浏览器开始解析那些script标签上带有“defer”脚本,也就是那些应在文档解析完成后才执行的脚本,文档状态将设置为“完成”,执行完毕触发DOMContentLoaded事件(当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,不会等待样式表、图像和iframe的完成加载)。
1.4 css和js解析过程
1.4.1 css解析
解析CSS会产生CSS规则树,前面已经说到,html不是与上下文无关的语法,而css和js是与上下文无关的语法,所以常规的解析方法都可以用。对于建立CSS 规则树,是需要比照着DOM树来的。CSS匹配DOM树主要是从右到左解析CSS选择器。解析CSS的顺序是浏览器的样式 -> 用户自定义的样式 -> 页面的link标签等引进来的样式 -> 写在style标签里面的内联样式
样式表不会更改 DOM 树,因此没有必要等待样式表并停止文档解析。而脚本在文档解析阶段会请求样式信息时还没有加载和解析样式,脚本就会获得错误的回复。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
1.4.2 js解析(重要)
-
网络整个解析的过程是同步的,会暂停 DOM 的解析。解析器遇到 script标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。
-
如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。
-
目前浏览器的script标签是并行下载的,他们互相之间不会阻塞,但是会阻塞其他资源(图片)的下载
所以为了用户体验,后来有了async和defer,将脚本标记为异步,不会阻塞其他线程解析和执行。标注为“defer”的script不会停止文档解析,而是等到解析结束才执行;标注为“async”只能引用外部脚本,下载完马上执行,而且不能保证加载顺序。
脚本的预解析:在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
脚本主要是通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree.
另外,我们又可以想到一个问题,为什么jsonp能response一个类eval字符串就马上执行呢?其实也是因为普通的script标签解析完成就马上执行,我们在服务器那边大概是这样子返回: res.end('callback('+data+')')
整个过程,就是:动态创建script标签,src为服务器的一个get请求接口,遇到src当然马上请求服务器,然后服务器返回处理data的callback函数这样子的代码。其实,我们可以看作是前端发get请求,服务端响应文档是js文件,而且这个文件只有一行代码:callback(data)。当然你可以写很多代码,不过一般没见过有人这么干。
2. 渲染树
html、css、js解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree(渲染树)。
- 在渲染树中,会把DOM树中没有的元素给去除,比如head标签以及里面的内容,以及display:none的元素也会被去除,但是 visibility 属性值为“hidden”的元素仍会显示
- CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上渲染树上的每个Element,也就是所谓的Frame(Firefox 将渲染树中的元素称为frame,WebKit 的是呈现器或呈现对象,其实就是DOM节点,别以为是什么高大上的东西。 呈现器知道如何布局并将自身及其子元素绘制出来 )。然后,计算每个Frame的位置,这通常是layout和reflow过程中发生。
- 一旦渲染树构建完成,浏览器会把树里面的内容绘制在屏幕上。
需要注意的点:
-
有一些 DOM 元素对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。如“select”元素有 3 个呈现器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的呈现器而添加。
-
inline 元素只能包含 block 元素或 inline 元素中的一种。如果出现了混合内容,则应创建匿名的 block 呈现器,以包裹 inline 元素。所以我们平时的inline-block可以设置宽高。
-
有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。脱离文档流的浮动定位和绝对定位的元素就是这样,被放置在树中的其他地方,并映射到真正的frame,而放在原位的是占位frame。
2.1 CSS样式计算
构建渲染树之前,需要计算每一个呈现对象的可视化属性。这是通过计算每个元素的样式属性来完成的。
Firefox:CSS 解析生成 CSS Rule Tree,通过比对DOM生成Style Context Tree,然后Firefox通过把Style Context Tree和其Render Tree(Frame Tree)关联上完成样式计算
Webkit:把Style对象直接存在了相应的DOM结点上了
样式被js改变过的话,会重新计算样式(Recalculate Style)。Recalculate被触发的时,处理脚本给元素设置的样式。Recalculate Style会计算Render树(渲染树),然后从根节点开始进行页面渲染,将CSS附加到DOM上的过程。所以任何企图改变元素样式的操作都会触发Recalculate,在JavaScript执行完成后才触发的,下面将会讲到的layout也是。
2.2 构建渲染树
Firefox:系统会针对 DOM 更新注册展示层,作为侦听器。展示层将框架创建工作委托FrameConstructor,由该构造器解析样式并创建frame。
WebKit:解析样式和创建呈现器的过程称为“附加”。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。
处理 html 和 body 标记就会构建渲染树根节点。这个根节点呈现对象对应于 CSS 规范中所说的容器 block,这是最上层的 block,包含了其他所有 block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame,而 WebKit 称之为 RenderView。这就是文档所指向的呈现对象。渲染树的其余部分以 DOM 树节点插入的形式来构建。
3. 布局(重要)
呈现器在创建完成并添加到渲染树时,并不包含位置和大小信息。**计算这些值的过程**称为布局(layout)或重排(repaint)。这个得记住了,记准确了!为什么呢?计算offsetWidth和offsetHeight的、js操作dom、改变style属性时候,都会引发重排!
前面通过样式计算确定了每个DOM元素的样式,这一步就是具体计算每个DOM元素最终在屏幕上显示的大小和位置。Web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,元素的width变化会影响其后代元素的宽度。因此,layout过程是经常发生的。
HTML 是流式布局,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。坐标系是相对于根节点而建立的,使用的是上坐标和左坐标。根呈现器的位置左边是 0,0,其尺寸为视口。layout过程计算一个元素绝对的位置和尺寸。Layout计算的是布局位置信息。任何有可能改变元素位置或大小的样式都会触发这个Layout事件。
layout是一个递归的过程。它从根呈现器(对应于 HTML 文档的 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。所有的呈现器都有一个“layout”或者“reflow”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。任何有可能改变元素位置或大小的样式都会触发这个Layout事件。
由于元素相覆盖,相互影响,稍有不慎的操作就有可能导致一次自上而下的布局计算。所以我们在进行元素操作的时候要一再小心尽量避免修改这些重新布局的属性。当你修改了元素的样式(比如width、height或者position等)也就是修改了layout,那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个reflow过程完成重新布局。被reflow的元素,接下来也会激发绘制过程也就是重绘(repaint),最后激发渲染层合并过程,生成最后的画面。由于元素相覆盖,相互影响,稍有不慎的操作就有可能导致一次自上而下的布局计算。所以我们在进行元素操作的时候要一再小心尽量避免修改这些重新布局的属性。
如果呈现器在布局过程中需要换行,会立即停止布局,并告知其父代需要换行。父代会创建额外的呈现器,并对其调用布局。
几种布局模式
- 父呈现器确定自己的宽度。
- 父呈现器依次处理子呈现器,并且放置子呈现器(设置 x,y 坐标)。如果有必要,调用子呈现器的布局,这会计算子呈现器的高度。
- 父呈现器根据子呈现器的累加高度以及边距和补白的高度来设置自身高度,此值也可供父呈现器的父呈现器使用。
3.1 Dirty 位系统(Dirty bit system)
为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty 位”系统。如果某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则需要进行布局。类似于脏检测。
有“dirty”和“children are dirty”两种标记方法。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。dirty就是自己都变化了。
3.2 全局布局和增量布局
- 全局布局:指触发了整个呈现树范围的布局,呈现器的全局样式更改或者屏幕大小调整都会触发全局布局。
- 增量布局:采用增量方式,也就是只对 dirty 呈现器进行布局(这样可能存在需要进行额外布局的弊端)。
当呈现器为 dirty 时,会异步触发增量布局。例如,当来自网络的额外内容添加到 DOM 树之后,新的呈现器附加到了呈现树中。
3.3 异步布局和同步布局
增量布局是异步执行的。Firefox 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。WebKit 也有用于执行增量布局的计时器:对呈现树进行遍历,并对 dirty 呈现器进行布局。 请求样式信息(例如“offsetHeight”)的脚本可同步触发增量布局。 全局布局往往是同步触发的。 有时,当初始布局完成之后,如果一些属性(如滚动位置)发生变化,布局就会作为回调而触发。
浏览器的自身优化
如果布局是由“大小调整”或呈现器的位置(而非大小)改变而触发的,那么可以从缓存中获取呈现器的大小,而无需重新计算。 在某些情况下,只有一个子树进行了修改,因此无需从根节点开始布局。这适用于在本地进行更改而不影响周围元素的情况,例如在文本字段中插入文本(否则每次键盘输入都将触发从根节点开始的布局)。
因为这个优化方案,所以你每改一次样式,它就不会reflow或repaint一次。但是有些情况,如果我们的程序需要某些特殊的值,那么浏览器需要返回最新的值,而会有一些样式的改变,从而造成频繁的reflow/repaint。比如获取下面这些值,浏览器会马上进行reflow:
offsetTop, offsetLeft, offsetWidth, offsetHeight scrollTop/Left/Width/Height clientTop/Left/Width/Height getComputedStyle(), currentStyle
我们可以做的性能优化
大家倒背如流的老话,再啰嗦一遍:尽量减少重绘重排。具体:
- 不要一条一条地修改DOM的样式(用class批量操作)
- 缓存dom节点,供后面使用(for循环,取html集合长度,你懂的)
- 把DOM离线后修改(documentFragment、虚拟dom、把它display:none再改再显示)
- 尽量修改层级比较低的DOM
- 有动画的DOM使用fixed或absoult的position,脱离文档流
4. 重绘与重排(重要)
4.1 重排(reflow)
重排(也叫回流)会计算页面布局(Layout)。某个节点Reflow时会重新计算节点的尺寸和位置,而且还有可能触其后代节点reflow。重排后,浏览器会重新绘制受影响的部分到屏幕,该过程称为重绘。另外,DOM变化不一定都会影响几何属性,比如改变一个元素的背景色不影响宽高,这种情况下只会发生重绘,代价较小。
当DOM的变化影响了元素的几何属性(宽或高),浏览器需要重新计算元素的几何属性,由于流式布局其他元素的几何属性和位置也受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。 reflow 会从根节点开始递归往下,依次计算所有的结点几何尺寸和位置,在reflow过程中,可能会增加一些frame,如文本字符串。DOM 树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。
当渲染树的一部分(或全部)因为元素的尺寸、布局、隐藏等改变而需要重新构建。所以,每个页面至少需要一次reflow,就是页面第一次加载的时候。
4.2 重绘(repaint)
repaint(重绘)遍历所有节点,检测节点的可见性、颜色、轮廓等可见的样式属性,然后根据检测的结果更新页面的响应部分。当渲染树中的一些元素需要更新一些不会改变元素不局的属性,比如只是影响元素的外观、风格、而不会影响布局的那些属性,这时候就只发生重绘。当然,页面首次加载也是要重绘一次的。
光栅:光栅主要是针对图形的一个栅格化过程。现代浏览器中主要的绘制工作主要用光栅化软件来完成。所以元素重绘由这个元素和绘制层级的关系,来决定的是否会很大程度影响你的性能-,如果这个元素盖住的多层元素都被重新绘制,性能损耗当然大。
5. paint(绘制)
在绘制阶段,系统会遍历渲染树,并调用呈现器的“paint”方法,将呈现器的内容绘制成位图。绘制工作是使用用户界面基础组件完成的 你所看见的一切都会触发paint。包括拖动滚动条,鼠标选择中文字等这些完全不改变样式,只改变显示结果的动作都会触发paint。paint的工作就是把文档中用户可见的那一部分展现给用户。paint是把layout和样式计算的结果直接在浏览器视窗上绘制出来,它并不实现具体的元素计算,只是layout后面的那一步。
绘制顺序:背景颜色->背景图片->边框->子代->轮廓
其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。
再说回来,在样式发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个渲染树都会进行重新布局和绘制。
6. composite(重要)
概念不复杂,即是渲染层合并,我们将渲染树绘制后,形成一个个图层,最后把它们组合起来显示到屏幕。渲染层合并。前面也说过,对于页面中DOM元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将绘制的位图发送给GPU绘制到屏幕上,将所有层按照合理的顺序合并成一个图层,然后在屏幕上呈现。
对于有位置重叠的元素的页面,这个过程尤其重要,因为一量图层的合并顺序出错,将会导致元素显示异常。另外,这部分主要的是这涉及到我们常说的GPU加速的问题。
说到性能优化,针对页面渲染过程的话,我们希望的是代价最小,避免多余的性能损失,少一点让浏览器做的步骤。比如我们可以分析一下开头的那幅图:
明显,我们改的越深,代价越大,所以我们只改最后一个流程——合成的时候,性能是最好的。浏览器会为使用了transform或者animation的元素单独创建一个层。当有单独的层之后,此元素的Repaint操作将只需要更新自己,不用影响到别,局部更新。所以开启了硬件加速的动画会变得流畅很多。
因为每个页面元素都有一个独立的渲染进程,包含了主线程和合成线程,主线程负责js的执行、CSS样式计算、计算Layout、将页面元素绘制成位图(Paint)、发送位图给合成线程。合成线程则主要负责将位图发送给GPU、计算页面的可见部分和即将可见部分(滚动)、通知GPU绘制位图到屏幕上。加上一个点,GPU对于动画图形的渲染处理比CPU要快,那么就可以达到加速的效果。
注意不能滥用GPU加速,一定要分析其实际性能表现。因为GPU加速创建渲染层是有代价的,每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。并且在移动端 GPU 和 CPU 的带宽有限制,创建的渲染层过多时,合成也会消耗跟多的时间,随之而来的就是耗电更多,内存占用更多。过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。
7. 浏览器加载的时间线(重要)
这是补充前面的html解析为dom部分的内容。
- 创建document对象,解析html,将元素对象和文本内容添加到文档中,此时document.readyState = 'loading'
- 遇到link外部css的时候,创建新的线程异步加载,继续解析html
- 遇到有src的scripts(没有async和defer标记)加载外部的js时,同步加载并阻塞解析html,而且加载完马上执行
- 遇到设置async和defer的script,创建新的线程异步加载,继续解析html。async加载完马上执行,defer在DOMContentLoaded前执行
- 遇到带有src的img,解析dom结构,再异步加载src的图片资源,不会等待img加载完成继续解析文档。另外,img要等待css加载完才解码,所以css阻塞图片的呈现,类似于js阻塞html解析一样。可以想一下,如果css被设置为display:none,还有意义吗?所以此时虽然对后台有请求但不解码
- 文档解析完毕,document.readyState = 'interactive'
- 此时带有defer的js开始按顺序执行
- DOMContentLoaded触发,程序从同步脚本执行转化为事件驱动阶段(类似ele.onclick = handel已经开始生效)
- 当所有的script加载完成并且成功执行、img和css加载完毕,document.readyState = 'completed',触发onload事件
- 异步响应ui行为,开始交互
补充:script和link标签的问题
明显,CSSOM树和DOM树是互不关联的两个过程。平时我们把link标签放部头而script放body尾部,因为js阻塞阻塞DOM树的构建。但是js需要查询CSS信息,所以js还要等待CSSOM树构建完才可以执行。这就造成CSS阻塞了js,js阻塞了DOM树构建。所以我们只要设置link的preload来预加载css文件,解决了js执行时CSSOM树还没构建好的阻塞问题。当然,script异步加载也是另外的方法。
总的来说,参考一下很多人说过的规律:
- CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。
- JS 阻塞 DOM 解析,但浏览器会"偷看"DOM,提前下载资源。
- 浏览器遇到 script且没有defer或async属性的标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。