前端優化帶來的思考,淺談前端工程化

葉小釵發表於2015-10-28

重複優化的思考

這段時間對專案做了一次整體的優化,全站有了20%左右的提升(本來載入速度已經1.2S左右了,優化度很低),算一算已經做了四輪的全站效能優化了,回顧幾次的優化手段,基本上幾個字就能說清楚:

傳輸層面的從來都是優化的核心點,而這個層面的優化要對瀏覽器有一個基本的認識,比如:

① 網頁自上而下的解析渲染,邊解析邊渲染,頁面內CSS檔案會阻塞渲染,非同步CSS檔案會導致迴流

② 瀏覽器在document下載結束會檢測靜態資源,新開執行緒下載(有併發上限),在頻寬限制的條件下,無序併發會導致主資源速度下降,從而影響首屏渲染

③ 瀏覽器快取可用時會使用快取資源,這個時候可以避免請求體的傳輸,對效能有極大提高

衡量效能的重要指標為首屏載入速度(指頁面可以看見,不一定可互動),影響首屏的最大因素為請求,所以請求是頁面真正的殺手,一般來說我們會做這些優化:

減少請求數

① 合併樣式、指令碼檔案

② 合併背景圖片

③ CSS3圖示、Icon Font

降低請求量

① 開啟GZip

② 優化靜態資源,jQuery->Zepto、閹割IScroll、去除冗餘程式碼

③ 圖片無失真壓縮

④ 圖片延遲載入

⑤ 減少Cookie攜帶

很多時候,我們也會採用類似“時間換空間、空間換時間”的做法,比如:

① 快取為王,對更新較緩慢的資源&介面做快取(瀏覽器快取、localsorage、application cache這個坑多)

② 按需載入,先載入主要資源,其餘資源延遲載入,對非首屏資源滾動載入

③ fake頁技術,將頁面最初需要顯示Html&Css內聯,在頁面所需資源載入結束前至少可看,理想情況是index.html下載結束即展示(2G 5S內)

④ CDN

……

從工程的角度來看,上述優化點半數以上是重複的,一般在釋出時候就直接使用專案構建工具做掉了,還有一些只是簡單的伺服器配置,開發時不需要關注。

可以看到,我們所做的優化都是在減少請求數,降低請求量,減小傳輸時的耗時,或者通過一個策略,優先載入首屏渲染所需資源,而後再載入互動所需資源(比如點選時候再載入UI元件),Hybrid APP這方面應該儘可能多的將公共靜態資源放在native中,比如第三方庫,框架,UI甚至城市列表這種常用業務資料。

攔路虎

有一些網站初期比較快,但是隨著量的積累,BUG越來越多,速度也越來越慢,一些前端會使用上述優化手段做優化,但是收效甚微,一個比較典型的例子就是程式碼冗餘:

① 之前的CSS全部放在了一個檔案中,新一輪的UI樣式優化,新老CSS難以拆分,CSS體量會增加,如果有業務團隊使用了公共樣式,情況更不容樂觀;

② UI元件更新,但是如果有業務團隊脫離介面操作了元件DOM,將導致新元件DOM更新受限,最差的情況下,使用者會載入兩個元件的程式碼;

③ 胡亂使用第三方庫、元件,導致頁面載入大量無用程式碼;

……

以上問題會不同程度的增加資源下載體量,如果聽之任之會產生一系列工程問題:

① 頁面關係錯綜複雜,需求迭代容易出BUG;

② 框架每次升級都會導致額外的請求量,常載入一些業務不需要的程式碼;

③ 第三方庫氾濫,且難以維護,有BUG也改不了;

④ 業務程式碼載入大量非同步模組資源,頁面請求數增多;

……

為求快速佔領市場,業務開發時間往往緊迫,使用框架級的HTML&CSS、繞過CSS Sprite使用背景圖片、引入第三方工具庫或者UI,會經常發生。當遇到效能瓶頸時,如果不從根源解決問題,用傳統的優化手段做頁面級別的優化,會出現很快頁面又被玩壞的情況,幾次優化結束後我也在思考一個問題:

工程問題在專案積累到一定量後可能會發生,一般來說會有幾個現象預示著工程問題出現了:

① 程式碼編寫&除錯困難

② 業務程式碼不好維護

③ 網站效能普遍不好

④ 效能問題重複出現,並且有不可修復之勢

像上面所描述情況,就是一個典型的工程問題;定位問題、發現問題、解決問題是我們處理問題的手段;而如何防止同一型別的問題重複發生,便是工程化需要做的事情,簡單說來,優化是解決問題,工程化是避免問題,今天我們就站在工程化的角度來解決一些前端優化問題,防止其死灰復燃。

文中是我個人的一些開發經驗,希望對各位有用,也希望各位多多支援討論,指出文中不足以及提出您的一些建議

消滅冗餘

我們這裡做的第一個事情便是消除優化路上第一個攔路虎:程式碼冗餘(做程式碼精簡),單從一個頁面的載入來說,他需要以下資源:

① 框架MVC骨架模組&框架級別CSS

② UI元件(header元件、日曆、彈出層、訊息框……)

③ 業務HTML骨架

④ 業務CSS

⑤ 業務Javascript程式碼

⑥ 服務介面服務

因為產品&視覺會經常折騰全站樣式加之UI的靈活性,UI最容易產生冗餘的模組。

UI元件

UI元件本身包括完整的HTML&CSS&Javascript,一個複雜的元件下載量可以達到10K以上,就UI部分來說容易導致兩個工程化問題:

① 升級產生程式碼冗餘

② 對外介面變化導致業務升級需要額外開發

UI升級

最理想的升級是保持對外的介面不變甚至保持DOM結構不變,但多數情況的UI升級其實是UI重做,最壞的情況是不做老介面相容,這個時候業務同事便需要修改程式碼。為了防止業務抱怨,UI製作者往往會保留兩個元件(UI+UI1),如果原來那個UI是核心依賴元件(比如是UIHeader元件),便會直接打包至核心框架包中,這時便出現了新老元件共存的局面,這種情況是必須避免的,UI升級需要遵守兩個原則:

① 核心依賴元件必須保持單一,相同功能的核心元件只能有一個

② 元件升級必須做介面相容,新的特性可以做加法,絕不允許對介面做減法

UI組成

專案之初,分層較好的團隊會有一個公共的CSS檔案(main.css),一個業務CSS檔案,main.css包含公共的CSS,並且會包含所有的UI的樣式:

半年後業務頻道增,UI元件需求一多便容易膨脹,弊端馬上便暴露出來了,最初main.css可能只有10K,但是不出半年就會膨脹至100K,而每個業務頻道一開始便需要載入這100K的樣式檔案頁面,但是其中多數的UI樣式是首屏載入用不到的。

所以比較好的做法是,main.css只包含最核心的樣式,理想情況是什麼業務樣式功能皆不要提供,各個UI元件的樣式打包至UI中按需載入:

如此UI拆分後,main.css總是處於最基礎的樣式部分,而UI使用時按需載入,就算出現兩個相同元件也不會導致多下載資源。

拆分頁面

一個PC業務頁面,其模組是很複雜的,這個時候可以將之分為多個模組:

一經拆分後,頁面便是由業務元件組成,只需要關注各個業務元件的開發,然後在主控制器中組裝業務元件,這樣主控制器對頁面的控制力度會增加。

業務元件一般重用性較低,會產生模組間的業務耦合,還會對業務資料產生依賴,但是主體仍然是HTML&CSS&Javascript,這部分程式碼也是經常導致冗餘的,如果能按模組拆分,可以很好的控制這一問題發生:

按照上述的做法現在的載入規則是:

① 公共樣式檔案

② 框架檔案,業務入口檔案

③ 入口檔案,非同步載入業務模組,模組內再非同步載入其它資源

這樣下來業務開發時便不需要引用樣式檔案,可以最大限度的提升首屏載入速度;需要關注的一點是,當非同步拉取模組時,內部的CSS載入需要一個規則避免對其它模組的影響,因為模組都帶有樣式屬性,頁面迴流、頁面閃爍問題需要關注。

一個實際的例子是,這裡點選出發後的城市列表便是一個完整的業務元件,城市選擇的資源是在點選後才會發生請求,而業務元件內部又會細分小模組,再細分的資源控制由實際業務情況決定,過於細分也會導致理解和程式碼編寫難度上升:

demo演示地址程式碼地址

如果哪天需求方需要用新的城市選擇元件,便可以直接重新開發,讓業務之間使用最新的城市列表即可,因為是獨立的資源,所以老的也是可以使用的。

只要能做到UI級別的拆分與頁面業務元件的拆分,便能很好的應付樣式升級的需求,這方面冗餘只要能避過,其它冗餘問題便不是問題了,有兩個規範最好遵守:

冗餘是首屏載入速度最大的攔路虎,是歷史形成的包袱,只要能消除冗餘,便能在後面的路走的更順暢,這種元件化程式設計的方法也能讓網站後續的維護更加簡單。

資源載入

解決冗餘便拋開了歷史的包袱,是前端優化的第一步也是比較難的一步,但模組拆分也將全站分成了很多小的模組,載入的資源分散會增加請求數;如果全部合併,會導致首屏載入不需要的資源,也會導致下一個頁面不能使用快取,如何做出合理的入口資源載入規則,如何合理的善用快取,是前端優化的第二步。

經過幾次效能優化對比,得出了一個較優的首屏資源載入方案:

① 核心框架層:mvc骨架、非同步模組載入器(require&seajs)、工具庫(zepto、underscore、延遲載入)、資料請求模組、核心依賴UI(header元件訊息類元件)

② 業務公共模組:入口檔案(require配置,初始化工作、業務公共模組)

③ 獨立的page.js資源(包含template、css),會按需載入獨立UI資源

④ 全域性css資源

這裡如果追求極致,libs.js、main.css與main.js可以選擇合併,劃分結束後便可以決定靜態資源快取策略了。

資源快取

資源快取是為二次請求加速,比較常用的快取技術有:

① 瀏覽器快取

② localstorage快取

③ application快取

application快取更新一塊不好把握容易出問題,所以更多的是依賴瀏覽器以及localstorage,首先說下瀏覽器級別的快取。

時間戳更新

只要伺服器配置,瀏覽器本身便具有快取機制,如果要使用瀏覽器機制作快取,勢必關心一個何時更新資源問題,我們一般是這樣做的:

每次框架更新便不做檔案覆蓋,直接生成一個唯一的檔名做增量釋出,這個時候如果框架先發布,待業務釋出時便已經存在了最新的程式碼;當業務先發布框架沒有新的時,便繼續沿用老的檔案,一切都很美好,雖然業務開發偶爾會抱怨每次都要向框架拿MD5對映,直到框架一次BUG發生。

seed.js時代

突然一天框架發現一個全域性性BUG,並且馬上做出了修復,業務團隊也馬上釋出上線,但這種事情出現第二次、第三次框架這邊便壓力大了,這個時候框架層面希望業務只需要引用一個不帶快取的seed.js,seed.js要怎麼載入是他自己的事情:

當然,由於js載入是順序是不可控的,我們需要為seed.js實現一個最簡單的順序載入模組,對映什麼的由構建工具完成,每次做覆蓋釋出即可,這樣做的缺點是額外增加一個seed.js的檔案,並且要承擔模組載入程式碼的下載量。

localstorage快取

也會有團隊將靜態資源快取至localstorage中,以期做離線應用,但是我一般用它存json資料,沒有做過靜態資源的儲存,想要嘗試的朋友一定要做好資源更新的策略,然後localstorage的讀寫也有一定損耗,不支援的情況還需要做降級處理,這裡便不多介紹。

Hybrid載入

如果是Hybrid的話,情況有所不同,需要將公共資源打包至native中,業務類就不要打包了,否則native會越來越大。

伺服器資源合併

之前與淘寶的一些朋友做過交流,發現他們居然做到了零散資源在伺服器端做合併的地步了……這方面我們還是望洋興嘆吧

工程化&前端優化

所謂工程化,可以簡單認為是將框架的職責拓寬再拓寬,主旨是幫業務團隊更好的完成需求,工程化會預測一些常碰到的問題,將之扼殺在搖籃,而這種路徑是可重用的,是具有可持續性的,比如第一個優化去除冗餘,是在多次去除冗餘程式碼,思考冗餘出現的原因而最終思考得出的一個避免冗餘的方案,前端工程化需要考慮以下問題:

構建工具

要完成前端工程化,少不了工程化工具,requireJS與grunt的出現,改變了業界前端程式碼的編寫習慣,同時他們也是推動前端工程化的一個基礎。

requireJS是一偉大的模組載入器,他的出現讓javascript製作多人維護的大型專案變成了事實;grunt是一款javascript構建工具,主要完成壓縮、合併、圖片壓縮合並等一系列工作,後續又出了yeoman、Gulp、webpack等構建工具。

這裡這裡要記住一件事情,我們的目的是完成前端工程化,無論什麼模組載入器或者構建工具,都是為了幫助我們完成目的,工具不重要,目的與思想才重要,所以在完成工程化前討論哪個載入器好,哪種構建工具好是捨本逐末的。

理想的載入速度

現在站在前端優化的角度,以下面這個頁面為例,最優的載入情況是什麼呢:

以這個看似簡單頁面來說,如果要完整的展示涉及的模組比較多:

① 框架MVC骨架模組&框架級別CSS

② 幾個UI元件(header元件、日曆、彈出層、訊息框……)

③ 業務HTML骨架

④ 業務CSS

⑤ 業務Javascript程式碼

⑥ 服務介面服務

上面的很多資源事實上對於首屏渲染是沒有幫助的,根據之前的探討,得出的理想首屏載入所需資源是:

① 框架MVC骨架&框架級別CSS => main.css+libs.js

② 業務入口檔案 => main.js

③ 業務互動控制器 => page.js

有了這些資源,便能完成完整的互動,包括介面請求,列表展示,但若是隻需要給使用者“看見”首頁,便能採用一種fake的手段,只需要這些資源:

① 業務HTML骨架 => 最簡單的index.hrml載入

② 內嵌CSS

這個時候,頁面一旦下載結束便可完成渲染,在其它資源載入結束後再將頁面重新渲染即可,很多時候前端優化要做的就是靠近這種理想載入速度,解決那些制約的因素,比如:

CSS Sprite

由於現代瀏覽器的與解析機制,在拿到一個頁面的時候馬上會分析其靜態資源,然後開執行緒做下載,這個時候反而會影響首屏渲染,如圖(模擬2G):

如果做fake頁優化的話,便需要將樣式也做非同步載入,這樣document載入結束便能渲染頁面,2G情況都能3S內可見頁面,大大避免白屏時間,而後面的單個背景圖片便是需要解決的工程問題。

CSS Sprite旨在降低請求數,但是與去處冗餘問題一樣,半年後一個CSS Sprite資源反而不好維護,容易爛掉,grunt有一外掛支援將圖片自動合併為CSS Sprite,而他也會自動替換頁面中的背景地址,只需要按規則操作即可。

PS:其它構建工具也會有,各位自己找下吧

CSS Sprite構建工具:https://www.npmjs.com/package/grunt-css-sprite

正確的使用該工具便可以使業務開發擺脫圖片合併帶來的痛苦,當然一些弊端需要去克服,比如在小屏手機使用小屏背景,大屏手機使用大屏背景的處理辦法

其它工程化的體現

又比如,前端模板是將html檔案解析為function函式,這一步驟完全可以在釋出階段,將html模板轉換為function函式,免去了生產環境的大量正則替換,效率高還省電;

然後ajax介面資料的快取也直接在資料請求底層做掉,讓業務輕鬆實現介面資料快取;

而一些html壓縮、預載入技術、延遲載入技術等優化點便不一一展開……

渲染優化

當請求資源落地後便是瀏覽器的渲染工作了,每一次操作皆可能引起瀏覽器的重繪,在PC瀏覽器上,渲染對效能影響不大,但因為配置原因,渲染對移動端效能的影響卻非常大,錯誤的操作可能導致滾動遲鈍、動畫卡幀,大大降低使用者體驗。

減少重繪、減少迴流降低渲染帶來的耗損基本人盡皆知了,但是引起重繪的操作何其多,每次重繪的操作又何其微觀:

① 頁面滾動

② javascript互動

③ 動畫

④ 內容變化

⑤ 屬性計算(求元素的高寬)

……

與請求優化不同的是,一些請求是可以避免的,但是重繪基本是不可避免的,而如果一個頁面卡了,這麼多可能引起重繪的操作,如何定位到渲染瓶頸在何處,如何減少這種大消耗的效能影響是真正應該關心的問題。

Chrome渲染分析工具

工程化其中要解決的一個問題是程式碼除錯問題,以前端開發來說Chrome以及Fiddler在這方面已經做的非常好了,這裡就使用Chrome來檢視一下頁面的渲染。

Timeline工具

timeline可以展示web應用載入過程中的資源消耗情況,包括處理DOM事件,頁面佈局渲染以及繪製元素,通過該工具基本可以找到頁面存在的渲染問題。

Timeline使用4種顏色表示不同的事件:

以上圖為例,因為重新整理了頁面,會載入幾個完整的js檔案,所以js執行耗時必然會多,但也在50ms左右就結束了。

Rendering工具

Chrome還有一款工具為分析渲染而生:

show paint rectangles

開啟矩形框,便會有綠色的框將頁面中不同的元素框起來,如果頁面渲染便會整塊加深,舉個例子:

當點選+號時,三塊區域產生了重繪,這裡也可以看出,每次重繪都會影響一個塊級(Layer),連帶反應會影響周邊元素,所以一次mask全域性遮蓋層的出現會導致頁面級重繪,比如這裡的loading與toast便有所不同:

loading由於遮蓋mask的出現而產生了全域性重繪,而toast本身是絕對定位元素隻影響了區域性,這裡有一個需要注意的是,因為loading轉圈的動畫是CSS3實現的,雖然不停的再動,事實上只渲染了一次,如果採用javascript的話,便會不停重繪。

然後當頁面發生滾動時,下面的支付工具條一直呈綠色狀態,意思是滾動時一直在重繪,這個重繪的頻率很高,這也是fixed元素相當耗費效能的原因:

結合Timeline的渲染圖

如果這裡取消掉fixed元素的話:

這裡fixed元素支付工具欄滾動時候是綠的,但是同樣是fixed的header卻沒有變綠,那是因為header多了一個css屬性:

這個屬性會建立獨立的Layer,有效的降低了fixed屬性的效能損耗,如果header去掉此屬性的話,就不一樣了:

show composited layer borders

顯示組合層邊界,是因為頁面是由多個圖層組成,勾上後頁面便開始分塊了:

使用該工具可以檢視當前頁面Layer構成,這裡的+號以及header都是有自己獨立的圖層的,原因是使用了:

Layer存在的意義在於可以讓頁面最優的方式繪製,這個是CSS3硬體加速的祕密,就如header一樣,形成Layer的元素繪製會有所不同。

Layer的建立會消耗額外的資源,所以不能不加節制的使用,以上面的“+”來說,如果使用icon font效果也許更好。

因為渲染這個東西比較底層,需要對瀏覽器層面的瞭解更多,關於更多更全的渲染相關知識,推薦閱讀我好友的部落格:

http://www.ghugo.com/

結語

今天我們站在工程化的層面總結了前幾次效能優化的一些方法,以期在後續的專案開發中能直接繞過這些效能的問題。

前端優化僅僅是前端工程化中的一環,結合之前的程式碼開發效率探討(【元件化開發】前端進階篇之如何編寫可維護可升級的程式碼),後續我們會在前端工具的製作使用、前端監控等環節做更多的工作,期望更大的提升前端開發的效率,推動前端工程化的程式。

本文關聯的程式碼:

https://github.com/yexiaochai/mvc

相關文章