探真無阻塞載入javascript指令碼技術(1)
下面的圖片是我使用firefox和chrome瀏覽百度首頁時候記錄的http請求
下面是firefox:
下面是chrome:
在瀏覽百度首頁前我都將瀏覽器的緩衝全部清理掉,讓這個情境最接近第一次訪問百度首頁的情景。
在firefox的請求瀑布圖裡有個表現非常之明顯:就是javascript檔案下載完畢後,有一段時間是沒有網路請求被處理的,這段時間過後http請求才會接著執行,這段空閑時間就是所謂的http請求被阻塞。
瀏覽器裡的http請求被阻塞一般都是由javascript所引起,具體原因是javascript下載完畢之後會立即執行,而javascript執行時候會阻塞瀏覽器的其他行為,例如阻塞其他javascript的執行以及其他的http請求的執行。這樣會導致頁面載入變慢,如果這個變慢很明顯,此時使用者操作網頁會發現頁面沒有反應會反應很慢,慢是網站使用者體驗的夢魘。
我目前開發的一些系統,在開發環境裡經常碰到javascript阻塞頁面載入的問題,主要原因是我們網站很多靜態資源和指令碼都被獨立抽取在了一台單獨的靜態資源伺服器上,而本地的開發環境類比的靜態資源服務環境常常很不穩定經常宕機),有時一些建立的指令碼沒有及時更新到開發環境上,因此某些js指令檔無法正確擷取,這些問題導致頁面載入時候這些js指令碼就會阻塞頁面的載入,此時瀏覽器會反覆嘗試請求這些js檔案,直到請求逾時才會認定該指令碼的url無效,如果中途你無法忍受這種等待,強制關閉瀏覽器的請求,會發現在瀏覽器控制台裡很多指令碼都無法找到,這樣你就無法在控制台裡設定js代碼斷點調試js,如果等待js載入完畢,時間又會被浪費,無奈之下只要找到那些無效的url將其注釋掉,哎,結果好幾次都將有注釋的錯誤碼提交到了svn伺服器上,這些事情真是很惱人。
不管那種瀏覽器,也不管是新版本還是老版本的瀏覽器,它們都秉持瀏覽器的單線程特性,這個特性似乎是一個很難撼動的準則,當我們在瀏覽器的地址欄裡輸入一個url地址,訪問一個新頁面時候,頁面展示的快慢就是由一個單線程所控制,這個線程叫做UI線程,UI線程會根據頁面裡資源資源是指css檔案,圖片等等)書寫的先後順序來載入資源,載入資源也就是使用http請求擷取資源,像css外部檔案,html檔案以及圖片等資源http請求處理完畢也就意味著資源載入結束,但是像外部的javascript檔案則不同,它的載入過程被分為兩步,第一步和載入css檔案和圖片一樣,就是執行一個http請求下載外部的js檔案,但是javascript完成http操作後並不意味操作完畢,UI線程接著會執行它,如果你開發的頁面裡js代碼執行時間過長,那麼使用者就會明顯感覺到頁面的延遲。為什麼瀏覽器不能把javascript代碼的載入過程拆分為下載和執行兩個並行的過程,這樣就可以充分利用時間完成http請求,這樣不是就能提升頁面的載入效率了嗎?這個問題的答案當然是否定的,javascript是一個程式設計語言,js代碼是有智力的,它除了可以完成邏輯性的工作,還可以通過操作頁面元素來改變頁面UI效果,如果我們忽略javascript對UI的影響,讓它順延強制,結果會造成頁面展示的混亂。那麼他會產生什麼樣的混亂呢?這個混亂的描述如下:
最簡單最好理解和最好掌握的思路是線性思路,對於瀏覽器UI顯示要按線性思路理解即放在頁面前部的資源會優先載入和執行,瀏覽器還會認為前一步的內容都可能會是後一步頁面展示前提,如果瀏覽器擅自停止中間某個代碼的執行,很有可能頁面最終呈現的效果和設計者設計的不同,這樣我們就無法開發出正確的頁面。而且按線性思路當你碰到頁面UI效果出問題時候,你很容易定位問題所在,如果我們將js代碼的載入和執行分隔開來,這就好比把線性思路變成了樹狀結構,那麼你掌握頁面載入的思路和解決UI載入問題時候就會變得更加困難,到時很多人都會抓狂和思路混亂,所以我在這裡說混亂。
綜上所述,js指令碼下載和執行是一個完整的操作,是絕對不能被割裂的。
瀏覽器為了提升使用者體驗,加快UI線程的執行是一個無法迴避的問題,看來拆分js的下載和執行是不可行的,如是乎瀏覽器換了種方式,這個方式也就是在同一個時間能下載多個資源,我們再看,在同一個網域名稱下,firefox可以同時下載兩種圖片,chrome可以同時下載4個靜態資源,不過這是針對圖片和css檔案,對於js檔案似乎還是一個接著一個的下載,下載一個執行一個,不過在ie8以上版本,js可以和圖片一樣並行載入,ie這樣做就提升了js檔案下載的效率,不過到了js執行時候還是要嚴格按照順序執行。
多個http串連並行下載資源就好比多個線程共同完成某個任務,如果並行http串連更多,那麼能有更多http資源同時被下載,但是瀏覽器提供並存執行的http串連實在太少了,例如上面firefox才兩個,chrome也只有4個,那如何突破瀏覽器的串連個數的限制了?方法很簡單就是將常用的,穩定的靜態資源統一放在一個靜態資源伺服器上,由統一的網域名稱對外提供,這個網域名稱要和主體請求的網域名稱不一樣,原理是因為瀏覽器只通過網域名稱來限制串連的個數,如果一個頁面裡有兩個不同的域的,那麼並行的http請求個數也會變成兩倍。這個做法有點討瀏覽器的巧,是程式員發現瀏覽器的特點而總結出的技術,它類似一個hack技術,而hack技術不會是標準技術,所以它肯定有它的瓶頸區,所以這樣的技術都是會有個度的,瀏覽器限制請求個數絕對不是無緣無故的,我們看百度頁面並行下載圖片的http協議的版本都是1.1,http1.1特點就是長串連,長串連的好處是在頁面和服務端頻繁互動時候效率很好,但是瀏覽器的頁面操作並不是總是頻繁的請求伺服器,而為了載入靜態資源而建立很多長串連,伺服器會不得不維護大量無用的長串連,給伺服器的壓力是可想而知的。相比之下http1.0協議,它不使用長串連,而是短串連,因此早期版本的瀏覽器會給http1.0協議開啟的串連數要高於http1.1串連數,因此有些網站將靜態資源伺服器提供的http協議版本降低到1.0,這樣可以有效提升瀏覽器的並發串連數,這個做法會給網頁效能帶來意想不到的提升,不過現代的瀏覽器似乎更願意平等對待兩個不同版本的協議了,新版的瀏覽器有些將兩類協議的並發數變成一樣了。而對於處於用戶端地位的瀏覽器維護多個連結對於資源消耗是龐大的,而且網域名稱過多也會增加dns解析的開銷,所以並發串連開啟越多,並不一定真的會達到提升效能的目的,那麼多少個域最合適了?雅虎的前端工程師給了一個答案:2個是最佳的,這個資料怎麼得來的我就不太清楚了,不過結果很簡單很好用,記住就行了。
下面我就要聊聊如何解決js阻塞頁面載入的問題了,js之所以會阻塞UI線程的執行,是因為js能控制UI的展示,而頁面載入的規則是要順序執行,所以在碰到js代碼時候UI線程會首先執行它,而這點很多程式員不知道或者知道但被忽視,因此導致編寫代碼時候將用於展示的代碼和用於處理邏輯的代碼混淆在一起,這樣做的後果是使js代碼造成的阻塞更加嚴重,所以雅虎公司的前端團隊提出了一個前端最佳化的規則:將js指令碼放置到html文檔的末尾,這樣就能有效避免UI的阻塞。但是這個方法太簡單了,不利於我們對網站進行更加深入的最佳化。為了做的深入,我們要需要更進一步分析,首先我們知道js指令碼按出現的位置分為兩類一類是行內指令碼即寫在頁面裡的指令碼,一類是js的外部檔案,行內指令碼的最佳化比較簡單,就是盡量在頁面寫更少的代碼,就算要寫代碼也主要是控制頁面載入的UI顯示的代碼,沒必要的代碼就放在外部的js檔案裡,js外部檔案最佳化就比較複雜,為了精簡行內指令碼,我們就不得不將大量的js代碼放到外部檔案裡,早先時候我都會盡量將所有外部js檔案合并成一個js檔案,但是現在我發現一個複雜的外部指令碼很有可能會讓頁面的阻塞情況變得更加的嚴重,因此外部指令碼要根據其功能拆分為展示指令碼和邏輯指令碼,但是事實上展示代碼和邏輯代碼很難分離,其實有個更加簡單的標準讓我們拆分代碼:將所有外部代碼分為UI初始化代碼和其他代碼,,UI初始化代碼是在頁面載入時候執行的代碼,我們現在只要判斷哪些代碼在頁面載入時候執行就行了,這個標準就容易多了。
另外,上文我提到過我碰到頁面被js阻塞的情況,有時我為了調試js代碼會一直等待瀏覽器判斷無效的js載入失敗,那麼我是怎麼判斷瀏覽器已經判斷外部js載入無效了?很簡單就是查看瀏覽器忙指示結束,瀏覽器的忙指示如所示:
忙指示(忙指示現象包括:瀏覽器選項卡的旋轉圓圈,滑鼠變成漏鬥滑鼠,瀏覽器左下的狀態列顯示正在載入某某url以及老版的ie顯示頁面載入進度的現象)標記結束了,就表明頁面的載入操作結束了,為了防止js指令碼阻塞頁面載入,那麼我們要做到的就是讓那些不會用於頁面初始化展示的js代碼的載入和執行操作在瀏覽器忙指示結束後觸發,為了做到這一點我們就得知道忙指示結束後會觸發什麼命令,這個命令就是瀏覽器的onload事件,因此我們讓那些和頁面載入無關的js指令碼在onload方法裡執行,在onload事件裡我就會使用dom技術,構建script節點,設定它的src指向需要載入的指令碼路徑,然後將這個srcipt節點加入到html文檔的head裡,為了完全確保這個js在忙指示結束後執行,我使用setTimeout方法調用動態載入指令碼的方法,進一步確保代碼在忙指示結束後執行,實踐下來感覺效果的確不錯。
理解到這裡我本來很高興,認為自己又理解了一個前端開發的痛點,並且有一個好的解決方案,但是等我回味一下發現有點不對頭,我經常使用的jQuery定義了ready方法,ready方法會在dom載入完畢後執行,而我自己的方案卻是在onload後執行,代碼執行遠遠落後jQuery的ready方法執行時機,這個感覺讓我很不舒服,其次,在頁面開發裡我們會使用很多第三方庫,雖然我現在開發盡量做到只用jQuery這一個第三方庫,但是其他人則不盡然,他們會使用很多第三方庫,很多庫有大量UI操作的通用方法,這些方法非常好用,經常使用這些庫會導致我們自己寫的控制UI的js代碼常常會依賴它們,那麼拆分UI控制指令碼和其他指令碼就無從談起了。總之,現在web前端開發太依賴第三方庫,就算一個牛逼的前端團隊,拒絕使用第三方庫,什麼都自己開發,當web應用變複雜後,通用庫和業務代碼的耦合度都是很難解決的問題,這也會導致我們沒法真正將UI展示代碼和邏輯代碼真正的分離。
我的方案其實滿足不了實際生產的需求,不夠完美,所以本文到這裡沒有推匯出通用規則,真令人失望,面對上面的新問題那我們該怎麼辦了?這個問題不是無解的,現行技術就有它的解決方案,那就是無阻塞指令碼的載入。無阻塞載入指令碼技術的核心就是:載入js指令碼時候,被載入的js指令碼不會阻塞UI線程的執行和以阻塞方式載入的指令碼。