本文主要是從性能優化的角度來探讨JavaScript在加載與執行過程中的優化思路與實踐方法,既是細說,文中在涉及原理性的地方,不免會多說幾句,還望各位讀者保持耐心,仔細了解,請相信,您的耐心付出一定會讓您得到與之比對的回報。
緣起
随着使用者體驗的日益重視,前端性能對使用者體驗的影響備受關注,但由于引起性能問題的原因相對複雜,我們很難但從某一方面或某幾個方面來全面解決它,這也是我行此文的原因,想以此文為起點,用一系列文章來深層次探讨與梳理有關Javascript性能的方方面面,以填補并夯實自己的知識結構。
目錄結構
本文大緻的行文思路,包含但不局限:
- 不得不說的JavaScript阻塞特性
- 合理放置腳本位置,以優化加載體驗,js腳本放在
标簽閉合之前。<body>
- 減少HTTP請求次數,壓縮精簡腳本代碼。
- 無阻塞加載JavaScript腳本:
- 使用
标簽的defer屬性。<script>
- 使用HTML5的async屬性。
- 動态建立
元素加載JavaScript。<script>
- 使用XHR對象加載JavaScript。
- 使用
不得不說的JavaScript的阻塞特性
前端開發者應該都知道,JavaScript是單線程運作的,也就是說,在JavaScript運作一段代碼塊的時候,頁面中其他的事情(UI更新或者别的腳本加載執行等)在同一時間段内是被挂起的狀态,不能被同時處理的,是以在執行一段js腳本的時候,這段代碼會影響其他的操作。這是JavaScript本身的特性,我們無法改變。
我們把JavaScript的這一特性叫做阻塞特性,正因為這個阻塞特性,讓前端的性能優化尤其是在對JavaScript的性能優化上變得相對複雜。
為什麼要阻塞?
也許你還會問,既然JavaScript的阻塞特性會産生這麼多的問題,為什麼JavaScript語言不能像Java等語言一樣,采用多線程,不就OK了麼?
要徹底了解JavaScript的單線程設計,其實并不難,簡單總結就是:最初設計JavaScript的目的隻是用來在浏覽器端改善網頁的使用者體驗,去處理一些頁面中類似表單驗證的簡單任務。是以,那個時候JavaScript所做的事情很少,并且代碼不會太多,這也奠定了JavaScript和界面操作的強關聯性。
既然JavaScript和界面操作強相關,我們不妨這樣了解:試想,如果在某個頁面中有兩段js腳本都會去更改某一個dom元素的内容,如果JavaScript采用了多線程的處理方式,那麼最終頁面元素顯示的内容到底是哪一段js腳本操作的結果就不确定了,因為兩段js是通過不同線程加載的,我們無法預估誰先處理完,這是我們不想要的結果,而這種界面資料更新的操作在JavaScript中比比皆是。是以,我們就不難了解JavaScript單線程的設計原因:JavaScript采用單線程,是為了避免在執行過程中頁面内容被不可預知的重複修改。
關于JavaScript的更多“身世”之謎,可以看阮一峰老師的 Javascript誕生記
從加載上優化:合理放置腳本位置
由于JavaScript的阻塞特性,在每一個<script>出現的時候,無論是内嵌還是外鍊的方式,它都會讓頁面等待腳本的加載解析和執行,并且<script>标簽可以放在頁面的<head>或者<body>中,是以,如果我們頁面中的css和js的引用順序或者位置不一樣,即使是同樣的代碼,加載體驗都是不一樣的。舉個栗子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>js引用的位置性能優化</title>
<script type="text/javascript" src="index-1.js"></script>
<script type="text/javascript" src="index-2.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
複制代碼
以上代碼是一個簡單的html界面,其中加載了兩個js腳本檔案和一個css樣式檔案,由于js的阻塞問題,當加載到index-1.js的時候,其後面的内容将會被挂起等待,直到index-1.js加載、執行完畢,才會執行第二個腳本檔案index-2.js,這個時候頁面又将被挂起等待腳本的加載和執行完成,一次類推,這樣使用者打開該界面的時候,界面内容會明顯被延遲,我們就會看到一個空白的頁面閃過,這種體驗是明顯不好的,是以我們應該盡量的讓内容和樣式先展示出來,将js檔案放在<body>最後,以此來優化使用者體驗。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>js引用的位置性能優化</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="index-1.js"></script>
<script type="text/javascript" src="index-2.js"></script>
</body>
</html>
複制代碼
從請求次數上優化: 減少請求次數
有一點我們需要知道:頁面加載的過程中,最耗時間的不是js本身的加載和執行,相比之下,每一次去後端擷取資源,用戶端與背景建立連結才是最耗時的,也就是大名鼎鼎的Http三次握手,當然,http請求不是我們這一次讨論的主題,想深入了解的自行搜尋,網絡上相關文章很多。
是以,減少HTTP請求,是我們着重優化的一項,事實上,在頁面中js腳本檔案加載很很多情況下,它的優化效果是很顯著的。要減少HTTP的請求,就不得不提起檔案的精簡壓縮了。
檔案的精簡與壓縮
要減少通路請求,則必然會用到js的**精簡(minifucation)和壓縮(compression)**了,需要注意的是,精簡檔案實際并不複雜,但不适當的使用也會導緻錯誤或者代碼無效的問題,是以在實際的使用中,最好在壓縮之前對js進行文法解析,幫我們避免不必要的問題(例如檔案中包含中文等unicode轉碼問題)。
解析型的壓縮工具常用有三:YUI Compressor、Closure Complier、UglifyJs
YUI Compressor: YUI Compressor的出現曾被認為是最受歡迎的基于解析器的壓縮工具,它将去去除代碼中的注釋和額外的空格并且會用單個或者兩個字元去代替局部變量以節省更多的位元組。但預設會關閉對可能導緻錯誤的替換,例如with或者eval();
Closure Complier: Closure Complier同樣是一個基于解析器的壓縮工具,他會試圖去讓你的代碼變得盡可能小。它會去除注釋和額外的空格并進行變量替換,而且會分析你的代碼進行相應的優化,比如他會删除你定義了但未使用的變量,也會把隻使用了一次的變量變成内聯函數。
UglifyJs:UglifyJs被認為第一個基于node.js的壓縮工具,它會去除注釋和額外的空格,替換變量名,合并var表達式,也會進行一些其他方式的優化
每種工具都有自己的優勢,比如說YUI壓縮後的代碼準确無誤,Closure壓縮的代碼會更小,而UglifyJs不依靠于Java而是基于JavaScript,相比Closure錯誤更少,具體用哪個更好我覺得沒有個确切的答案,開發者應該根據自己項目實際情況酌情選擇。
從加載方式上優化:無阻塞腳本加載
在JavaScript性能優化上,減少腳本檔案大小并限制HTTP請求的次數僅僅是讓界面響應迅速的第一步,現在的web應用功能豐富,js腳本越來越多,光靠精簡源碼大小和減少次數不總是可行的,即使是一次HTTP請求,但檔案過于龐大,界面也會被鎖死很長一段時間,這明顯不好的,是以,無阻塞加載技術應運而生。
簡單來說,就是頁面在加載完成後才加載js代碼,也就是在window對象的load事件觸發後才去下載下傳腳本。 要實作這種方式,常用以下幾種方式:
延遲腳本加載(defer)
HTML4以後為<script>标簽定義了一個擴充屬性:defer。defer屬性的作用是指明要加載的這段腳本不會修改DOM,是以代碼是可以安全的去延遲執行的,并且現在主流浏覽器已經全部對defer支援。
<script type="text/javascript" src="index-1.js" defer></script>
複制代碼
帶defer屬性的<script>标簽在DOM完成加載之前都不會去執行,無論是内嵌還是外鍊方式。
延遲腳本加載(async)
HTML5規範中也引入了async屬性,用于異步加載腳本,其大緻作用和defer是一樣的,都是采用的并行下載下傳,下載下傳過程中不會有阻塞,但不同點在于他們的執行時機,async需要加載完成後就會自動執行代碼,但是defer需要等待頁面加載完成後才會執行。
從加載方式上優化:動态添加腳本元素
把代碼以動态的方式添加的好處是:無論這段腳本是在何時啟動下載下傳,它的下載下傳和執行過程都不會則色頁面的其他程序,我們甚至可以直接添加帶頭部head标簽中,都不會影響其他部分。
是以,作為開發的你肯定見到過諸如此類的代碼塊:
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);
複制代碼
這種方式便是動态建立腳本的方式,也就是我們現在所說的動态腳本建立。通過這種方式下載下傳檔案後,代碼就會自動執行。但是在現代浏覽器中,這段腳本會等待所有動态節點加載完成後再執行。這種情況下,為了確定目前代碼中包含的别的代碼的接口或者方法能夠被成功調用,就必須在别的代碼加載前完成這段代碼的準備。解決的具體操作思路是:
現代浏覽器會在script标簽内容下載下傳完成後接收一個load事件,我們就可以在load事件後再去執行我們想要執行的代碼加載和運作,在IE中,它會接收loaded和complete事件,理論上是loaded完成後才會有completed,但實踐告訴我們他兩似乎并沒有個先後,甚至有時候隻會拿到其中的一個事件,我們可以單獨的封裝一個專門的函數來展現這個功能的實踐性,是以一個統一的寫法是:
function LoadScript(url, callback) {
var script = document.createElement('script');
script.type = 'text/javascript';
// IE浏覽器下
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
// 確定執行兩次
script.onreadystatechange = null;
// todo 執行要執行的代碼
callback()
}
}
} else {
script.onload = function () {
callback();
}
}
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);
}
複制代碼
LoadScript函數接收兩個參數,分别是要加載的腳本路徑和加載成功後需要執行的回調函數,LoadScript函數本身具有特征檢測功能,根據檢測結果(IE和其他浏覽器),來決定腳本處理過程中監聽哪一個事件。
實際上這裡的LoadScript()函數,就是我們所說的LazyLoad.js(懶加載)的原型。
有了這個方法,我們可以實作一個簡單的多檔案按某一固定順序加載代碼塊:
LoadScript('file-1.js', function(){
LoadScript('file-2.js', function(){
LoadScript('file-3.js', function(){
console.log('loaded all')
})
})
})
複制代碼
以上代碼執行的時候,将會首先加載file-1.js,加載完成後再去加載file-2.js,以此類推。當然這種寫法肯定是有待商榷的(多重回調嵌套寫法簡直就是地獄),但這種動态腳本添加的思想,和加載過程中需要注意的和避免的問題,都在LoadScript函數中得以澄清解決。
當然,如果檔案過多,并且加載的順序有要求,最好的解決方法還是建議按照正确的順序合并一起加載,這從各方面講都是更好的法子。
從加載方式上優化:XMLHttpRequest腳本注入
通過XMLHttpRequest對象來擷取腳本并注入到頁面也是實作無阻塞加載的另一種方式,這個我覺得不難了解,這其實和動态添加腳本的方式是一樣的思想,來看具體代碼:
var xhr = new XMLHttpRequest();
xhr.open('get', 'file-1.js', true);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
// 如果從背景或者緩存中拿到資料,則添加到script中并加載執行。
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = xhr.responseText;
// 将建立的script添加到文檔頁面
document.body.appendChild(script);
}
}
}
複制代碼
通過這種方式拿到的資料有兩個優點:其一,我們可以控制腳本是否要立即執行,因為我們知道新建立的script标簽隻要添加到文檔界面中它就會立即執行,是以,在添加到文檔界面之前,也就是在appendChild()之前,我們可以根據自己實際的業務邏輯去實作需求,到了想要讓它執行的時候,再appendChild()即可。其二:它的相容性很好,所有主流浏覽器都支援,它不需要想動态添加腳本的方式那樣,我們自己去寫特性檢測代碼;
但由于是使用了XHR對象,是以不足之處是擷取這種資源有“域”的限制。資源 必須在同一個域下才可以,不可以跨域操作。
最後總結
文章主要從JavaScript的加載和執行這一過程中挖掘探讨對前端優化的解決方案,并較細緻的羅列了各個解決方案的優勢和不足之處,當然,前端性能優化本就相對複雜,要想徹底了解其各中原由,還有很長一段路要走!
本文主要行文思路:
-
<body>
- 減少HTTP請求,壓縮精簡腳本代碼。
-
-
<script>
-
<script>
-
最後,由于個人水準原因,若有行文不全或疏漏錯誤之處,懇請各位讀者批評指正,一路有你,不勝感激!。
感謝這個時代,讓我們可以站在巨人的肩膀上,窺探程式世界的宏偉壯觀,我願以一顆赤子心,踏遍程式世界的千山萬水!願每一個行走在程式世界的同仁,都活成心中想要的樣子,加油!
作者:bs123
連結:https://juejin.im/post/5b27f67951882574ca1d44c8
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。