script标签的defer和async属性

defer & async

前提

在说defer和async两个script标签的属性之前,要先说一下浏览器对于页面进行加载的流程,浏览器渲染页面是基于两棵树进行的,分别是DOM树和render树,DOM树由HTML决定,HTML元素会被解析为一个个节点,自上而下形成一棵树;而render树是由CSS和DOM树决定的,每个节点在接收了CSS之后,会根据其所占据的空间,形成一棵render树,render树和DOM树是具有映射关系的,但是节点却不一样,比如设置display: none的元素是不具有render树节点的,render树决定的是显示在视口中的元素。

  1. 当输入网址,发出一个请求,并且服务端响应了HTML文件,这时,浏览器会对HTML文件进行解析,来逐步生成DOM树。
  2. 如果浏览器在解析HTML文件的时候遇到了外部资源,则会发送请求,来向服务器请求该外部资源。
  3. 如果资源是script脚本文件,那么DOM树的构建过程会被阻塞,直到脚本加载并且执行完毕,才会继续进行DOM树的构建。
  4. 其他资源会进行异步请求,在构建DOM树的过程中,也会同时进行render树的构建。
  5. render树中的内容,会被布局和绘制到页面中。

那这里面有两个问题,JavaScript会阻塞页面的渲染,是如何阻塞的?CSS是否会阻塞页面的渲染呢?

测试的服务器端

使用NodeJS写一个简单的服务器来进行调试:

    const http = require('http');
    const fs = require('fs');
    const SysPath = require('path');

    const server = http.createServer((req, res) => {
      const path = req.url;
      console.log(`request file is ${path}`);
      let filePath = '';
      if (path === '/') {
        filePath = './index.html';
      } else {
        filePath = SysPath.join('./', path);
      }
      const readStream = fs.createReadStream(filePath);
      readStream.on('error', function(error) {
        res.statusCode = 404;
        res.end('file is not found');
      });
      // 这里可以选择需要延迟返回的文件,现在设置为根目录的html和index.js脚本
      // 两个元素可以直接进行返回
      if (path === '/' || path === '/index.js') {
        res.statusCode = 200;
        readStream.pipe(res);
        return;
      }
      setTimeout(function() {
        res.statusCode = 200;
        readStream.pipe(res);
      }, 5000);
    });

    server.listen('9999', function() {
      console.log(`server is listening in port 9999!!`);
    });

CSS的延迟加载情况

使用上面的服务器,请求HTML,并且将CSS引入的部分放到head标签中:

    <head>
      <link href="./main.css" rel="stylesheet" />
    </head>

可以看到,当样式表被阻塞的时候,页面的渲染也被阻塞,虽然之前div已经有了其他的样式,但是这些样式仍然不能被渲染出来。

css defer

JavaScript阻塞

JavaScript会阻塞页面的渲染大家基本上是都知道的,JavaScript会完全阻塞页面的同步渲染,为了防止这种情况发生,加快首屏显示的速度,标准里面给出了两种异步加载脚本的方法。

其中一个是defer,这个是在HTML5之前就实现了的,这个属性可以让这个外联脚本在另外一个线程中进行请求与加载,然后等待DOM树构建完成,再执行脚本。

而async和defer类似,都是在另外一个线程中进行脚本的加载,但是不同点在于,async会在脚本加载完成之后立刻进行执行。当然,脚本在执行的时候仍会阻塞掉浏览器渲染的主进程。

大概的加载流程就和下面这个图一样:

async-defer

但是async有一个很大的缺点在于,当有多个脚本同时标注为异步脚本,那么这些脚本均会在其被加载完成后直接执行,这样由于某些脚本的加载速度比较快,有些脚本的加载速度慢,会导致脚本的执行顺序和预料当中的不一致,所以有依赖关系的脚本尽量不要使用async进行加载。

而defer脚本由于是在DOMContentLoaded事件触发之后开始执行,所以脚本的执行顺序也是严格按照脚本的引入顺序进行的。

这些延迟都是针对外联脚本的,而内联脚本则不受这个属性的影响,内联脚本会直接执行,并且阻塞页面的渲染进程。

延迟脚本的执行顺序

可以修改一下服务端的代码对执行顺序进行一下验证:

    if (path === '/') {
      res.statusCode = 200;
      readStream.pipe(res);
      return;
    }
    // 对于main.js进行5s的延迟,对于index.js进行2s的延迟
    let delay = 2000;
    if (path === './main.js') {
      delay = 5000;
    }
    setTimeout(function() {
      res.statusCode = 200;
      readStream.pipe(res);
    }, delay);

HTML中先加载延迟较大的main.js,在加载延迟较小的index.js

两个脚本都添加defer,虽然main脚本延迟了5s才加载完成,但是index脚本会等待main脚本完成之后,再进行执行:

defer

两个脚本都添加async,由于index脚本只延迟了2s,在浏览器接收到了index脚本之后,会对其进行立即执行,这样就打乱了正常的脚本执行顺序,可能会导致相互依赖的脚本发生错误:

async

结论

当需要使用延迟加载的脚本的时候,需要考虑这些脚本之间的依赖关系,由于render树布局的原因,所以对于一些需要操作DOM的脚本来说,如果对其进行延迟加载,那么在页面渲染完毕之后,再进行一些会影响布局的DOM操作,这样会造成页面的reflow或者repaint,会有非常不好的影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值