引子
在做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之前都执行完毕了
你会发现,这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的执行时机和预期的不太一样,看一下加载时机
也被浏览器优化的提前加载了,也就是说当2.normal1.js执行完成后,3.async2.js
和4.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标签表现类似,不过其加载时机会更直观一些,因为它不会被浏览器优化。
那接着再来看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, theasync
DOM property defaults totrue
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 thedocument.createElement("script").async
evaluates totrue
(such as Firefox 4), setasync="false"
on the scripts you want to maintain order.
也就是说,async="false"
只是可以让create
出来的script
标签按照顺序执行,其加载过程还是异步的
另外,上面这个例子,从另外一个角度说明HTMLonload
事件会等之前开始加载的资源加载完成后才触发
总结
加载形式 | 浏览器优化加载 | 同步 | 有序 | HTML解析后可用 |
---|---|---|---|---|
普通Script 标签 |
✅ | ✅ | ✅ | |
普通Script 标签async |
✅ | ❌ | ❌ | |
普通Script 标签defer |
✅ | ❌ | ✅ | |
createElement | ❌ | ❌ | ❌ | ✅ |
createElement且async为false | ❌ | ❌ | ✅ | ✅ |