高效能JavaScript之載入和執行
JS在瀏覽器中的效能,可以認為是開發者所面臨的最重要的可行性問題。這個問題因JS的阻塞特性變得複雜,也就是說當瀏覽器在執行JS程式碼時,不能同時做其他任何事情。事實上,大多數瀏覽器都使用單一程序來處理UI(使用者介面)更新和JavaScript指令碼執行,所以同一時刻只能做其中一件事情。JS執行過程耗時越久,瀏覽器等待響應使用者輸入的時間就越長。
從基礎層面來說,這意味著<script>標籤每次出現都霸道地讓頁面等待指令碼的解析和執行。無論當前的JS程式碼是內嵌的還是在外鏈檔案中,頁面的下載和渲染都必須停下來等待指令碼的執行完成。這在頁面生存週期中是必要的,因為指令碼執行過程中可能會修改頁面的內容。一個典型的例子就是在頁面中使用document.write()(經常用來顯示廣告)。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> </head> <body> <p> <script> document.write("The date is "+(new Date()).toDateString());</script> </p> </body> </html>
當瀏覽器遇到<script>標籤時,當前的HTML頁面無從獲知JS是否會向<p>標籤新增內容,或引入其他元素,或關閉該標籤。因此,這時瀏覽器會停滯處理頁面,先執行JS程式碼,然後再繼續解析和渲染頁面。同樣的情況也發生在使用src的屬性載入JS的過程中,瀏覽器必須先花時間下載外鏈檔案中的程式碼,然後解析並執行它。在這個過程中,頁面渲染和使用者互動完全被阻塞了。
1.1指令碼位置
這裡先說說HTML4規範,HTML4規範指出<script>標籤可以放在HTML文件的<head>或<body>中,並允許出現多次。按照慣例,<script>標籤用來加載出現在<head>中的外鏈JS檔案中,挨著的<link>標籤用來載入外部CSS檔案或其他頁面元資訊。也就是說,把與樣式和行為有關的指令碼放在一起,並先載入它們,使得頁面能夠顯示正確的外觀和互動。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> <script src="file1.js"></script> <script src="file2.js"></script> <script src="file3.js"></script> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <p> Hello World </p> </body> </html>
這些看似正常的程式碼實際上有十分嚴重的效能問題:在<head>中載入了三個JS檔案。由於指令碼會阻塞頁面的渲染,直到它們全部下載並執行完成後,頁面的渲染才會繼續。
因此頁面的效能問題會很明顯。請記住,瀏覽器在解析到<body>標籤之前,不會渲染頁面的任何部分。把指令碼放到頁面頂部將會導致明顯的延遲,通常表現為顯示空白頁面,使用者無法瀏覽內容,也無法與頁面進行互動。
所以通常建議像JS指令碼一般都放在</body>前,也就是頁面最底下,而CSS檔案放在<head></head>之間。雖然說CSS檔案過大也會導致延遲,但是這種延遲是可以接受的,如果是JS指令碼與CSS指令碼放在<head></head>之間如上面的程式碼所示,那樣的話,延遲會顯得十分明顯。因此推薦<script>標籤儘可能放到<body>標籤底部,</body>標籤之前,以儘量減少對整個頁面下載的影響。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <p> Hello World </p> <script src="file1.js"></script> <script src="file2.js"></script> <script src="file3.js"></script> </body> </html>
記得在《高效能網站建設》這本書,其中提到的建議之一:就是將指令碼放在底部。
1.2組織指令碼
由於每個<script>標籤初始下載時,都會阻塞頁面渲染,所以減少頁面包含的<script>標籤數量有助於改善這一情況。這不僅僅針對外鏈指令碼,內嵌指令碼的數量同樣也要限制。瀏覽器在解析HTML頁面的過程中每遇到一個<script>標籤,都會因執行指令碼而導致一定的延時,因此最小延遲時間將會明顯改善頁面的總體效能。
一般情況下,組織指令碼不單單是將JS檔案中的註釋或者其他無關緊要的內容去掉,而且也要將其壓縮,通過YUI或者是將多個JS檔案合併壓縮成一個大的JS檔案。只需引用一個<script>標籤,就可以減少效能的損耗(主要是減少了因載入多個指令碼導致的延時)。多個合併壓縮成一個大的JS檔案,並將其放在CDN中並引入也是可以的。
1.3無阻塞指令碼
JS傾向於阻止瀏覽器的某些處理過程,如HTTP請求和使用者介面更新,這是開發者所面臨的最顯著的效能問題。減少JS檔案大小並限制HTTP請求數僅僅是建立響應迅速的Web應用的第一步。Web應用的功能越豐富,所需要的JS程式碼就越多,所以精簡原始碼不總是可行的。儘管下載單個較大的JS檔案只產生一個HTTP請求,卻會鎖死瀏覽器一大段時間。為了避免這種情況,你需要向頁面中逐步載入JS檔案,這樣做在某種程度上來說不會阻塞瀏覽器。
無阻塞指令碼的祕訣在於,在頁面載入完成後才載入JS程式碼。用專業術語來說,這意味著在window物件中的load事件觸發後再下載指令碼。有多種方式可以實現這一效果。
1.3.1延遲指令碼
HTML4為<script>標籤定義了一個擴充套件屬性:defer。Defer屬性指明本元素所含的指令碼不會修改DOM,因此程式碼能安全地延遲執行。該屬性只有IE4和FireFox3.5+的瀏覽器支援,所以它不是一個理性的跨瀏覽器解決方案。在其他瀏覽器中,defer屬性會被直接忽略,因此<script>標籤會以預設的方式處理(即會造成阻塞)。然而,如果你的目標瀏覽器支援的話,這仍然是個有用的解決方案。
帶有defer屬性的<script>標籤可以放置在文件的任何位置。對應JS檔案將頁面解析到<script>標籤時開始下載,但並不會執行,直到DOM載入完成(onload事件被觸發前)。當一個帶有defer屬性的JS檔案下載時,它不會阻塞瀏覽器的其他程序,因此這類檔案可以與頁面中的其他資源並行下載。
任何帶有defer屬性的<script>元素在DOM完成載入之前都不會被執行,無論內嵌或外鏈指令碼都是如此。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> </head> <body> <script defer> alert("defer"); </script> <script> alert("script"); </script> <script> window.onload=function(){ alert("load"); } </script> </body> </html>
這段程式碼在頁面處理過程中會彈出三次提示框。不支援defer屬性的瀏覽器的彈出屬性是"defer"、"script"、"load"。而在支援defer屬性的瀏覽器上,彈出的順序是:"script"、"defer"、"load"。請注意,帶有defer屬性的<script>元素不是跟在第二個後面執行,而是在onload事件處理器執行之前被呼叫。
當然了,目前我在我自己電腦上執行了上述程式碼,基本都不支援defer,可能需要更低的版本才能支援。
1.3.2動態指令碼元素
通過文件物件模型,你幾乎可以用JS動態建立HTML中的所有內容。其根本在於,<script>標籤與頁面中的其他元素並無差異:都能通過DOM引用,都能在文件中移動、刪除、甚至被建立。用標準的DOM方法可以非常容易地建立一個新的<script>元素。
13.3XMLHttpRequest指令碼注入
另外一種無阻塞載入的指令碼方法是使用XMLHttpRequest物件獲取指令碼並注入頁面中。此技術會先建立一個XHR物件,然後用它下載JS檔案,最後通過建立動態<script>元素將程式碼注入頁面中。
var xmlhttp;
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
}
}
xmlhttp.open("GET","test.js",true);
xmlhttp.send();
}
這段程式碼傳送一個GET請求獲取test.js檔案。事件處理函式onReadyStateChange檢查readyState是否為4,同時校驗HTTP狀態碼是否有效(200表示有效響應,304意味著從快取中讀取)。
這種方法主要優點是:你可以下載JS程式碼但不立即執行。由於程式碼是在<script>標籤之外返回的,因此它下載後不會自動執行,這使得你可以把指令碼的執行推行到你準備好的時候。另一個優點是,同樣的程式碼再所有的主流瀏覽器中無一例外都能正常工作。
這種方法的主要侷限性是JS檔案必須與所請求的頁面處於相同的域,這意味著JS檔案不能從CDN下載。因此大型Web應用通常不會採用XHR指令碼注入。
1.3.4推薦的無阻塞模式
向頁面中新增大量JS的推薦做法只需兩步:先新增動態載入所需的程式碼,然後載入初始化頁面所需的剩下的程式碼。因為第一部分的程式碼儘量精簡,甚至可能只包含loadScript()函式,它下載執行都很快,所以不會對頁面有太多影響。一旦初始程式碼就位,就用它來載入剩餘的JS。
例如:
<script src="loader.js"></script> <script> loadScript("the-rest.js",function(){ Application.init(); });
把這段程式碼載入放在</body>閉合標籤之前。這樣做有幾個好處:
(1)確保JS執行過程中不會阻礙其他內容顯示;
(2)當第二個JS檔案完成下載時,應用所需的所有DOM結構已經建立完畢,並做好互動準備,從而避免了需要另一個事件(比如window.onload)來檢測頁面是否準備好。
小結:
管理瀏覽器中的JS程式碼是個棘手的問題,因為程式碼執行過程中會阻塞瀏覽器的其他程序,比如使用者介面繪製。每次遇到<script>標籤,頁面都必須停下了等待程式碼下載(如果是外鏈檔案)並執行,然後繼續處理其他部分。儘管如此,還是有幾種方法能減少JS對效能的影響:
(1)</body>閉合標籤之前,將所有的<script>標籤放到頁面底部。這能確保在指令碼執行前,頁面已經完成渲染;
(2)合併指令碼。頁面中的<script>標籤越少,載入也就越快,響應也就越迅速。無論是外鏈還是內嵌指令碼都是如此;
(3)有多種無阻塞下載JS的方法:
a.使用<script>的defer屬性(注意:高版本瀏覽器不支援);
b.動態建立<script>元素來下載並執行程式碼;
c.使用XHR物件下載JS程式碼並注入頁面中;
通過以上策略,可以極大的提高那些需要使用大量JS的Web應用的實際效能。
我的感觸:
全文字質其實這麼幾個?
1.JS指令碼放置最底下(避免延遲導致渲染效果差);
2.合併程式碼,將大量JS合併和壓縮為一個JS檔案,本質上減少HTTP請求,同時也減少並行下載帶來的延遲;
做到上述兩點Web應用的效能也會得到很大程度上的提升,特別是做到2,2也正說明了webpack或者gulp流行的重要原因。