優化腳本文件的加載提升頁面的加載速度,一直是前端工程師提升頁面加載速度很重要的一條。由於涉javascript
及到各個瀏覽器對解析腳本文件的不一樣機制,以及加載腳本會阻塞其餘資源和文件的加載。當瀏覽器解析器遇到<script>時,會當即加載(加載:下載,解析和執行),瀏覽器對其餘資源和文檔的加載會中止。爲了提升頁面的加載速度,得讓JS不阻塞其餘資源的加載。css
Webkit 和 Firefox 對JS的執行過程進行了優化,增長了「預解析」這個過程,「預解析」過程不會修改DOM樹,因此能夠跟其餘解析過程並行,該過程由預解析器去完成,而可能會改變DOM樹執行過程則由主解析器來完成,在經過解析過程瞭解JavaScript文章中有提到的JS的「預解析」過程,此過程應該就是由瀏覽器的預解析器完成,預解析器還負責解析樣式表和圖片。html
另外一方面,瀏覽器同事請求http的數量也是有必定限制的,加載js不像加載樣式那樣是並行的。樣式表是構建呈現樹的一部分,瀏覽器在解析頁面結構是由DOM樹和呈現樹兩部分組成,而解析執行樣式表只會改變樣式表不會更改DOM樹,呈現樹跟DOM樹雖然是相對應的,但並不是一一對應。所以,也就沒有必要中止對其餘資源和文檔的加載了。前端
提升頁面加載速度的最簡單快速的方法就是將腳本文件放到body底部。但這並非提升頁面加載速度最優方案的方案,接下來咱們介紹其餘方案。html5
首先來介紹一下<script>時能讓腳本延遲和異步執行的兩個屬性:defer和async。java
defer是html4.0中定義的,該屬性使得瀏覽器能延遲腳本的執行,等文檔完成解析完成後會按照他們在文檔出現順序再去下載解析。也就是說defer屬性的<script>就相似於將<script>放在body的效果。ajax
async是HTML5新增的屬性,IE10和瀏覽器都是支持該屬性的。該屬性的做用是讓腳本能異步加載,也就是說當瀏覽器遇到async屬性的<script>時瀏覽器加載css同樣是異步加載的。跨域
支持async屬性的瀏覽器貌似沒什麼問題,可是defer屬性在各個瀏覽器中支持程度有點不一樣。測試代碼以下瀏覽器
<script type="text/javascript" defer> alert('defer')</script><script type="text/javascript"> alert('script')</script><script type="text/javascript"> window.onload = function(){ alert('onload') }</script>defer測試代碼,可將代碼複製到本地本身測試,外部腳本src引入,內聯腳本直接粘帖
運行以上代碼,得出如下結論:網絡
外部JS在各個瀏覽器裏運行結果跟定義的執行順序正常,alert信息會按照 script->defer->onload順序彈出;
內聯腳本,若是腳本都是IE9/8/7/6按照定義的順序彈出信息,其餘瀏覽器則按照 defer->script->onload 順序彈出信息,表示defer失效。
而若是有多個內聯defer腳本、在body和head都有分佈或者在iframe中也有內聯defer腳本,則在IE6中表現一致。
若是想給腳本增長defer屬性讓其延遲加載的話,最好是外部腳本,內聯的defer不只多數瀏覽器不支持,並且IE6的表現也不一致。
因此將腳本放在body底部比給腳本增長defer屬性讓腳本延遲加載更好,就像yslow建議的那樣:put style top,put script bottom。
瀏覽器的在遇到defer和async屬性的<script>的瀏覽器執行過程以下(如下摘自javascript權威指南):
WEB瀏覽器建立Document對象,而且開始解析WEB頁面,解析HTML元素和它們的文本內容後添加Element對象和Text節點到文檔中。這個過程的readystate的屬性值是「loading」
當HTML解析器遇到沒有async和defer屬性的<script>時,它把這些元素添加到文檔中,而後執行行內或外部腳本。這些腳本會同步執行,而且在腳本下載(若是須要)和執行解析器會暫停。這樣腳本就能夠用document.write()來把文本插入到輸入流中。解析器恢復時這些文本會成爲文檔的一部分。同步腳本常常單定義函數和註冊後面使用的註冊事件處理程序,但它們能夠遍歷和操做文檔樹,由於在它們執行時已經存在了。這樣同步腳本能夠看到他本身的<script>元素和它們以前的文檔內容
當解析器遇到了設置async屬性的<script>元素時,它開始下載腳本,並繼續解析文檔。腳本會在它下載完成後儘快執行,可是解析器沒有停下來等他下載。異步腳本禁止document.write()方法。它們能夠看到本身的<script>元素和它以前的全部文檔元素,而且可能或乾脆不可能訪問其餘的文檔內容。
當文檔完成解析,document.readyState屬性變成「interactive」。
全部有defer屬性的腳本,會被它們在文檔的裏的出現順序執行。異步腳本可能也會在這個時間執行。延遲腳本能訪問完整的文檔樹,禁止使用document.write()方法。
瀏覽器在Document對象上觸發DOMContentLoaded事件。這標誌着程序執行從同步腳本執行階段轉到異步事件驅動階段。但要注意,這時可能還有異步腳本沒有執行完成。
這時,文檔已經徹底解析完成,可是瀏覽器可能還在等待其餘內容載入,如圖片。當全部這些內容完成載入時,而且全部異步腳本完成載入和執行,document.readyState屬性變爲「complete」,WEB瀏覽器出發Window對象上的load事件。
今後刻起,會調用異步事件,以異步響應用戶輸入事件,網絡事件,計算器過時等。
瞭解瀏覽器在遇到async、defer屬性的腳本執行順序,咱們能夠利用這兩個屬性來改善JS的阻塞問題,使用這兩個屬性會有幾種可能的狀況:
defer爲true:延遲加載腳本,在文檔完成解析完成開始執行,而且在DOMContentLoaded事件以前執行完成。
async爲true:異步加載腳本,下載完畢後再執行,在window的load事件以前執行完成
利用這兩個屬性異步加載js,還得了解它們的毛病:
使用defer屬性,最好是外部的script
使用defer、async的腳本禁止使用document.write()方法
當腳本嘗試訪問的樣式屬性可能還沒有加載的樣式表時,瀏覽器會禁止該腳本等待樣式表加載完成,這等於樣式表阻塞了腳本的執行。因此使用defer、async的腳本最好不要請求樣式信息時。
不論是使用defer仍是async屬性,都須要首先將頁面中的js文件進行整理,各個腳本文件之間的依賴性,哪些文件能夠延遲加載等等,作好js代碼的合併和拆分,而後再根據頁面須要合理的使用這兩個屬性。defer屬性聲明這個腳本中將不會有 document.write 或 dom 修改。
當全部腳本解析完成後,JavaScript進入第二個階段,這個階段的是異步的,而且由事件驅動的。在事件驅動階段,WEB瀏覽器調用事件處理程序函數,來響應異步發生的事件。調用事件處理函數一般是用戶輸入,網絡活動,運行和JavaScript中的錯誤來觸發。
經過註冊事件處理程序函數來處理程序,註冊的事件在發生時異步調用這些函數,setTimeout()和setInterval()也都是異步的。因此頁面內容中有內聯script放在setTimeout()執行是異步JS的一種方法,固然將代碼程序放在DOMReady內執行也是異步加載的方法。二者都將代碼執行階段放在了事件驅動階段。
在dom中建立的script標籤在瀏覽器中則是異步,以下:
function delay_js(src){ var objScript = document.createElement('script'); objScript.setAttribute('src', src); objScript.setAttribute('type', 'text/javascript'); document.body.appendChild(objScript); return objScript;}異步加載JS
以上代碼異步加載的JS下載是跟其餘同樣是並行的,可是執行階段仍是會阻止頁面渲染,延長了window.onload的事件。怎麼樣才能下載和執行JS都不阻塞頁面的渲染呢,以下:
function loadjs(src, succ) { var elem = delay_js(src); if ((navigator.userAgent.indexOf('MSIE') == -1) ? false: true) { elem.onreadystatechange = function() { if (/loaded|complete/.test(this.readyState)){ succ() } }; }else{ elem.onload = function(){ succ(); } } elem.onerror = function() {};}JS異步下載+執行方案
代碼分析:
非IE瀏覽器能捕捉到script的 script.onload 事件,因此只能藉助script.onreadystatechange.
檢測onreadystatechange狀態中,IE7/8最後一個狀態就只是loaded,而IE6中最後一個狀態可能 complete 也多是loaded,因此用正則loaded|complete兩個狀態都檢測。
異步加載JS的問題是沒法使用 document.write 輸出文檔內容,由於根本沒法肯定 document.write 應該輸出到什麼位置,但仍是能夠在DOMReady以後執行操做dom
除了DOMContentLoaded 與 OnLoad 事件、async屬性以及defer屬性script能解決JS異步加載外,還有其餘方法能夠異步加載JS:
經過ajax獲取js內容,而後eval執行。
var xhrObj = getXHRObject(); xhrObj.onreadystatechange = function() { if ( xhrObj.readyState != 4 ) return; eval(xhrObj.responseText); }; xhrObj.open('GET', 'A.js', true); xhrObj.send('');
經過建立iframe:建立並插入iframe元素。
var iframe = document.createElement('iframe');document.body.appendChild(iframe);var doc = iframe.contentWindow.document;doc.open().write('<body onload="insertJS()">');doc.close();
此方法存在跨域問題,若是父頁面域名修改,則經過javascript協議執行一樣域名升級語句。
頁內 js 的內容被註釋不會執行,可是在須要的時候去掉註釋,eval執行js,
在頁面中document.write Script Tag
用 setTimeout 延遲0秒 與 其它方法組合