前面的话
在浏览器拿到HTML、CSS、JS等外部资源到渲染出页面的过程中,会存在css与Javascript的阻塞资源。这篇文章介绍css与Javascript是如何阻塞资源的。
1: css被视为渲染阻塞资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕,才会进行下一阶段。
2: JavaScript 被认为是解释器阻塞资源,HTML解析会被JS阻塞,它不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。
下面来看看 CSS 与 JavaScript 是具体如何阻塞资源的。
css
<style>p {color:red;} </style>
<link rel="stylesheet" href="index.css">
这样的 link 标签(无论是否 inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至 CSSOM 构建完毕。
解决方法:
- 精简 CSS 并尽快提供它。
- 用媒体类型(media type)和媒体查询(media query)来解除对渲染的阻塞。
<link href="index.css" rel="stylesheet">
<link href= "print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">
第一个资源会加载并阻塞。第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。第三个资源提供了媒体查询,会在符合条件时阻塞渲染。
关于CSS加载的阻塞情况:
-
css加载不会阻塞DOM树的解析
-
css加载会阻塞DOM树的渲染
-
css加载会阻塞后面js语句的执行
没有js的理想情况下,html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)
JavaScript
如果没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script标签之下的HTML元素之前,也就是说不等待后续载入的HTML元素,读到就加载并执行。
解析过程中无论遇到的JavaScript是内联还是外链,只要浏览器遇到 script 标记,唤醒 JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕,才去执行js脚本。因为脚本中可能会操作DOM元素,而如果在加载执行脚本的时候DOM元素并没有被解析,脚本就会因为DOM元素没有生成取不到响应元素,所以实际工程中,我们常常将资源放到文档底部。
解决方法:
- 改变脚本加载次序:就要提到defer与async
defer 与 async 可以改变之前的那些阻塞情形,这两个属性都会使 script 异步加载,然而执行的时机是不一样的。注意 async 与 defer 属性对于 inline-script 都是无效的, 所以下面这个示例中三个 script 标签的代码会从上到下依次执行。
<script async> console.log("1")< /script>
<script defer> console.log("2")< /script>
<script> console.log("3")< /script>
下面分别讨论defer与async的区别:
defer:
<script src="app1.js" defer> < /script>
<script src="app2.js" defer> < /script>
<script src="app3.js" defer> < /script>
defer 属性表示延迟执行引入 JavaScript,即 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。 整个HTML解析完毕且defer-script也加载完成之后,才会执行所有由defer-script加载的JavaScript代码,再触发 DOMContentLoaded(初始的 HTML 文档被完全加载和解析完成之后触发) 事件。
defer 不会改变 script 中代码的执行顺序,示例代码会按照 1、2、3 的顺序执行。
所以,defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 的解析完成之后。
async
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发(HTML解析完成事件)之后。
从上一段也能推出,多个 async-script 的执行顺序是不确定的,谁先加载完谁执行。
注意:向 document 动态添加 script 标签时,async 属性默认是 true。即:使用 document.createElement 创建的 script 默认是异步的。
console.log(document.createElement("script").async);// true
所以,通过动态添加 script 标签引入 JavaScript 文件默认是不会阻塞页面的。如果想同步执行,需要将 async 属性人为设置为 false。