什么是阻塞?
我的理解:当外部引入的js文件或者css文件一直没有下载成功,导致页面DOM没有渲染出来时,就形成了页面阻塞。这显然对用户体验很差。
怎么才能不阻塞?
从阻塞的形成,我们就知道造成阻塞可能有以下原因:
- js,css文件较多,较大。下载时间长。
- js 文件在DOM文档结构之前,js一直在下载或执行中。
所以针对以上可能的原因,我们发现根本在于JS加载执行时间和DOM渲染时间的冲突。那么,只要保证JS加载执行在DOM加载后,是不是就能保证是无阻塞脚本了呢。从技术的角度讲,就是在window的load事件触发之后再下载文件。
对于CSS,如果CSS比较少,可以采取内联的方式放在HTML<style></style>里面,可以看到,webpack在打包时就是引入相关的CSS内联到页面中。
对于JS,有以下几种方式:
- <script>标签放在<body>里面,但是DOM结构之后,这样就能保证DOM加载完再加载JS文件,或者script标签里的JS代码。
- HTML4 script标签有一个defer属性,意思是延迟。jquery.min.js会异步加载,不影响DOM的加载,只是会等到DOM加载完成后再执行。
<script src="jquery.min.js" defer></script>
注意:HTML5新增了一个属性async,它和defer的区别在于,在异步加载JS之后会自动执行,执行的过程可能会阻塞DOM。
- 动态脚本插入
<script type="text/javascript">
function loadScript(url, callback){
var script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState) { //IE
script.onreadystatechange = function(){
if (script.readyState === 'loaded' || script.readyState === "complete") {
script.onreadystatechange = null;
callback();
}
}
}else{
script.onload = function(){
callback();
}
}
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
loadScript("https://www.greatytc.com/analytics.js", function(){
//js逻辑
alert('loaded');
})
</script>
IE 对这两个 readyState 值所表示的最终状态并不一致,有时<script>元素会得到“loaded”却从不出现“complete”,但另外 一些情况下出现“complete”而用不到“loaded”。最安全的办法就是在 readystatechange 事件中检查这两种状 态,并且当其中一种状态出现时,删除 readystatechange 事件句柄(保证事件不会被处理两次)。
通过这种方法,无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。你甚至可以将这些代码放在 <head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)。同时,这种方法可以跨域加载,所以比较常用。
- XMLHttpRequest动态脚本注入
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open('get', 'file.js', true);
xhr.onreadystatechange = function(){
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = xhr.responseText;
console.log(script.text);
document.body.appendChild(script);
}
}
}
xhr.send(null);
执行之后如下:
但这种无法跨域下载,所以运用较少。
建议的无阻塞模式
通过以上的分析,权衡利弊之后,应该是通过loadScript方式,会更好一些。将页面初始化所需的JS单独加载之后,再通过动态加载的方式加载其他不需要立即执行的JS代码。 例如:
<script type="text/javascript" src="loader.js"></script> //初始化页面时需要的JS代码
<script type="text/javascript">
loadScript("the-rest.js", function(){
Application.init();
});
</script>
另外,再结合第一种将script放在</body>之前,DOM之后。这样当第二部分 JavaScript 文件完成下载,所有应用程序所必须的 DOM 已经创建好了,并做好被访问的准备,避免使用额外的事件处理(例如 window.onload) 来得知页面是否已经准备好了。
对于这种方式,有完整的开源实现,只需要引用就行了: