一个网站在浏览器端是如何进行渲染的呢?
- 根据HTML结构生成
DOM tree
- 根据CSS生成
CSSOM
- 将
DOM
和CSSOM
整合形成RenderTree
- 根据
RenderTree
开始渲染和展示 - 遇到
<script>
时,会执行并阻塞渲染
顺序执行、并发加载
因为解析过程是一个从上到下的过程,所以渲染过程是顺序执行的。而所谓的并发加载指的是当浏览器引入<link>
或<script>
,多个标签的资源可以并发加载。但是并发度是受浏览器自身能力限制的。
对于<img>
所载入的图片,是异步请求的,并不会阻塞页面的渲染,图片的加载速度受其本身大小的影响(图片太大可能在整个页面加载完之后它还没加载出来)。
1 | window.addEventListener('load', function() { |
阻塞
CSS阻塞
- css在
<head>
中阻塞页面的渲染:即这个页面要呈现出效果需要等待这个<link>
所对应的css资源加载完成以后才能进行渲染。如果css并不是在<head>
中引入的话,会出现元素先展示在页面上过一会样式才呈现的情况,所以推荐css在<head>
标签中就引入。 - css阻塞js的执行:即在css资源加载完成之前,后续的js的是无法执行的。由于js文件经常会操作
DOM
元素,而操作过程中可能涉及css样式的修改,它的修改是依赖之前引入的css所具有的样式的,所以css会阻塞js的执行。 - css不阻塞外部脚本的加载:即css资源不会阻塞后续的js资源的加载,但是只能加载不会执行。由于webkit存在
HTMLPreloadScanner
类,是一个预先扫描器,它可以预先扫描后面的词语,在扫描到一些需要加载的资源后,会通过预资源加载器请求后续的资源加载。
JS阻塞
- 直接引入的js阻塞页面的渲染:直接引入指的是没有通过
defer
和async
方法直接用<script>
引入的js资源。如果在标签中指定了defer
方法,这个资源将在页面解析到<script>
的时候就开始下载,但不会执行,直到DOM
加载完成(触发onload
事件前)才会被调用。而async
与defer
的作用是相同的,它们的区别在于sync
的执行是在下载完成之后就开始执行了,所以进行异步加载的脚本最好不要操作DOM
元素。 - js不阻塞资源的加载:与css的加载同理,由于有扫描器的存在,资源会并行加载。
- js顺序执行,阻塞后续js逻辑的运行:即js的执行顺序是和引入的顺序一致。这是由于js的执行是单线程的,所以它会顺序执行并阻塞后续js的执行。
引入方法
脚本的位置
1 | <html> |
当我们在<head>
中引入js文件时,由于js的阻塞特性,当浏览器解析到<script>
标签时,浏览器会停止解析其后的内容,而优先下载脚本文件,并执行其中的代码。虽然后续的资源仍然可以继续加载,却无法渲染,这就造成了打开页面后的空白时间过长,影响用户体验。
1 | <html> |
所以建议把<script>
放在<body>
末尾,因为此时样式和DOM
元素都已经加载并渲染完毕,所以页面的下载就不会显得太慢。
无阻塞脚本
defer
属性:是HTML4为<script>
拓展的属性,指明本元素所含的脚本不会修改DOM
,因此代码能安全地延迟执行。只支持IE4和Firefox 3.5以上版本的浏览器。
1 | <html> |
该段代码执行后的结果是script
、defer
、load
,表明含有defer
属性的脚本是在onload
执行前被调用的,不论它写的位置在哪里。该方法虽好却存在兼容性问题。
async
属性:是HTML5为<script>
拓展的属性,作用和defer
一样,能够异步地加载和执行脚本。它比defer
有更好的兼容性,但由于async
在加载完毕后就会立即执行,所以脚本的执行顺序就可能不是按照html文本的引入顺序,如果两个js前后有依赖关系,就会出现错误。
动态脚本
1 | var script = document.createElement ("script"); |
该方式可以让<script>
无论在什么地方引入,文件的下载和运行都不会阻塞页面的处理过程。但是由于引入的文件的下载和运行和其他DOM
元素是并行的,所以可能出现这个文件中绑定操作的DOM
元素还没加载,因为找不到而报错。
1 | var script = document.createElement ("script"); |
在Firefox、Opera、Chrom和Safari 3+中提供了script.onload
事件,可以监听onload事件来加载js脚本。而对于IE,则是用另一种方式,即readystatechange
事件。
uninitialized
:默认状态loading
:下载开始loaded
:下载完成interactive
:下载完成但尚不可用complete
:所有数据已经准备好
1 | var script = document.createElement("script"); |
虽然以上的动态加载方法可以解决阻塞问题,却还是存在依赖问题,即无法控制两个互相依赖的js文件的先后加载顺序,所以我们可以对这个方法进行封装。
1 | function loadScript(url, callback) { |
如此就可以通过嵌套调用来保证他们的加载顺序:
1 | loadScript("script1.js",function() { |
XMLHttpRequest(XHR)对象
可以利用ajax异步请求的方式,向服务器发送一个获取js文件的请求,在请求成功之后执行动态加载的方法。该方法的优点是可以下载但不立即执行js代码,并且兼容性好。但此方法也有限制:即js文件必须与页面放置在同一个域内(跨域问题),不能从CDN下载,所以大型网站通常不采用XHR脚本注入技术。