先从文件的下载和解析时机开始聊起
作者:互联网
03 先从文件的下载和解析时机开始聊起
本文的图,建议重新打开链接放大看
那么从什么时候开始呢?
- 从URL输入地址开始,浏览器的主进程调度其它进程开始服务
- DNS解析域名,找到服务器地址,和服务器建立TCP连接
- 发送一个GET网络请求,根据URL(统一资源符)描述,返回相应报文和资源
- 浏览器获取HTML资源,开始解析…。
这里就涉及到一个问题:浏览器一定要等到HTML全部下载完毕,才开始解析吗?
我的答案:是的,它表现的行为是这样的,也应该这么做。
如何证明:你无论去那个网站去看,无论多慢,或者多快,一直要等到HTML的请求完成,资源下载并加载完成,才会开始解析
具体请看证明1
一、解析开始
(1) 解析什么
下载HTML文件,直到下载完成才能进行第二步 => 解析HTML (Parse HTML)。HTML、JavaScript和CSS中,除了CSS,没有文件可以边被下载件,然后边被解析(都无法知道是否能下载完成,是否有效,干嘛要解析,即使是在两个可以同时工作进程上),但是这并不意味这,当前文件在解析时,不能下载其它资源(这就涉及到资源的并发下载)。
PS:浏览器的渲染进程的GUI线程在解析HTML。网络资源的下载是浏览器的主进程在执行,所以它有能力同时解析资源和下载其它资源(理想状态下)。
(2) 在Parse Html的大致过程
GUI线程,先解析html(HTML Parser)构建DOM树(DOM Tree),注意解析完DOM树;再解析CSS(CSS Parser )构建CSS对象模型(CSSOM <=> CSS Object Tree);DOM树和CSS对象模型进行连接(attachment),生成渲染树(Render Tree);进行回流(根据Render Tree 计算他们在设备视口的确切位置和大小,也叫 Layout );进行重绘(拿到回流之后的呈现树,得到节点的绝对像素,主要是视觉上的效果,注意只是计算了像素,还未渲染展示,它也叫Painting);最后,Display 将像素发送给GPU,展示在也页面上。
PS:这个Display操作,是交付给GPU的,涉及GPU的是浏览器进程 即 GPU进程,因此 GUI线程的Parse HTML 和渲染进程的Display可以并行!这意味着浏览器是可以边进行GUI线程的解析,边进行GPU的Display工作。
关于上面的过程还是有很多细节可以扣:
上面的操作真的是所有的情况吗?那我们来自己走一遍Parse HTML,但是在这之前,我们还需要一些基本认识
=> 文件下载及其解析的时机!
(3) 处理HTML、脚本和样式表的顺序
前面说过浏览器是有能力边资源下载和边解析,并且资源下载可以多线程并行下载,但是为什么会有阻塞现象呢?
先来了解一些相对权威的东西
-
第一点:脚本
网络的模型是同步的。解析器遇到 <script> 标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕,如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续,此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。作者也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行PS:补充说明 JavaScript 的性能优化:加载和执行
我们可以发现一个有趣的现象:第一个 JavaScript 文件开始下载,与此同时阻塞了页面其他文件的下载。此外,从 script1.js 下载完成到 script2.js 开始下载前存在一个延时,这段时间正好是 script1.js 文件的执行过程。每个文件必须等到前一个文件下载并执行完成才会开始下载。在这些文件逐个下载过程中,用户看到的是一片空白的页面。从 IE 8、Firefox 3.5、Safari 4 和 Chrome 2 开始都允许并行下载 JavaScript 文件。这是个好消息,因为 <script> 标签在下载外部资源时不会阻塞其他 <script> 标签。遗憾的是,JavaScript 下载过程仍然会阻塞其他资源的下载,比如样式文件和图片。尽管脚本的下载过程不会互相影响,但页面仍然必须等待所有 JavaScript 代码下载并执行完成才能继续。因此,尽管最新的浏览器通过允许并行下载提高了性能,但问题尚未完全解决,脚本阻塞仍然是一个问题。
由于脚本会阻塞页面其他资源的下载,因此推荐将所有 <script> 标签尽可能放到 标签的底部,以尽量减少对整个页面下载的影响。
-
第二点:预解析
WebKit 和 Firefox 都进行了这项优化(1中说的模型)。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。 -
第三点:样式表
另一方面,样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
HTML解析的时候,允许CSS资源并行下载(如果是链接的话,因为CSS的执行(即Parse)对DOM的解析并没有影响),但是对JS却不同(JavaScript能够对DOM节点进行操作,会影响DOM Tree的构建),具体请况如下
PS:注意 本文作者认为JS只阻塞比JS后下载的资源下载(即在js外联后面的链接资源,一定要等到JavaScript下载完成,才能完成下载)
(3) 整理一下 (1)、(2)和(3):
刚才1的时候说过了,要走一遍Parse HTML,接下来我们要做的事情就是这个。
前提知识点:1. style标签的样式和link外联的样式是一个级别的(从浏览器对样式的处理可以看的出来,内嵌的样式会被解析为DOM Tree的一部分);2. 资源解析默认不会和资源下载冲突,因为是两个进程的东西; 3. 以下会涉及一些Chrome Devtools 的performance的一些名词,如果你还没有怎么了解它,我不推荐以看下面的文章。
-
在Parse HTML开始时,也意味着HTML全部下载完毕,这个时候并不会发生和预解析一样的操作(即不会去扫描剩下的HTML加载对应资源),而是等到Parse HTML解析到Link标签外联和script的哪一部分的时候,才会通知浏览器进程下载对应的资源。
-
假设,HTML解析了一部分,从这个点开始继续解析
-
如果碰到style内联标签,style标签会作为DOM Tree的一部分,但是style里的样式并不会。你可能觉得我在开玩笑,但是我认真的告诉你,DOM Tree包含 标签 和 内嵌样式(后面会讲到,
JS中操作的DOM就是DOM Tree,内嵌样式其实是作为元素的属性),一个能证明的点就是 Element.style 在访问的时候只能获取到 内嵌样式的信息,并且它是可以被修改的(JS操作DOM => JS操作DOM Tree)。而内联样式(外联样式一样)却只能被Window的getComputedStyle访问到(内联样式和外联样式,最后都是到CSSOM上) -
既然是这样,那style内联标签怎么解析的呢?这就涉及两个东西DOM和CSSOM,虽然资料上说他们是Parse … 构建 …,但是CSSOM和DOM就是个数据结构的树,有树根,和子树,子叶,他们功能构成整体的树 => CSSOM 或者 DOM,因此,在一开始解析HTML的时候,DOM Tree已经存在,只不过它是一个空树,当然也可以是一个document作为根节点的单节点树。CSSOM也是如此。
当解析到style的时候,Parse HTML,这个阶段搁置,开始进行Parse CSS,将样式解析到CSSOM(这个时候可能会说performance在这段时间并没有进行Parse CSS显示啊? 对的,是没有显示,但是由于Window的getComputedStyle可以访问到它,所有它必须解析,但是performance的确对它的确没有明确表明。但是一个值得注意的task => Recalucate Style是每次解析完HTML都会发生,Recalucate Style字面上的意思就是重新计算样式,是将DOM Tree的内嵌样式和CSSOM进行一次合并,以便合成Render Tree渲染展示,我更倾向于认为它是在进行attachment操作。 -
当遇到link 的 外联 CSS的时候,GUI线程通知浏览器主进程下载CSS资源,这段下载CSS资源的时间,Parse HTML不会停下来(CSS的是否解析在逻辑上并不会影响DOM Tree的构建,即使是Link标签,其实已经直接识别到了),继续解析剩下的HTML;这个CSS一直到下载完成,却不会立刻的进行Parse CSS,一般都会等到Parse HTML完成再去解析(在performance会明显标记出来这个任务触发),可是有特殊情况 => 在该CSS后面的HTML中,包含script代码!
-
当遇到 嵌入JavsScript代码的script 标签(这里不考虑async 和 defer 属性,当你完全理解这些东西,你自然就理解了,我还是希望下一篇去讲这些东西),由于 (3) 中的第一点,当即停下Parse HTML,转而将JavaScript引擎线程唤醒,执行JS(在这里,你可以访问到之前所有的CSS,内嵌样式通过Element.style,其它样式通过Window.getComputedStyle获取)操作DOM,获取样式。
基于这一点,也产生了一个问题,JS要能获取到前面的CSS,对于内联或者说内嵌,都好理解,但是外联的CSS还要下载,这肯定会消耗一定的时间,如果当Parse HTML已经解析到了script的时候,之前解析到的CSS还未下载完成,那么就会发生阻塞(很多人把它叫做CSS半阻塞);这个CSS会阻塞到直到下载完成,并进行Parse CSS(在performance会明显标记出来这个任务触发)。直到Parse CSS完成,切换回JavaScript引擎线程,继续解析编译JavaScript;当JS执行完成,通知GUI线程继续执行Parse HTML。
具体请看证明2 -
但是,大部分情况下,script标签都是有src的属性,包含外部文件,这种情况尤为复杂。
一旦HTML解析到该标签,老样子,通知下载,但是JavaScript下载时,会阻塞除了其它外部script文件的其它资源的下载,样式和图片。并且更为难受的是,它也会让Parse HTML立刻停止解析,直到下载完成,并执行完所有的代码,才回复到解析HTML。但是在执行JavaScript代码的时候,不会阻止剩余HTML的其它资源的下载(预解析)。但是我实际测试发现,JavaScript在执行阶段确实会请求剩余HTML的资源进行加载,但是只要在JavaScript执行完成,才会剩余的资源才会下载完成。
具体请看证明3
PS:有一个很变态,但是有意思的事情,下面一段代码
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<!-- bg.jpg很大,index.js很小,但是JavaScript执行时间很长 -->
<img src="./bg.jpg" alt="">
<script src="./index.js"></script>
<link rel="stylesheet" href="./global.css">
</head>
<body>
</body>
</html>
上面情况,img标签放在script上面,如果是“美好的情况”,图片下载图片的,js执行js的,互不干扰,但是JS在执行过程中,一直无法将img下载完成,直到js结束,它才有下载完成的趋势。但是有趣的是,这对网页来说视乎并没有影响,因为js线程一直阻塞了GUI线程,HTML还未解析完成,用户本来就看不见页面。还有一点值得注意的就是,IMG在等待下载完成,并轮到它解析的时候,会进行一次Parse HTML,解析的是该img标签,并在之后执行一些js的文档状态变化回调函数。
-
当完成Parse HTML的DOM Tree 和 Parse CSS的CSSOM,进行连接attachment形成Render Tree,注意Render Tree并不包含所有DOM Tree节点,比如display:none,是不需要显示的。(这也说明DOM操作的不是Render Tree而是DOM Tree)
-
再次进行layout,计算样式,再进行Painting计算像素点,最后进行交付GPU进行渲染
引用一段话:需要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来
这句话,一般发生
-
在网络资源都还在下载中,而HTML早就已经解析完成,这个时候浏览器直接进行布局,绘制,展示操作,不会等待资源下载才进行。当资源下载完成时,专门对资源处理解析,在对DOM Tree和CSSOM进行更新,在进行一次之后一系列的操作。
-
当HTML太大,如果有一个10000行代码的html,它会在解析大约3500行,强制停止HTML的解析,但是不会跳过Parse CSS,最后进行layout,多次循环处理,进行多轮的Parse HTML
三、能解决什么问题?
(1) 什么时候HTML成功被渲染出现了
HTML要在GPU渲染之后,才会展示,但是你可能会说那HTML都没解析完,如果里面有JS,为什么能访问DOM?
首先:上面说过,我认为JS能访问的DOM是DOM Tree,所以只要DOM Tree的对于节点已经被解析,就能被JS访问,HTML的解析就是从上到下,所以JS刚好能访问在其之前的所有元素。
(2) readstatuschange、load、DOMContentLoaded
readystate: interactive
DOMContentLoaded DOM Tree建立完成(HTML完全被加载并解析)
readystate: complete
load 所有资源加载完成包括样式、图片
事件执行顺序
(3) 一个! tab键生成的HTML渲染发生事件
- Send Request 发送请求请求页面
- Receive Response 接收响应
- domLoading dom加载
- Receive Data 接收数据
- Finsh Loading 资源加载完成
- Parse HTML
- dominteractive dom 交互
- Event: readstatechange
- domContentLoadedEventStart dom内容加载事件触发
- Event: DOMContentLoaded dom内容加载事件执行
- domContentLoadedEventEnd dom内容加载事件执行完成
- Recalculate Style计算样式,进行attachment
- domComplete dom完成
- Event: readystatechange
- loadEventStart load事件触发
- Event: load load事件执行
- loadEventEnd load事件完成
- pageshow 页面展示之前的事件触发
- Layout 布局(回流)
- Update Layer Tree 分层
- Paint 计算绘制像素点
- Composite Layers 合并分层
上面有一个存疑的是
domLoading和domComplete,说的真的是DOM Tree建立吗?
我的回答:不是的,domComplete建立完成之前发生了attachment,说明是Render Tree树建立,而紧接触发readystatechange,所有我认为
domComplete指的是DOM Tree和CSSOM都建立好的情况,这种情况下,dom又和JS的documennt似乎一致,或许说domLoading就是完成document的建立。
(4) defer,async脚本下载和执行时机
- defer下载不会阻塞其它资源解析,直到下载完成也不会立即执行,等到文档被完全解析和显示,再执行。defer会按照原来在HTML中的顺序执行
- async 下载不会阻塞其它资源解析,一旦下载完成立即执行。async不会按照HTML的解析他们的顺序执行
四、附件证明
打开Chrome无痕模式,打开f12,进入performancetab页,进行reload
(1) HTML需要全部下载完成,才开始解析吗?
https://www.taobao.com 进入淘宝网,进行性能分析
标签:DOM,聊起,Tree,从文件,HTML,CSS,解析,下载 来源: https://blog.csdn.net/wucan111/article/details/110432682