浏览器渲染HTML全过程详解,包括preload,prefetch,defer,async用法

1,088 阅读5分钟

HTML解析过程

  1. 解析HTML文档,构建DOM(Document Object Model)树
  2. 假如遇到link标签(没有其他特殊的属性),异步下载对应的样式文件,下载完毕开始构建CSSOM树,此过程并不会阻塞DOM树的构建(但会影响渲染树,即所谓的阻塞渲染)
  3. 假如遇到script标签(没有其他特殊的属性),阻塞DOM的构建,并在下载完成后直接执行JS代码
  4. DOM树构建完成,DOMContentLoaded事件触发
  5. 结合DOM树和CSSOM树,形成渲染树
  6. 计算布局和绘制元素,呈现页面,window load事件触发

验证解析过程

所需工具

  1. chrome浏览器
  2. 一个js和css文件cdn链接

场景1: 验证遇到Link标签,会异步下载css文件而不阻塞DOM树的构建,代码如下

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    document.addEventListener("DOMContentLoaded",()=>{
     console.log("DOMContentLoaded")
   })
   window.addEventListener("load",()=>{
     console.log("load")
   })
 </script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"   /> 
</head>
<body>
  <div class="test">
    This is the content of the test div
  </div>
  
</body>
</html>

我们在进入页面时,监听了页面的DOMContentLoad和load事件,同时引入一个css文件,接着,我们打开该网页,在chrome浏览器中将网速调到1kb/s。清空缓存刷新网页。

1680074781152.png

1680074771174.jpg

可以看到当浏览器在下载css文件的过程中,DOM树已经构建完成了,因此CSS文件的下载并不会阻塞DOM树的构建。

场景2:script标签会阻塞DOM树的构建

和场景1同样的代码,但将引入的css文件换成js文件,如

  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js" ></script>

同样的,调低网速,清空缓存刷新网页。

1680075244468.jpg

1680075257105.jpg

可以看到,在js下载过程中,DOMContentLoaded事件一直没被触发。此时取消网络限制,让浏览器下载完JS文件,可以看到控制台打印信息

image.png

此时可以看到DOMContentLoaded事件和load事件都已经触发了,证明了script标签的下载和执行会阻塞DOM树的构建。

css文件下载同样会影响网页的呈现

在场景1的例子截图中,我们可以看到在下载css文件的过程中,虽然DOMContetLoaded事件触发了,但一直没有触发load事件,且页面是空白的。这是因为最终页面的呈现是由渲染树决定的,而渲染树又由DOM树和CSSOM树共同决定,因此在CSSOM树构建完成之前,页面就可能出现留白的现象。

为何推荐将css样式放在head中,而将js代码写在body之前

浏览器在渲染页面过程中,会尽可能先呈现已解析内容,由上面可知,CSSOM树和DOM树的构建是异步的,因此,将样式代码放在head标签中,可以让浏览器更早地构建CSSOM树和DOM树,尽早的呈现完整的页面。

但由于JS代码会阻塞DOM树的构建,且会被立刻执行,将其放在后面才能避免操作不到页面最终需要的东西,如:

image.png

为何说DOMContentLoaded事件执行时,还可能存在一些js和css未被加载

  1. 存在css未被加载情况从场景1很容易看出
  2. 本文前面说script标签会阻塞DOM树的构建,按道理只有页面上的所有脚本都执行完之后,DOMContentLoaded事件才可能完成,但这只限于未给script标签加其他属性的情况,假如给script加上async属性,情况又有不同。

preload,prefetch,defer,async

defer,async

这两个属性都是script标签上的属性,其中

  1. async 表示脚本会被并行请求,并尽快解析和执行。因此假如scirpt的下载速度很慢,且有async属性,则在DOMContentLoaded时候,就可能存在还未执行的js文件。如

image.png

image.png

image.png

可见,此时DOMCotentLoaded已经触发了,但async的script资源尚未下载完。

  1. defer 表示通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。如

image.png

image.png

可见资源还未下载完时,会阻塞DOMContentLoaded事件

preload,prefetch同样会让资源下载变成异步

preload和prefetch都是优化浏览器加载资源的手段,它们都是link标签上的属性,与之相关的还有一个as属性,可以用来表示连接的文档内容,as属性仅在rel属性设置了preload或prefetch有效。

  1. preload指示某资源为重要资源,指示浏览器在加载当前页面时必须立即下载资源,它只会指示该资源下载和缓存的优先级而不会加载该js文件,如
<link rel="preload" href="style.css" as="style"> //它的`as`属性指定了资源类型,以便浏览器可以优化加载。

在使用的时候,还是需要再引入一次

<link rel="stylesheet" href="style.css"> 

让我们用animate.css来验证这点,看下面例子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    document.addEventListener("DOMContentLoaded",()=>{
     console.log("DOMContentLoaded")
   })
   window.addEventListener("load",()=>{
     console.log("load")
   })
 </script>
 <link
  rel="preload"
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
  as="style"
/>
</head>
<body>
  <h1 class="animate__animated animate__bounce">An animated element</h1>
</body>
</html>

假设preload只是让浏览器提前下载该文件而不加载,h1元素在页面加载完的时候就不会抖动。让我们看结果:

image.png

确实浏览器下载了该文件,且h1元素没有抖动。

接下来我们在使用普通的link引入animate css,如

 <link
  rel="preload"
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
  as="style"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>

此时清空缓存再次刷新页面。 可以看到浏览器下载了animate.css并且h1元素抖动。

  1. prefetch用于指示浏览器在将来可能需要使用的资源。如
<link rel="prefetch" href="image.png">

这告诉浏览器,在当前页面加载完成后,可以预取image.png文件,因为它可能在用户进行后续导航时需要使用。这可以减少页面之间的加载时间。