乐趣区

前端渲染过程的二三事

前端渲染过程的二三事
本文不会介绍整个前端渲染过程的步骤,只是记录最近阅读的文章的些许思考和感悟。(文章地址一(系列), 文章地址二)
希望大家在阅读这篇文章之前能将上述文章仔细浏览一篇,因为本文所述基本是基于其内容。
Navigation Timing API 和浏览器加载事件
Navigation Timing API:可通过打印(performance)查看;
浏览器加载事件:domContentLoaded,onload

上图整理了 Navigation Timing API 中的一些事件和浏览器加载事件的发生顺序。
这些事件的含义是什么呢?我们直接引用文章地址一(系列)的介绍:

domLoading:这是整个过程的起始时间戳,浏览器即将开始解析第一批收到的 HTML 文档字节。
domInteractive:表示浏览器完成对所有 HTML 的解析并且 DOM 构建完成的时间点。

domContentLoaded:表示 DOM 准备就绪并且没有样式表阻止 JavaScript 执行的时间点,这意味着现在我们可以构建渲染树了。
许多 JavaScript 框架都会等待此事件发生后,才开始执行它们自己的逻辑。因此,浏览器会捕获 EventStart 和 EventEnd 时间戳,让我们能够追踪执行所花费的时间。

domComplete:顾名思义,所有处理完成,并且网页上的所有资源(图像等)都已下载完毕,也就是说,加载转环已停止旋转。
loadEvent:作为每个网页加载的最后一步,浏览器会触发 onload 事件,以便触发额外的应用逻辑。

看了上述事件的介绍,大致了解了每个事件所代表的含义,但也带着少许疑惑:

HTML 解析是分批进行的吗?为什么要分批进行?
domContentLoaded 事件结束后可以构建渲染树了,是否意味着 CSSOM 树构建也在该事件之前就已完成?
domContentLoaded 事件表示 DOM 准备就绪并且没有样式表阻止 JavaScript 执行的时间点,可是 JavaScript 执行是在构建 DOM 树之前,那这是不是意味着该事件在 DOM 树构建完成后就执行了?若是这与 domInteractive 事件有什么区别呢?

首先看第一个问题,我们直接用 chrome 控制台的 performance 来看效果。
上图中,上方的 Network 的蓝条是 HTML 的下载时间,下方箭头所指是 HTML 的解析时间。
从中我们可以得出 HTML 解析的确是分批进行,并且解析并不需要等 HTML 完全下载完。那为什么要分批进行呢?这个问题我找了许多资料也没有得出结论,于是自己从中思考。假设不是分批进行,那实现会有两种情况:1. 等 HTML 全部下载完,再一起解析;2. 每下载一定量的 HTML 就将其放入解析器等待排队解析。前者首先肯定被排除,若 HTML 很大,会影响首屏加载速度,后者按理速度更快,或许其实现的难度以及可能会带来的一些问题而没有采用?这个问题思考了良久,感觉从理论上是可行的,但从技术角度,由于自己这方面的知识实在薄弱,所以也无法得知其实现的过程是否存在技术瓶颈。
第二个问题:CSSOM 树构建是否在 domContentLoaded 事件之前?
话不多说我们自己实践。
首先我先创建一个 HTML 文件并写入少许标签,再创建一个 CSS 并于 HTML 引入,在 CSS 文件中写入大量内容(自己实践时写了 5W 多行)。然后用 performance 查看。

上图每个箭头代表的含义:

HTML 解析结束时间
domContentLoaded 加载时间
CSS 文件下载完的时间
CSS 文件解析的时间

从这个步骤以及其所处时间,我们可以清晰的得出,该结论不准确。那么该作者为什么会得出该结论呢,是他犯错了吗?我随后发现他在这篇文章下面还写了一句“domContentLoaded 一般表示 DOM 和 CSSOM 均准备就绪的时间点”。那么这句话意味着大部分的时候 CSSOM 树的构建是在 domContentLoaded 事件之前。可是这个大部分又指的是什么情况,这又涉及到另一个知识点“DOM 树,CSSOM 树,JS 的三角关系”,构造 DOM 树时遇见 JS 会先解析执行 JS,而在解析执行 JS 时遇到 CSSOM, 又会先构造 CSSOM 树,这个过程稍后会具体说明。那么现在我们可以明白这个问题的关键所在了,因为在大部分页面中是拥有 JS 的,而由于其解析顺序,那么在 domContentLoaded 事件之前必定已经成功构造 CSSOM 树。
第三个问题:domInteractive 与 domContentLoaded 的区别是什么呢?这两个事件中间是否还会进行其他操作?
我们都知道 script 标签有 defer 和 async 两个属性。有了这两个属性,浏览器就会加一个进程下载 JS。那么下载完的执行时间点是在什么时候?其中 async 会在 JS 下载完后立马执行,也正是这个原因,会导致 JS 的执行顺序不一定按标签的从上至下,而是按照下载完的时间。那么 defer 属性的执行时间呢,我想大家应该都能猜到了,它的执行时间点的确就是在上述的两个事件之间,那么我们也就得知这两个事件的区别所在。
三大树
我们都知道前端渲染有三大树:DOM 树,CSSOM 树,RENDER 树。那么这三大树的构造时间和上述的事件执行时间的顺序又是怎样的呢。
其中 DOM 树和 RENDER 树所在位置其实是显而易见的,并且在之前内容也已经指出。DOM 树在 domInteractive 事件之前,RENDER 树在 domContentLoaded 事件之后。但是 CSSOM 树就难以捉摸,其与 DOM 树的关系,完全受到是否拥有 JS 影响。
在确定 CSSOM 树所处位置前,我们先确定一个上面提到的概念:构造 DOM 树时遇见 JS 会先解析执行 JS,而在解析执行 JS 时遇到 CSSOM, 又会先构造 CSSOM 树。官方给出的原因是,JS 会使用 document.write 而改变 DOM 树,所以构造 DOM 树时碰到 JS 会先执行 JS;而 JS 在执行时,需要查找 CSS,所以执行 JS 时,碰到构造 CSSOM 树,会先构造 CSSOM 树。但是这里有一点奇怪的时,JS 也可以通过创造 Link 标签的方式改变 CSSOM 树,所以个人感觉官方的这种解释有点牵强。不过官方的解释虽然不能让人完全信服,但这执行顺序是不会有错的。
接下来我们分别通过有无 JS 两大类确认 CSSOM 树所处位置:

无 JS 的情况由于 DOM 树和 CSSOM 树是并行解析的情况,所以这两个树构建完成的顺序完全无法固定,只由它们自己本身大小有关。因此 CSSOM 树构建完成的时间既可能在 domInteractive 之前,可能在 domContentLoaded 之后,也可能在这两事件之间。
有 JS 的情况这里我们先假设 CSS 文件很小,在没还解析到 JS 时就已完成解析,那么这种情况其实必定发生在 domInteractive 之前,因为都还没解析到 JS,说明 DOM 树必定没有构建完成。那么再假设 CSS 文件很大,然后中途遇到 JS 文件,这时候 JS 文件发现在构造 CSSOM 树,其就会等待 CSSOM 树构建完成后再解析执行 JS,所以这种情况 CSSOM 也必定在 domInteractive 之前。但是有 2 个有意思的情况:
(1) 如果我们动态创建 link 标签并添加到 html 中,那么又会发生什么呢?若 JS 还没解析执行完,那么会停止 JS 而去解析 CSS,若 JS 已执行完那么 CSSOM 树其实也不是必定在 domInteractive 之前。(当然这可能已经不算初次渲染构建)(2) link 标签放到 JS 后面又会发生什么呢?这种情况我发现不管我怎么尝试,CSSOM 树必定在 DOM 树构建之前构建,但其又在 JS 执行完成后。如果有兴趣的同学可以查查是为什么。

总结
其实前端渲染是一个很庞大的知识点,并且其涉及的周边知识也及其庞大,本文只是对其中一个小知识点做了思考和实践。最后要说的一点是,以上内容,纯属个人见解,如有不当,请多指教。

退出移动版