Script标签加载与执行时机

引子

在做Web端的性能优化时,基本上都会涉及到HTML的加载解析过程,其中如何控制Javascript的加载和执行时机尤为重要。

这里涉及到多种Javascript的加载方法,有同步的、异步的,也有动态添加的,他们分别在什么时候开始加载,又会在什么时候开始执行。

另外在研究web优化时,也会有两个重要的事件:

  • DOMContentLoaded:HTML解析完毕
  • load:资源加载完毕

这里增加了两个事件的监听,方便查看执行时机

document.addEventListener('DOMContentLoaded', (event) => {
  console.log('DOMContentLoaded');
});
window.addEventListener('load', (event) => {
  console.log('loaded');
});

复制代码

同步加载

简单说明一下同步加载的概念,主要含义是其会阻塞HTML的继续解析。我们知道HTML解析过程是从上到下解析的,而遇到需要同步加载的script标签的时候,会等待script加载完成,并执行完成后,在继续解析HTML。

案例

<script src="/1.normal1.js?wait=1000"></script>
<script src="/2.normal2.js?wait=500"></script>
<script src="/3.normal3.js?wait=500"></script>

复制代码

执行结果

1.normal1.js wait: 1000
2.normal2.js wait: 500
3.normal3.js wait: 500
DOMContentLoaded
loaded

复制代码

按照顺序执行,并且在DOMContentLoaded之前都执行完毕了

3de0b9f9f2a749c888f7a9cb5f4e79e3_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp 你会发现,这3个脚本的加载时机竟然是一样,都是在HTML获取到之后,马上就开始加载。这里其实涉及到浏览器的一个优化,他会预先找到HTML中的所有script标签,并直接开始加载,并不是在解析到该标签时才开始加载。这样算是浏览器一种优化,等HTML解析到对应标签后,JS已经加载好了,直接执行即可,这样可以节省大量的时间,提升用户体验。当然,从上面的执行结果看,其执行实际还是保持原有逻辑的。

async

script标签也支持async和defer属性来异步加载,即不阻塞HTML的解析。

对于我们现在要研究的,简单来说就是异步加载,加载完了就立即执行。

示例

<script src="/1.async1.js?wait=3000" async></script>
<script src="/2.normal1.js?wait=500"></script>
<script src="/3.async2.js?wait=100" async></script>
<script src="/4.normal2.js?wait=400"></script>

复制代码

执行结果

2.normal1.js wait: 500
3.async2.js wait: 100
4.normal2.js wait: 400
DOMContentLoaded
1.async1.js wait: 3000
loaded
复制代码

这里的3.async2.js的执行时机和预期的不太一样,看一下加载时机

065c508bf00841f9a3b725ad7960d180_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp 也被浏览器优化的提前加载了,也就是说当2.normal1.js执行完成后,3.async2.js4.normal2.js都已经加载完成了,这里表现出同步脚本执行优先级似乎比异步的高(并不确定)。

defer

script标签还支持defer来实现异步加载

大体也就是异步加载,等到HTML解析完成,但是在DOMContentLoaded事件前,按照顺序执行

示例

<script src="/1.defer1.js?wait=3000" defer></script>
<script src="/2.normal1.js?wait=500"></script>
<script src="/3.defer2.js?wait=100" defer></script>
<script src="/4.normal2.js?wait=400"></script>

复制代码

执行结果为

2.normal1.js wait: 500
4.normal2.js wait: 400
1.defer1.js wait: 3000
3.defer2.js wait: 100
DOMContentLoaded
loaded

复制代码

符合预期,另外,js加载时机也被浏览器优化成一开始就全部加载了。

结合下async来看下

<script src="/1.normal1.js?wait=400"></script>
<script src="/2.defer1.js?wait=200" defer></script>
<script src="/3.async1.js?wait=600" async></script>
<script src="/4.defer2.js?wait=700" defer></script>
<script src="/5.normal2.js?wait=500"></script>

复制代码

其结果为

1.normal1.js wait: 400
5.normal2.js wait: 500
2.defer1.js wait: 200
3.async1.js wait: 600
4.defer2.js wait: 700
DOMContentLoaded
loaded

复制代码

createElement

还有一种比较常见的动态加载script的方法,就是使用createElement动态创建script标签,然后在添加到HTML中,为了做简单测试,这边在HTML也简单加了一个方法。

window.loadScript = function(url, async) {
  var po = document.createElement('script');
  po.async = async;
  po.src = url;
  document.body.appendChild(po);
};

复制代码

其中async属性默认为true,为了方便测试,把这个参数也传入了

<script>loadScript('/1.create1.js?wait=3000', true);</script>
<script src="/2.normal1.js?wait=500"></script>
<script>loadScript('/3.create2.js?wait=100', true);</script>
<script src="/4.normal2.js?wait=700"></script>

复制代码

看下执行结果

2.normal1.js wait: 500
3.create2.js wait: 100
4.normal2.js wait: 700
DOMContentLoaded
1.create1.js wait: 3000
loaded

复制代码

其和直接写带async的script标签表现类似,不过其加载时机会更直观一些,因为它不会被浏览器优化。

7475533864cd45a68a35660e206f0615_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp

那接着再来看create出来的script标签的async属性,看名字和之前普通script标签一样,那是不是这个属性设置为false就可以实现动态创建同步脚本了呢。

<script>loadScript('/1.create1.js?wait=3000', false);</script>
<script src="/2.normal1.js?wait=500"></script>
<script>loadScript('/3.create2.js?wait=100', false);</script>
<script src="/4.normal2.js?wait=700"></script>
<script>
  setTimeout(function() {
    loadScript('/5.create3.js?wait=3000', false);
  }, 1000);
</script>

复制代码

执行结果

2.normal1.js wait: 500
4.normal2.js wait: 700
DOMContentLoaded
1.create1.js wait: 3000
3.create2.js wait: 100
5.create3.js wait: 3000
loaded

复制代码

都在DOMContentLoaded后才执行,没有阻断HTML解析。

In older browsers that don't support the async attribute, parser-inserted scripts block the parser; script-inserted scripts execute asynchronously in IE and WebKit, but synchronously in Opera and pre-4 Firefox. In Firefox 4, the async DOM property defaults to true for script-created scripts, so the default behavior matches the behavior of IE and WebKit. To request script-inserted external scripts be executed in the insertion order in browsers where the document.createElement("script").async evaluates to true (such as Firefox 4), set async="false" on the scripts you want to maintain order.

也就是说,async="false"只是可以让create出来的script标签按照顺序执行,其加载过程还是异步的

另外,上面这个例子,从另外一个角度说明HTMLonload事件会等之前开始加载的资源加载完成后才触发

总结

加载形式 浏览器优化加载 同步 有序 HTML解析后可用
普通Script标签
普通Script标签async
普通Script标签defer
createElement
createElement且async为false

猜你喜欢

转载自juejin.im/post/7075107761977032735
今日推荐