家好,我是前端西瓜哥。今天我們來了解一下 script 腳本的三種加載方式。
一般的 script 寫法為:
<script src="app.js"></script>
這種寫法有一個問題:它會 阻塞 HTML 的 DOM 構建。
假如我們在 head 元素中使用了 script 腳本,它就會阻止后面元素的渲染,包括 body 元素,此時執行document.querySeletor('body') 拿到的是 null。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
// 拿到 null
console.log(document.querySeletor('body'));
</script>
</head>
<body></body>
</html>
此外,當腳本足夠大,加載執行足夠久時,會導致頁面長時間沒能渲染出完整頁面。
這也是我們將業務代碼腳本放到 body 最下邊的原因,這樣能確保腳本能夠訪問一個完整的 DOM 樹,也不會阻止頁面的渲染。
缺點是,HTML 很長的時候,解析到腳本就會花上一點時間,然后才會請求對應的腳本資源。
不過通常來說,HTML 內容都比較簡單,二者感受不到太大區別,除非你網很卡。
<script defer src="app.js"></script>
defer,“延遲” 之意。這里的延遲,指的是延遲執行腳本,下載則不會被阻塞。
需要注意的是, defer 屬性對內嵌腳本無效。畢竟腳本內容就在 HTML 里了,完全不需要請求資源了好吧。
給 script 標簽添加了 defer 屬性后,腳本不會阻塞 DOM 樹的構建,會先下載資源,然后等待到在 DOMContentLoaded 事件前執行。
DOMContentLoaded 事件的觸發時機為初始 HTML 被構建完成時,此時 CSS、圖片等資源不需要加載完,但我們的腳本要執行完。
如果多個 script 設置了 defer 屬性,這幾個 script 的執行順序和聲明順序相同,即最前面的腳本先執行。并不是誰先下載誰先執行。
實際開發中,我們可以將業務代碼腳本加上 defer 屬性,放到更上層的 head 標簽下。
這也是最新版 HtmlWebpackPlugin 插件的默認引入打包腳本的方式。
<script async src="app.js"></script>
async,“異步” 之意。同樣對內嵌腳本無效。
設置 async 后,腳本一旦被下載好了就會執行,不管什么時機。
適合與執行順序無關的腳本,比如廣告、網站流量分析腳本。
比如插入 Google 分析腳本:
<script async src="//www.google-analytics.com/analytics.js"></script>
還有一種用腳本加載腳本的特殊情況,這里也說一說。
<script>
const script = document.createElement('script');
script.src = 'app-a.js';
document.body.appendChild(script);
</script>
腳本里創建一個 script 元素,設置好 src,然后加載到 DOM 樹上,接著腳本就會下載和執行了。
創建的 script 元素默認會給 async 設置為 true,即一旦下載好就立即執行。
如果你要加載有依賴關系的多個腳本,就需要將 async 設置為 false。
<script>
const script = document.createElement('script');
// 取消 async 加載方式
script.async = false;
script.src = 'app-a.js';
document.body.appendChild(script);
const script2 = document.createElement('script');
script2.async = false;
script2.src = 'app-b.js';
document.body.appendChild(script2);
</script>
<script>console.log('我還是所有腳本中最先執行的')</script>
這樣寫,就能保證先執行 app-a.js,再執行 app-b.js
但 它無法做到比 HTML 中其他非動態加載的 script 腳本更早執行,這點需要注意。
script 有三種常見加載模式:
此外還有動態加載的腳本的情況,這種腳本默認為 async 加載形式,可通過將 async 屬性設置為 false 來解除,讓腳本順序執行。
我是前端西瓜哥,歡迎關注我。
前端代碼離不開瀏覽器環境,理解 js、css 代碼如何在瀏覽器中工作是非常重要的。
如何優化渲染過程中的回流,重繪?script 腳本在頁面中是怎么個加載順序?了解這些對前端性能優化起著非常大的作用。
借著這篇文章,讓自己對這塊知識的理解更深一步。
瀏覽器通過解析 HTML 和 CSS 后,形成對應的 DOM 樹和 CSSOM 樹。
從根節點開始解析 DOM 樹節點并匹配對應的 CSSOM 樣式規則,選擇可見的的節點,最終結合成一顆渲染樹。
從上圖能看到渲染樹的特點:
根據上圖,整個渲染階段分為三部分:
兩者的關系:觸發回流一定會觸發重繪, 而觸發重繪卻不一定會觸發回流
下圖很形象的展示了 Mozilla 頁面的渲染過程。
都知道頻繁的渲染過程會影響網頁性能,但怎么知道網頁開始渲染內容了呢?
我們可以通過 Chrome 的 F12,選擇 Rendering 來查看網頁的性能。
結合上面的方法,用 一個簡單的 Demo 來示意:
能從圖中看到,這些操作 觸發了瀏覽器的重繪:
布局/回流 和 繪制/重繪 是頁面渲染必須會經過的兩個過程,不斷觸發它們肯定會增加性能的消耗。
瀏覽器會對這些操作做優化(把它們放到一個隊列,批量進行操作),但如果我們調用上面提到的 offsetXX, clientXX,scrollXX,getClientRects 等屬性方法就會強制刷新這個隊列,導致這些隊列批量優化無效。
下面列舉一些簡單優化方式:
注:均放在 head 標簽內。
考個問題:CSS 定義在 head 中,其需加載 5 秒,請問頁面加載后內容會先優先展示嗎?
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 延遲5秒 -->
<link rel="stylesheet" href="/css/demo.css?t=5000" />
</head>
<body>
<div class="layout">我被渲染出來了</div>
</body>
</html>
我原先以為頁面內容會優先渲染,CSS 加載完成后才改變內容樣式。其實這是錯的。
從上圖看到,頁面加載后,body 內元素就已經解析好了,只是沒有渲染到頁面上。隨后 CSS 文件加載后,帶有樣色的內容才被渲染到頁面上。
延遲的 link 的加載阻斷了頁面渲染,但并沒有影響 HTML 的解析,當 CSS 加載后,DOM 完成解析,CSSOM 和 DOM 形成渲染樹,最后將內容渲染到頁面上。
反問,將 link 替換成 script 效果也一樣嗎?
與 link 不同,script 的加載會阻斷頁面 HTML 的解析,瀏覽器解析完 script 后,會等待 js 文件加載完后,頁面才開始后續的解析,body 內容才出現。
學前端時相信都聽過這樣的名言:
CSS 寫在 head 里,js 寫在 body 結束標簽前
知道了上面 link 和 script 的區別后,應該明白前半句的含義,下面來解釋下后半句。
下面 script 均在 body 中。
先看下腳本在 body 中的一般情況:
在 body 內部的首位分別加載兩個 js 文件,前者延遲 3 秒,后者延遲 5 秒,為了清楚他們的“工作”情況,在 head 中添加了定時器示意。
<html lang="en">
<head>
<script>
var t = 0;
var timer = setInterval(function () {
t++;
console.log('已加載 ', t, ' 秒');
if (t == 10) {
clearInterval(timer);
}
}, 1 * 1000);
</script>
</head>
<body>
<script>
var foo = 0;
console.log('init foo', foo);
</script>
<script src="/js/addTen.js?t=3000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=5000"></script>
</body>
</html>
能看到 body 中定義的內聯腳本首先工作,初始化 foo 變量。
隨后加載 addTen.js,并阻斷頁面渲染。3 秒后,輸出 js 內容(foo 賦值為 10),頁面并重新開始解析,展示 div 內容。
最后加載 addOne.js ,繼續等待 2 秒后,輸出 js 內容(foo 賦值為 11)。
如果前一個 js 文件加載慢于后一個,會有怎么個效果?
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
兩個 script 標簽并行加載,1 秒后 addOne.js 首先加載完畢,等待 4s 秒后,addTen.js 加載完后,頁面直接渲染(因為 script 已經全部完成)。
所以建議 script 放在 body 結束標簽之前,確保頁面內容全部解析完成并開始渲染。
DOMContentLoaded 事件可以來確定整個 DOM 是否全部加載完成,下面我們簡單測試下:
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('[ready] document');
});
</script>
<!-- ... -->
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
最終輸出:
addTen.js
foo 10
addOne.js
foo 11
[ready] document
DOMContentLoaded 事件的定義是異步回調方式,當 DOM 加載完成后觸發,即使寫在最前面,也會等待后面的 script 加載完成后才觸發。
這里順便提個 window.onload :
window.onload 和 DOMContentLoaded 不同,前者會等待頁面中所有的資源加載完畢后再調用執行(比如:img 標簽),后者在 DOM 加載完畢后即觸發。
能看到無論 script 放在那個位置,瀏覽器都會等待他們直至 body 內的文件全部加載完。
那有什么 真正的異步 腳本加載嗎?(不會阻斷頁面解析)
那就是 動態腳本。
如果你接觸過第三方網頁統計腳本,那將比較了解,下面給段示例代碼:
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('[ready] document');
});
</script>
<script>
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = '/js/dynamicScript.js?t=8000';
document.getElementsByTagName('head')[0].appendChild(newScript);
// 腳本加載完畢
if (newScript.readyState) {
newScript.onreadystatechange = function () {
if (newScript.readyState == 'loaded' || newScript.readyState == 'complete') {
console.log('dynamicScript.js loaded');
}
};
} else {
newScript.onload = function () {
console.log('dynamicScript.js loaded');
};
}
</script>
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
最終輸出:
addTen.js
afoo 10
addOne.js
foo 11
[ready] document
已加載 5 秒
已加載 6 秒
已加載 7 秒
已加載 8 秒
dynamicScript.js is running
dynamicScript.js loaded
已加載 9 秒
已加載 10 秒
定義了需要加載 8 秒的 dynamicScript.js 文件,所有的 script 加載方式依舊異步,但 dynamicScript.js 在 DOMContentLoaded 觸發后,最后才執行,瀏覽器并沒有等待它的加載完成后才渲染頁面。
我們也可以將它放在 head 中。這種通過腳本來動態修改 DOM 結構的加載方式是 無阻塞式 的,不受其他腳本加載的影響。
我們可以在 script 定義 defer 、 async ,使整個腳本加載方式更加友好。比如:被修飾的腳本在 head 中,將不會阻斷 body 內容的展示。
注意: defer 修飾的腳本將延遲到 body 中所有定義的腳本之后,DOM(頁面內容)加載完之前觸發; async 不會像 defer 一樣等待 body 中的腳本,而是當前腳本一加載完畢就觸發。
<head>
<!-- 如果上面沒有其他響應慢的腳本,解析到此處加載完后將立馬執行 -->
<script async src="/js/scriptAsync.js?t=3000"></script>
<!-- 1 秒,延遲到 DOM 加載完畢 -->
<script defer src="/js/scriptDefer.js?t=1000"></script>
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('[ready] document');
});
</script>
<script>
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = '/js/dynamicScript.js?t=8000';
document.getElementsByTagName('head')[0].appendChild(newScript);
// 腳本加載完畢
if (newScript.readyState) {
newScript.onreadystatechange = function () {
if (newScript.readyState == 'loaded' || newScript.readyState == 'complete') {
console.log('dynamicScript.js loaded');
}
};
} else {
newScript.onload = function () {
console.log('dynamicScript.js loaded');
};
}
</script>
<script>
console.log('init foo', 0);
var foo = 0;
</script>
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
</body>
加載順序:
已加載 1 秒
已加載 2 秒
scriptAsync.js
已加載 3 秒
已加載 4 秒
addTen.js
foo 10
addOne.js
foo 11
scriptDefer.js
[ready] document
已加載 5 秒
已加載 6 秒
已加載 7 秒
已加載 8 秒
dynamicScript.js is running
dynamicScript.js loaded
已加載 9 秒
已加載 10 秒
本文使用 mdnice 排版
么是JS延遲加載?
JS延遲加載,也就是等頁面加載完成之后再加載JavaScript文件
為什么讓JS實現延遲加載?
js的延遲加載有助于提高頁面的加載速度。
Js延遲加載的方式有哪些?一般有以下幾種方式:
·defer屬性
·async屬性
·動態創建DOM方式
·使用jQuery的getScript方法
·使用setTimeout延遲方法
·讓JS最后加載
HTML 4.01為<script>標簽定義了defer屬性。標簽定義了defer屬性元素中設置defer屬性,等于告訴瀏覽器立即下載,但延遲執行標簽定義了defer屬性。
用途:表明腳本在執行時不會影響頁面的構造。也就是說,腳本會被延遲到整個頁面都解析完畢之后再執行在<script>元素中設置defer屬性,等于告訴瀏覽器立即下載,但延遲執行
<!DOCTYPE html>
<html>
<head>
<script src="test1.js" defer="defer"></script>
<script src="test2.js" defer="defer"></script>
</head>
<body>
<!--這里放內容-->
</body>
</html>
說明:雖然<script>元素放在了<head>元素中,但包含的腳本將延遲瀏覽器遇到</html>標簽后再執行HTML5規范要求腳本按照它們出現的先后順序執行。在現實當中,延遲腳本并不一定會按照順序執行defer屬性只適用于外部腳本文件。支持HTML5的實現會忽略嵌入腳本設置的defer屬性
HTML5 為<script>標簽定義了async屬性。與defer屬性類似,都用于改變處理腳本的行為。同樣,只適用于外部腳本文件。標簽定義了async屬性。與defer屬性類似,都用于改變處理腳本的行為。同樣,只適用于外部腳本文件。
目的:不讓頁面等待腳本下載和執行,從而異步加載頁面其他內容。異步腳本一定會在頁面 load 事件前執行。不能保證腳本會按順序執行
<!DOCTYPE html>
<html>
<head>
<script src="test1.js" async></script>
<script src="test2.js" async></script>
</head>
<body>
<!--這里放內容-->
</body>
</html>
async和defer一樣,都不會阻塞其他資源下載,所以不會影響頁面的加載。
缺點:不能控制加載的順序
//這些代碼應被放置在</ body>標簽前(接近HTML文件底部)
<script type="text/javascript">
function downloadJSAtOnload() {
varelement = document .createElement("script");
element.src = "defer.js";
document.body.appendChild(element);
}
if (window. addEventListener)
window.addEventListener("load" ,downloadJSAtOnload, false);
else if (window.attachEvent)
window.attachEvent("onload", downloadJSAtOnload) ;
else
window. onload =downloadJSAtOnload;
</script>
$.getScript("outer.js" , function(){ //回調函數,成功獲取文件后執行的函數
console.log(“腳本加載完成")
});
<script type="text/javascript" >
function A(){
$.post("/1ord/1ogin" ,{name:username,pwd:password},function(){
alert("Hello");
});
}
$(function (){
setTimeout('A()', 1000); //延遲1秒
})
</script>
把js外部引入的文件放到頁面底部,來讓js最后引入,從而加快頁面加載速度例如引入外部js腳本文件時,如果放入html的head中,則頁面加載前該js腳本就會被加載入頁面,而放入body中,則會按照頁面從上倒下的加載順序來運行JavaScript的代碼。所以我們可以把js外部引入的文件放到頁面底部,來讓js最后引入,從而加快頁面加載速度。
上述方法2也會偶爾讓你收到Google頁面速度測試工具的“延遲加載javascript”警告。所以這里的解決方案將是來自Google幫助頁面的推薦方案。
//這些代碼應被放置在</body>標簽前(接近HTML文件底部)
<script type= "text/javascript">
function downloadJSAtonload() {
var element = document.createElement("script");
element.src = "defer.js";
document.body.appendChild(element);
}
if (window.addEventListener)
window.addEventListener("load", downloadJSAtOnload, false);
else if (window.attachEvent )
window.attachEvent("onload", downloadJSAtonload);
else window.onload = downloadJSAtOnload;
</script>
這段代碼意思等到整個文檔加載完后,再加載外部文件“defer.js”。
使用此段代碼的步驟:
6.1)復制上面代碼
6.2)粘貼代碼到HTML的標簽前 (靠近HTML文件底部)
6.3)修改“defer.js”為你的外部JS文件名
6.4)確保文件路徑是正確的。例如:如果你僅輸入“defer.js”,那么“defer.js”文件一定與HTML文件在同一文件夾下。
注意:
這段代碼直到文檔加載完才會加載指定的外部js文件。因此,不應該把那些頁面正常加載需要依賴的javascript代碼放在這里。而應該將JavaScript代碼分成兩組。一組是因頁面需要而立即加載的javascript代碼,另外一組是在頁面加載后進行操作的javascript代碼(例如添加click事件。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。