本文主要是從性能優化的角度來探討JavaScript在載入與執行過程中的優化思路與實踐方法,既是細說,文中在涉及原理性的地方,不免會多說幾句,還望各位讀者保持耐心,仔細理解,請相信,您的耐心付出一定會讓您得到與之匹配的回報。 緣起 隨著用戶體驗的日益重視,前端性能對用戶體驗的影響備受關註,但由於引起 ...
本文主要是從性能優化的角度來探討JavaScript在載入與執行過程中的優化思路與實踐方法,既是細說,文中在涉及原理性的地方,不免會多說幾句,還望各位讀者保持耐心,仔細理解,請相信,您的耐心付出一定會讓您得到與之匹配的回報。
緣起
隨著用戶體驗的日益重視,前端性能對用戶體驗的影響備受關註,但由於引起性能問題的原因相對複雜,我們很難但從某一方面或某幾個方面來全面解決它,這也是我行此文的原因,想以此文為起點,用一系列文章來深層次探討與梳理有關Javascript性能的方方面面,以填補並夯實自己的知識結構。
自己是從事了五年的前端工程師,不少人私下問我,2019年前端該怎麼學,方法有沒有?沒錯,自己整理了一份2019最全面前端學習資料,從最基礎的HTML+CSS+JS到HTML5的項目實戰的學習資料都有整理,送給每一位前端小伙伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小伙伴,需要可以私聊我喲,有我的前端開發qun,四八四,七五七,七六零,對於學習web前端有任何問題(學習方法,學習效率,如何就業)都可以問我,最後可以點一波關註喲!
目錄結構
本文大致的行文思路,包含但不局限:
不得不說的JavaScript阻塞特性
合理放置腳本位置,以優化載入體驗,js腳本放在 <body>標簽閉合之前。
減少HTTP請求次數,壓縮精簡腳本代碼。
無阻塞載入JavaScript腳本:
使用<script>標簽的defer屬性。
使用HTML5的async屬性。
動態創建<script>元素載入JavaScript。
使用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的阻塞特性,在每一個<script>出現的時候,無論是內嵌還是外鏈的方式,它都會讓頁面等待腳本的載入解析和執行,並且<script>標簽可以放在頁面的<head>或者<body>中,因此,如果我們頁面中的css和js的引用順序或者位置不一樣,即使是同樣的代碼,載入體驗都是不一樣的。舉個慄子:
以上代碼是一個簡單的html界面,其中載入了兩個js腳本文件和一個css樣式文件,由於js的阻塞問題,當載入到index-1.js的時候,其後面的內容將會被掛起等待,直到index-1.js載入、執行完畢,才會執行第二個腳本文件index-2.js,這個時候頁面又將被掛起等待腳本的載入和執行完成,一次類推,這樣用戶打開該界面的時候,界面內容會明顯被延遲,我們就會看到一個空白的頁面閃過,這種體驗是明顯不好的,因此我們應該儘量的讓內容和樣式先展示出來,將js文件放在<body>最後,以此來優化用戶體驗。
從請求次數上優化: 減少請求次數
有一點我們需要知道:頁面載入的過程中,最耗時間的不是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支持。
帶defer屬性的<script>標簽在DOM完成載入之前都不會去執行,無論是內嵌還是外鏈方式。
延遲腳本載入(async)
HTML5規範中也引入了async屬性,用於非同步載入腳本,其大致作用和defer是一樣的,都是採用的並行下載,下載過程中不會有阻塞,但不同點在於他們的執行時機,async需要載入完成後就會自動執行代碼,但是defer需要等待頁面載入完成後才會執行。
從載入方式上優化:動態添加腳本元素
把代碼以動態的方式添加的好處是:無論這段腳本是在何時啟動下載,它的下載和執行過程都不會則色頁面的其他進程,我們甚至可以直接添加帶頭部head標簽中,都不會影響其他部分。
因此,作為開發的你肯定見到過諸如此類的代碼塊:
這種方式便是動態創建腳本的方式,也就是我們現在所說的動態腳本創建。通過這種方式下載文件後,代碼就會自動執行。但是在現代瀏覽器中,這段腳本會等待所有動態節點載入完成後再執行。這種情況下,為了確保當前代碼中包含的別的代碼的介面或者方法能夠被成功調用,就必須在別的代碼載入前完成這段代碼的準備。解決的具體操作思路是:
現代瀏覽器會在script標簽內容下載完成後接收一個load事件,我們就可以在load事件後再去執行我們想要執行的代碼載入和運行,在IE中,它會接收loaded和complete事件,理論上是loaded完成後才會有completed,但實踐告訴我們他兩似乎並沒有個先後,甚至有時候只會拿到其中的一個事件,我們可以單獨的封裝一個專門的函數來體現這個功能的實踐性,因此一個統一的寫法是:
LoadScript函數接收兩個參數,分別是要載入的腳本路徑和載入成功後需要執行的回調函數,LoadScript函數本身具有特征檢測功能,根據檢測結果(IE和其他瀏覽器),來決定腳本處理過程中監聽哪一個事件。
實際上這裡的LoadScript()函數,就是我們所說的LazyLoad.js(懶載入)的原型。
有了這個方法,我們可以實現一個簡單的多文件按某一固定順序載入代碼塊:
以上代碼執行的時候,將會首先載入file-1.js,載入完成後再去載入file-2.js,以此類推。當然這種寫法肯定是有待商榷的(多重回調嵌套寫法簡直就是地獄),但這種動態腳本添加的思想,和載入過程中需要註意的和避免的問題,都在LoadScript函數中得以澄清解決。
當然,如果文件過多,並且載入的順序有要求,最好的解決方法還是建議按照正確的順序合併一起載入,這從各方面講都是更好的法子。
從載入方式上優化:XMLHttpRequest腳本註入
通過XMLHttpRequest對象來獲取腳本並註入到頁面也是實現無阻塞載入的另一種方式,這個我覺得不難理解,這其實和動態添加腳本的方式是一樣的思想,來看具體代碼:
通過這種方式拿到的數據有兩個優點:其一,我們可以控制腳本是否要立即執行,因為我們知道新創建的script標簽只要添加到文檔界面中它就會立即執行,因此,在添加到文檔界面之前,也就是在appendChild()之前,我們可以根據自己實際的業務邏輯去實現需求,到了想要讓它執行的時候,再appendChild()即可。其二:它的相容性很好,所有主流瀏覽器都支持,它不需要想動態添加腳本的方式那樣,我們自己去寫特性檢測代碼;
但由於是使用了XHR對象,所以不足之處是獲取這種資源有“域”的限制。資源 必須在同一個域下才可以,不可以跨域操作。
最後總結
文章主要從JavaScript的載入和執行這一過程中挖掘探討對前端優化的解決方案,並較細緻的羅列了各個解決方案的優勢和不足之處,當然,前端性能優化本就相對複雜,要想徹底理解其各中原由,還有很長一段路要走!
本文主要行文思路:
不得不說的JavaScript阻塞特性
合理放置腳本位置,以優化載入體驗,js腳本放在 <body>標簽閉合之前。
減少HTTP請求,壓縮精簡腳本代碼。
無阻塞載入JavaScript腳本:
使用<script>標簽的defer屬性。
使用HTML5的async屬性。
動態創建<script>元素載入JavaScript。
使用XHR對象載入JavaScript。
最後,由於個人水平原因,若有行文不全或疏漏錯誤之處,懇請各位讀者批評指正,一路有你,不勝感激!。
感謝這個時代,讓我們可以站在巨人的肩膀上,窺探程式世界的巨集偉壯觀,我願以一顆赤子心,踏遍程式世界的千山萬水!願每一個行走在程式世界的同仁,都活成心中想要的樣子,加油!