一篇文章帶你搞定JavaScript 性能調(diào)優(yōu)
大家好,我是皮皮。
JavaScript 是單線程運(yùn)行的,所以在在執(zhí)行效率上并不是很高,隨著用戶體驗(yàn)的日益重視,前端性能對用戶體驗(yàn)的影響備受關(guān)注,但由于性能問題相對復(fù)雜,接下來我們來了解下JavaScript如何提高性能;
從加載上優(yōu)化:合理放置腳本位置
由于 JavaScript 的阻塞特性,在每一個(gè)<script>出現(xiàn)的時(shí)候,無論是內(nèi)嵌還是外鏈的方式,它都會(huì)讓頁面等待腳本的加載解析和執(zhí)行,并且<script>標(biāo)簽可以放在頁面的<head>或者<body>中,因此,如果我們頁面中的 css 和 js 的引用順序或者位置不一樣,即使是同樣的代碼,加載體驗(yàn)都是不一樣的。示例如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>js 引用的位置性能優(yōu)化</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>
其后面的內(nèi)容將會(huì)被掛起等待,直到index-1.js 加載、執(zhí)行完畢,才會(huì)執(zhí)行第二個(gè)腳本文件 index-2.js,這個(gè)時(shí)候頁面又將被掛起等待腳本的加載和執(zhí)行完成,一次類推,這樣用戶打開該界面的時(shí)候,界面內(nèi)容會(huì)明顯被延遲,我們就會(huì)看到一個(gè)空白的頁面閃過,這種體驗(yàn)是明顯不好的,因此 我們應(yīng)該盡量的讓內(nèi)容和樣式先展示出來,將 js 文件放在 最后,以此來優(yōu)化用戶體驗(yàn)。如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>js 引用的位置性能優(yōu)化</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>
這段代碼展示了在 HTML 文檔中放置<script>標(biāo)簽的推薦位置。盡管腳本下載會(huì)阻塞另一個(gè)腳本,但是頁面的大部分內(nèi)容都已經(jīng)下載完 成并顯示給了用戶,因此頁面下載不會(huì)顯得太慢。這是雅虎特別性能小組提出的優(yōu)化 JavaScript 的首要規(guī)則:將腳本放在底部。
從請求次數(shù)上優(yōu)化:減少請求次數(shù)
由于每個(gè)<script>標(biāo)簽初始下載時(shí)都會(huì)阻塞頁面渲染,所以減少頁面包含的<script>標(biāo)簽數(shù)量有助于改善這一情況。這不僅針對外鏈腳本,內(nèi)嵌腳本的數(shù)量同樣也要限制。瀏覽器在解析 HTML 頁面的過程中每遇到一個(gè)<script>標(biāo)簽,都會(huì)因執(zhí)行腳本而導(dǎo)致一定的延時(shí),因此最小化延遲時(shí)間將會(huì)明顯改善頁面的總體性能。
這個(gè)問題在處理外鏈 JavaScript 文件時(shí)略有不同??紤]到 HTTP 請求會(huì)帶來額外的性能開銷,因此下載單個(gè) 100Kb 的文件將比下載 5 個(gè) 20Kb 的文件更快。也就是說,減少頁面中外鏈腳本的數(shù)量將會(huì)改善性能。
通常一個(gè)大型網(wǎng)站或應(yīng)用需要依賴數(shù)個(gè) JavaScript 文件。您可以把多個(gè)文件合并成一個(gè),這樣只需要引用一個(gè)<script>標(biāo)簽,就可以減少性能消耗。文件合并的工作可通過離線的打包工具或者一些實(shí)時(shí)的在線服務(wù)來實(shí)現(xiàn)。
需要特別提醒的是,把一段內(nèi)嵌腳本放在引用外鏈樣式表的之后會(huì)導(dǎo)致頁面阻塞去等待樣式表的下載。這樣做是為了確保內(nèi)嵌腳本在執(zhí)行時(shí)能獲得最精確的樣式信息。因此,建議不要把內(nèi)嵌腳本緊跟在標(biāo)簽后面。
有一點(diǎn)我們需要知道:頁面加載的過程中,最耗時(shí)間的不是 js 本身的加載和執(zhí)行,相比之下,每一次去后端獲取資源,客戶端與后臺(tái)建立鏈接才是最耗時(shí)的,也就是大名鼎鼎的Http 三次握手,當(dāng)然,http 請求不是我們這一次討論的主題,因此,減少 HTTP 請求,是我們著重優(yōu)化的一項(xiàng),事實(shí)上,在頁面中 js 腳本文件加載很很多情況下,它的優(yōu)化效果是很顯著的。
從加載方式上優(yōu)化:無阻塞腳本加載
在 JavaScript 性能優(yōu)化上,減少腳本文件大小并限制 HTTP 請求的次數(shù)僅僅是讓界面響應(yīng) 迅速的第一步,現(xiàn)在的 web 應(yīng)用功能豐富,js 腳本越來越多,光靠精簡源碼大小和減少 次數(shù)不總是可行的,即使是一次 HTTP 請求,但文件過于龐大,界面也會(huì)被鎖死很長一段 時(shí)間,這明顯不好的,因此,無阻塞加載技術(shù)應(yīng)運(yùn)而生。簡單來說, 就是 頁面在加載完成后才加載 s js 代碼,也就是在 w window 對象的 d load 事件觸 發(fā)后才去下載腳本。要實(shí)現(xiàn)這種方式,常用以下幾種方式:
延遲腳本加載( defer )
HTML4 為<script>標(biāo)簽定義了一個(gè)擴(kuò)展屬性:defer。Defer 屬性指明本元素所含的腳本不會(huì)修改 DOM,因此代碼能安全地延遲執(zhí)行。defer 屬性只被 IE 4 和 Firefox 3.5 更高版本的瀏覽器所支持,所以它不是一個(gè)理想的跨瀏覽器解決方案。在其他瀏覽器中,defer 屬性會(huì)被直接忽略,因此<script>標(biāo)簽會(huì)以默認(rèn)的方式處理,也就是說會(huì)造成阻塞。然而,如果您的目標(biāo)瀏覽器支持的話,這仍然是個(gè)有用的解決方案。
<script type="text/javascript" src="index-1.js" defer></script>
帶有 defer 屬性的<script>標(biāo)簽可以放置在文檔的任何位置。對應(yīng)的 JavaScript 文件將在頁面解析到<script>標(biāo)簽時(shí)開始下載,但不會(huì)執(zhí)行,直到 DOM 加載完成,即 onload事件觸發(fā)前才會(huì)被執(zhí)行。當(dāng)一個(gè)帶有 defer 屬性的 JavaScript 文件下載時(shí),它不會(huì)阻塞瀏覽的其他進(jìn)程,因此這類文件可以與其他資源文件一起并行下載?!と魏螏в?defer 屬性的<script>元素在 DOM 完成加載之前都不會(huì)被執(zhí)行,無論內(nèi)嵌或者是外鏈腳本都是如此。
延遲腳本加載( async )
HTML5 規(guī)范中也引入了 async 屬性,用于異步加載腳本,其大致作用和 defer 是一樣的,都是采用的并行下載,下載過程中不會(huì)有阻塞,但 不同點(diǎn)在于他們的執(zhí)行時(shí)機(jī),c async 需要加載完成后就會(huì)自動(dòng)執(zhí)行代碼 ,但是 r defer 需要等待頁面加載完成后才會(huì)執(zhí)行。
從加載方式上優(yōu)化:動(dòng)態(tài)添加腳本元素
把代碼以動(dòng)態(tài)的方式添加的好處是:無論這段腳本是在何時(shí)啟動(dòng)下載,它的下載和執(zhí)行過程都不會(huì)阻塞頁面的其他進(jìn)程,我們甚至可以直接添加帶頭部 head 標(biāo)簽中,都不會(huì)影響其他部分。因此,作為開發(fā)的你肯定見到過諸如此類的代碼塊:
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);
這種方式便是動(dòng)態(tài)創(chuàng)建腳本的方式,也就是我們現(xiàn)在所說的動(dòng)態(tài)腳本創(chuàng)建。通過這種方式下載文件后,代碼就會(huì)自動(dòng)執(zhí)行。但是在現(xiàn)代瀏覽器中,這段腳本會(huì)等待所有動(dòng)態(tài)節(jié)點(diǎn)加載完成后再執(zhí)行。這種情況下,為了確保當(dāng)前代碼中包含的別的代碼的接口或者方法能夠被成功調(diào)用,就必須在別的代碼加載前完成這段代碼的準(zhǔn)備。解決的具體操作思路是:現(xiàn)代瀏覽器會(huì)在 script 標(biāo)簽內(nèi)容下載完成后接收一個(gè)load 事件,我們就可以在 load 事件后再去執(zhí)行我們想要執(zhí)行的代碼加載和運(yùn)行,在 IE 中,它會(huì)接收 loaded 和 complete事件,理論上是 loaded 完成后才會(huì)有 completed,但實(shí)踐告訴我們他兩似乎并沒有個(gè)先后,甚至有時(shí)候只會(huì)拿到其中的一個(gè)事件,我們可以單獨(dú)的封裝一個(gè)專門的函數(shù)來體現(xiàn)這個(gè)功能的實(shí)踐性,因此一個(gè)統(tǒng)一的寫法是:
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') {
// 確保執(zhí)行兩次
script.onreadystatechange = null;
// todo 執(zhí)行要執(zhí)行的代碼
callback()
}
}
} else {
script.onload = function () {
callback();
}
}
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);
}
LoadScript 函數(shù)接收兩個(gè)參數(shù),分別是要加載的腳本路徑和加載成功后需要執(zhí)行的回調(diào)函數(shù),LoadScript 函數(shù)本身具有特征檢測功能,根據(jù)檢測結(jié)果(IE 和其他瀏覽器),來決定腳本處理過程中監(jiān)聽哪一個(gè)事件。實(shí)際上這里的 LoadScript()函數(shù),就是我們所說的 LazyLoad.js(懶加載)的原型。
從加載方式上優(yōu)化:XMLHttpRequest 腳本注入
通過 XMLHttpRequest 對象來獲取腳本并注入到頁面也是實(shí)現(xiàn)無阻塞加載的另一種方式,這個(gè)我覺得不難理解,這其實(shí)和動(dòng)態(tài)添加腳本的方式是一樣的思想,來看具體代碼:
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) {
// 如果從后臺(tái)或者緩存中拿到數(shù)據(jù),則添加到 script 中并加載執(zhí)行。
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = xhr.responseText;
// 將創(chuàng)建的 script 添加到文檔頁面
document.body.appendChild(script);
}
}
}
通過這種方式拿到的數(shù)據(jù)有兩個(gè)優(yōu)點(diǎn):其一,我們可以控制腳本是否要立即執(zhí)行,因?yàn)槲覀冎佬聞?chuàng)建的 script 標(biāo)簽只要添加到文檔界面中它就會(huì)立即執(zhí)行,因此,在添加到文檔界面之前,也就是在 appendChild()之前,我們可以根據(jù)自己實(shí)際的業(yè)務(wù)邏輯去實(shí)現(xiàn)需求,到想要讓它執(zhí)行的時(shí)候,再 appendChild()即可。其二:它的兼容性很好,所有主流瀏覽器都支持,它不需要想動(dòng)態(tài)添加腳本的方式那樣,我們自己去寫特性檢測代碼;但由于是使用了 XHR 對象,所以不足之處是獲取這種資源有“域”的限制。資源 必須在同一個(gè)域下才可以,不可以跨域操作。
總結(jié)
減少 JavaScript 對性能的影響有以下幾種方法:
- 將所有的<script>標(biāo)簽放到頁面底部,也就是</body>閉合標(biāo)簽之前,這能確保在 腳本執(zhí)行前頁面已經(jīng)完成了渲染。
- 盡可能地合并腳本。頁面中的<script>標(biāo)簽越少,加載也就越快,響應(yīng)也越迅速。無論是外鏈腳本還是內(nèi)嵌腳本都是如此。
- 采用無阻塞下載 JavaScript 腳本的方法:
使用<script>標(biāo)簽的 defer 屬性(僅適用于 IE 和 Firefox 3.5 以上版 本);
使用動(dòng)態(tài)創(chuàng)建的<script>元素來下載并執(zhí)行代碼;
使用 XHR 對象下載 JavaScript 代碼并注入頁面中。
通過以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 網(wǎng)站和應(yīng)用的實(shí)際性能。