DOM, CSS, JS的阻塞,解析渲染顺序

4,324 阅读7分钟

DOMContentLoaded

当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。另一个不同的事件 load 应该仅用于检测一个完全加载的页面。 在使用 DOMContentLoaded 更加合适的情况下使用 load 是一个令人难以置信的流行的错误,所以要谨慎。注意:DOMContentLoaded 事件必须等待script之前的样式表加载解析完成才会触发。

// 客户端 html 代码
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script>
		document.addEventListener('DOMContentLoaded',function(){
			console.log('3 seconds passed');
		});
	</script>
	<link rel="stylesheet" href="http://localhost:3000/sleep">
</head>
<body>
<div><a >1234567</a></div>
</body>

// express 代码
var express = require('express');
var app = express();

app.get('/sleep', function(req, res, next) {
	res.type('text/css')
	setTimeout(()=>{
		res.send("a {\n" +
			"  color: red;\n" +
			"}")

	},3000)
});

当script在上,link在下时:

控制台立即显示 3 seconds passed。 页面是空白的。 过三秒后,页面直接显示红字。

当link在上,script在下时:

页面是空白的。过三秒后,

控制台显示 3 seconds passed。

页面直接显示红字。

说明:css link会阻塞 DOM 渲染

  • css link会阻塞 DOM 渲染,当css加载完成后才会渲染。
  • DOMContentLoaded 事件只等待script之前的样式表
  • 因为:CSS 会阻塞 JS 执行,JS 会阻塞 DOM解析,下面会代码说明

因为 DOM 的渲染需要 DOM 树和 CSSOM 树共同来生成渲染树,所以在 CSS 加载完成之前, DOM 是不会进行渲染的。还是用上面那个例子,我们会发现 CSS 没有加载完成时,页面上是不显示的,而当 CSS 加载完成的瞬间,标签就被渲染到页面上了。

当把css link放在body内,而不是head内又不同

像截图这种代码,link也在script标签后面。

结果:页面先显示出div内容,过三秒后,字体颜色改变+控制台打印。

CSS 不会阻塞 DOM 解析

// html内的代码
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<link rel="stylesheet" href="http://localhost:3000/sleep">
	<script defer src="js/test.js"></script>

</head>
<body>
<div><a >1234567</a></div>
</body>


//  js/test.js
const div = document.querySelector('div');
console.log(div);

此时我们的 css 还没加载完成,但是我们可以看到 console 选项卡里面 div 标签已经被打印出来了,说明,此时 DOM 已经解析完成触发 DOMContentLoaded 事件。

CSS 会阻塞 JS 执行

<script>
	var starttime = new Date().getTime();
	console.log("page start" + starttime);
</script>
<link rel="stylesheet" href="http://localhost:3000/sleep">
<script src="js/test.js"></script>

//  js/test.js
var endtime = new Date().getTime();
console.log("delay:" + (endtime - starttime));

sleep 3秒后才返回

JS 会阻塞 DOM解析

当解析起遇到 script 标签时,文档会立即停止解析直到脚本执行完毕,如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。因为我们的脚本会操作 DOM,所以在脚本跑完之前浏览器不知道脚本会把 DOM 改成什么样,所以就等脚本执行完再进行解析。

<script >
	let ui = document.getElementById('ui')
	ui.innerText = 'blue'
</script>
<div id="ui">test3</div>

从代码和图中看到,当把dom放在js下面,取不到元素,报错。

script 标签对 dom 渲染的影响

GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。

  • 当遇到script标签为同步的形式,会立即执行js,执行完再渲染。
<div id="ui">test3</div>
<script >

	var now = new Date().getTime();

	let ui = document.getElementById('ui')

	while (new Date().getTime() - now < 3000) {
		continue;
	}

	ui.innerText = 'blue'

</script>

  • 当遇到script标签为引用的形式,会立即把前面已经解析的部分渲染了。
<div id="ui">test3</div>

<script src="js/test.js"></script>

// js/test.js 内容与上面script内的内容一样

另一套测试代码

// 引入远程文件
<script src="http://localhost:3000/test.js"></script>

// express 服务端代码,2秒后再返回
app.get('/test.js', function(req, res, next) {
	res.type('application/javascript')
	setTimeout(()=>{
		res.send(`alert()`)
	},2000)
});

总结

浏览器执行 css dom js 在同步模式下,都是从上往下执行。 渲染等到 JS 引擎空闲时立即被执行。

CSS 会阻塞 JS 执行

JS 会阻塞 DOM解析

CSS 不会阻塞 DOM 解析

Css 会阻塞 DOM 渲染

引用script 会渲染部分已经解析过的dom,

包含内的形式会先执行js,渲染挂起。

主要原因还是script内代码有没有加载完,如果没有加载完就会先渲染一次,加载完就会先执行js,看js有没有改变dom,避免浪费渲染一次,很智能。

比起js的懒加载, css懒加载和初始加载最小体积会更加提高性能。

defer async

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。

defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer。

一句话,defer是“解析完dom再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

preload prefetch

preload

<link>元素的 rel 属性的属性值preload能够让你在你的HTML页面中 <head>元素内部书写一些声明式的资源获取请求,可以指明哪些资源是在页面加载完成后即刻需要的。对于这种即刻需要的资源,你可能希望在页面加载的生命周期的早期阶段就开始获取,在浏览器的主渲染机制介入前就进行预加载。这一机制使得资源可以更早的得到加载并可用,且更不易阻塞页面的初步渲染,进而提升性能。

  • 只是预加载,并不运行。
  • 用下面两种方法即可加载完运行
// 预加载Css
<link rel="preload" as="style" href="async_style.css" onload="this.rel='stylesheet'">

// 预加载js
 <link rel="preload" as="script" href="async_script.js"
         onload="var script = document.createElement('script');
       script.src = this.href;
       document.body.appendChild(script);">


参考 MDN

参考 preload

DEMO preload

Link_prefetching_FAQ

prefetch

浏览器会查找关系类型(rel)为 next 或 prefetch 的 HTML 或 HTTP Link: header。例子:

<link rel="prefetch" href="/images/big.jpeg">

<link rel="prefetch alternate stylesheet" title="Designed for Mozilla" href="mozspecific.css">

<link rel="next" href="2.html">

已经被许多浏览器支持了相当长的时间,但它是意图预获取一些资源, 以备下一个导航/页面使用(比如,当你去到下一个页面时)。 这很好,但对当前的页面并没有什么助益。 此外,浏览器会给使用prefetch的资源一个相对较低的优先级——与使用preload的资源相比。毕竟,当前的页面比下一个页面相对更加重要。

preload > load > prefetch 优先级测试

<link  rel="stylesheet" href="http://localhost:3000/sleep2.load" >

<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload1" >
<link rel="prefetch" as="style" href="http://localhost:3000/sleep2.prefetch" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload2" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload3" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload4" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload5" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload6" >
<link rel="preload" as="style" href="http://localhost:3000/sleep2.preload7" >

<link  rel="stylesheet" href="http://localhost:3000/sleep.load" >

从下图看出,preload比正常load优先级大,会优先占用tcp链接,即使sleep2.load 写在最上面,也要等6条tcp占用释放一条。prefetch 会在空闲时再使用。

  • preload > load > prefetch
  • preload 不会阻塞 DOMContentLoaded
  • preload 会让请求在dom解析之前发出去,在真实请求的时候 会复用preload的请求缓存

html依赖的资源请求是什么时间发出?