一篇文章帶你搞定JavaScript 性能調(diào)優(yōu)

大家好,我是皮皮。

JavaScript 是單線程運(yùn)行的,所以在在執(zhí)行效率上并不是很高,隨著用戶體驗(yàn)的日益重視,前端性能對(duì)用戶體驗(yàn)的影響備受關(guān)注,但由于性能問(wèn)題相對(duì)復(fù)雜,接下來(lái)我們來(lái)了解下JavaScript如何提高性能;

從加載上優(yōu)化:合理放置腳本位置
由于 JavaScript 的阻塞特性,在每一個(gè)<script>出現(xiàn)的時(shí)候,無(wú)論是內(nèi)嵌還是外鏈的方式,它都會(huì)讓頁(yè)面等待腳本的加載解析和執(zhí)行,

并且<script>標(biāo)簽可以放在頁(yè)面的<head>或者<body>中,因此,如果我們頁(yè)面中的 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>

以上代碼是一個(gè)簡(jiǎn)單的 html 界面,其中加載了兩個(gè) js 腳本文件和一個(gè) css 樣式文件,由于 js 的阻塞問(wèn)題,當(dāng)加載到 index-1.js 的時(shí)候,

其后面的內(nèi)容將會(huì)被掛起等待,直到index-1.js 加載、執(zhí)行完畢,才會(huì)執(zhí)行第二個(gè)腳本文件 index-2.js,這個(gè)時(shí)候頁(yè)面又將被掛起等待腳

本的加載和執(zhí)行完成,一次類推,這樣用戶打開(kāi)該界面的時(shí)候,界面內(nèi)容會(huì)明顯被延遲,我們就會(huì)看到一個(gè)空白的頁(yè)面閃過(guò),這種體驗(yàn)是

明顯不好的,因此 我們應(yīng)該盡量的讓內(nèi)容和樣式先展示出來(lái),將 js 文件放在 最后,以此來(lái)優(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è)腳本,但是頁(yè)面的大部分內(nèi)容都已經(jīng)下載完

成并顯示給了用戶,因此頁(yè)面下載不會(huì)顯得太慢。這是雅虎特別性能小組提出的優(yōu)化 JavaScript 的首要規(guī)則:將腳本放在底部。

從請(qǐng)求次數(shù)上優(yōu)化:減少請(qǐng)求次數(shù)
由于每個(gè)<script>標(biāo)簽初始下載時(shí)都會(huì)阻塞頁(yè)面渲染,所以減少頁(yè)面包含的<script>標(biāo)簽數(shù)量有助于改善這一情況。這不僅針對(duì)外鏈腳本,內(nèi)嵌腳本的數(shù)量同樣也要限制。瀏覽器在解析 HTML 頁(yè)面的過(guò)程中每遇到一個(gè)<script>標(biāo)簽,都會(huì)因執(zhí)行腳本而導(dǎo)致一定的延時(shí),因此最小化延遲時(shí)間將會(huì)明顯改善頁(yè)面的總體性能。

這個(gè)問(wèn)題在處理外鏈 JavaScript 文件時(shí)略有不同??紤]到 HTTP 請(qǐng)求會(huì)帶來(lái)額外的性能開(kāi)銷,因此下載單個(gè) 100Kb 的文件將比下載 5 個(gè) 20Kb 的文件更快。也就是說(shuō),減少頁(yè)面中外鏈腳本的數(shù)量將會(huì)改善性能。

通常一個(gè)大型網(wǎng)站或應(yīng)用需要依賴數(shù)個(gè) JavaScript 文件。您可以把多個(gè)文件合并成一個(gè),這樣只需要引用一個(gè)<script>標(biāo)簽,就可以減少性能消耗。文件合并的工作可通過(guò)離線的打包工具或者一些實(shí)時(shí)的在線服務(wù)來(lái)實(shí)現(xiàn)。

需要特別提醒的是,把一段內(nèi)嵌腳本放在引用外鏈樣式表的之后會(huì)導(dǎo)致頁(yè)面阻塞去等待樣式表的下載。這樣做是為了確保內(nèi)嵌腳本在執(zhí)行時(shí)能獲得最精確的樣式信息。因此,建議不要把內(nèi)嵌腳本緊跟在標(biāo)簽后面。

有一點(diǎn)我們需要知道:頁(yè)面加載的過(guò)程中,最耗時(shí)間的不是 js 本身的加載和執(zhí)行,相比之下,每一次去后端獲取資源,客戶端與后臺(tái)建立鏈接才是最耗時(shí)的,也就是大名鼎鼎的Http 三次握手,當(dāng)然,http 請(qǐng)求不是我們這一次討論的主題,因此,減少 HTTP 請(qǐng)求,是我們著重優(yōu)化的一項(xiàng),事實(shí)上,在頁(yè)面中 js 腳本文件加載很很多情況下,它的優(yōu)化效果是很顯著的。

從加載方式上優(yōu)化:無(wú)阻塞腳本加載
在 JavaScript 性能優(yōu)化上,減少腳本文件大小并限制 HTTP 請(qǐng)求的次數(shù)僅僅是讓界面響應(yīng) 迅速的第一步,現(xiàn)在的 web 應(yīng)用功能豐富,js 腳本越來(lái)越多,光靠精簡(jiǎn)源碼大小和減少 次數(shù)不總是可行的,即使是一次 HTTP 請(qǐng)求,但文件過(guò)于龐大,界面也會(huì)被鎖死很長(zhǎng)一段 時(shí)間,這明顯不好的,因此,無(wú)阻塞加載技術(shù)應(yīng)運(yùn)而生。簡(jiǎn)單來(lái)說(shuō), 就是 頁(yè)面在加載完成后才加載 s js 代碼,也就是在 w window 對(duì)象的 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)的方式處理,也就是說(shuō)會(huì)造成阻塞。然而,如果您的目標(biāo)瀏覽器支持的話,這仍然是個(gè)有用的解決方案。

<script type="text/javascript" src="index-1.js" defer></script>
帶有 defer 屬性的<script>標(biāo)簽可以放置在文檔的任何位置。對(duì)應(yīng)的 JavaScript 文件將在頁(yè)面解析到<script>標(biāo)簽時(shí)開(kāi)始下載,但不會(huì)執(zhí)行,直到 DOM 加載完成,即 onload事件觸發(fā)前才會(huì)被執(zhí)行。當(dāng)一個(gè)帶有 defer 屬性的 JavaScript 文件下載時(shí),它不會(huì)阻塞瀏覽的其他進(jìn)程,因此這類文件可以與其他資源文件一起并行下載?!と魏螏в?defer 屬性的<script>元素在 DOM 完成加載之前都不會(huì)被執(zhí)行,無(wú)論內(nèi)嵌或者是外鏈腳本都是如此。

延遲腳本加載( async )
HTML5 規(guī)范中也引入了 async 屬性,用于異步加載腳本,其大致作用和 defer 是一樣的,都是采用的并行下載,下載過(guò)程中不會(huì)有阻塞,但 不同點(diǎn)在于他們的執(zhí)行時(shí)機(jī),c async 需要加載完成后就會(huì)自動(dòng)執(zhí)行代碼 ,但是 r defer 需要等待頁(yè)面加載完成后才會(huì)執(zhí)行。

從加載方式上優(yōu)化:動(dòng)態(tài)添加腳本元素
把代碼以動(dòng)態(tài)的方式添加的好處是:無(wú)論這段腳本是在何時(shí)啟動(dòng)下載,它的下載和執(zhí)行過(guò)程都不會(huì)阻塞頁(yè)面的其他進(jìn)程,我們甚至可以直接添加帶頭部 head 標(biāo)簽中,都不會(huì)影響其他部分。因此,作為開(kāi)發(fā)的你肯定見(jiàn)到過(guò)諸如此類的代碼塊:

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)在所說(shuō)的動(dòng)態(tài)腳本創(chuàng)建。通過(guò)這種方式下載文件后,代碼就會(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í)踐告訴我們他兩似乎并沒(méi)有個(gè)先后,甚至有時(shí)候只會(huì)拿到其中的一個(gè)事件,我們可以單獨(dú)的封裝一個(gè)專門的函數(shù)來(lái)體現(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ù)本身具有特征檢測(cè)功能,根據(jù)檢測(cè)結(jié)果(IE 和其他瀏覽器),來(lái)決定腳本處理過(guò)程中監(jiān)聽(tīng)哪一個(gè)事件。實(shí)際上這里的 LoadScript()函數(shù),就是我們所說(shuō)的 LazyLoad.js(懶加載)的原型。

從加載方式上優(yōu)化:XMLHttpRequest 腳本注入
通過(guò) XMLHttpRequest 對(duì)象來(lái)獲取腳本并注入到頁(yè)面也是實(shí)現(xiàn)無(wú)阻塞加載的另一種方式,這個(gè)我覺(jué)得不難理解,這其實(shí)和動(dòng)態(tài)添加腳本的方式是一樣的思想,來(lá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 添加到文檔頁(yè)面
            document.body.appendChild(script);
        }
    }
}

通過(guò)這種方式拿到的數(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)添加腳本的方式那樣,我們自己去寫特性檢測(cè)代碼;但由于是使用了 XHR 對(duì)象,所以不足之處是獲取這種資源有“域”的限制。資源 必須在同一個(gè)域下才可以,不可以跨域操作。

總結(jié)
減少 JavaScript 對(duì)性能的影響有以下幾種方法:

將所有的<script>標(biāo)簽放到頁(yè)面底部,也就是</body>閉合標(biāo)簽之前,這能確保在 腳本執(zhí)行前頁(yè)面已經(jīng)完成了渲染。
盡可能地合并腳本。頁(yè)面中的<script>標(biāo)簽越少,加載也就越快,響應(yīng)也越迅速。無(wú)論是外鏈腳本還是內(nèi)嵌腳本都是如此。
采用無(wú)阻塞下載 JavaScript 腳本的方法:
使用<script>標(biāo)簽的 defer 屬性(僅適用于 IE 和 Firefox 3.5 以上版 本);
使用動(dòng)態(tài)創(chuàng)建的<script>元素來(lái)下載并執(zhí)行代碼;
使用 XHR 對(duì)象下載 JavaScript 代碼并注入頁(yè)面中。
通過(guò)以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 網(wǎng)站和應(yīng)用的實(shí)際性能。

小伙伴們,快快用實(shí)踐一下吧!如果在學(xué)習(xí)過(guò)程中,有遇到任何問(wèn)題,歡迎加我好友,我拉你進(jìn)Python學(xué)習(xí)交流群共同探討學(xué)習(xí)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容