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

重複優化的思考

這段時間對項目作了一次總體的優化,全站有了20%左右的提高(原本載入速度已經1.2S左右了,優化度很低),算一算已經作了四輪的全站性能優化了,回顧幾回的優化手段,基本上幾個字就能說清楚:javascript

傳輸層面:減小請求數,下降請求量
執行層面:減小重繪&迴流

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

① 網頁自上而下的解析渲染,邊解析邊渲染,頁面內CSS文件會阻塞渲染,異步CSS文件會致使迴流html

② 瀏覽器在document下載結束會檢測靜態資源,新開線程下載(有併發上限),在帶寬限制的條件下,無序併發會致使主資源速度降低,從而影響首屏渲染前端

③ 瀏覽器緩存可用時會使用緩存資源,這個時候能夠避免請求體的傳輸,對性能有極大提升java

衡量性能的重要指標爲首屏載入速度(指頁面能夠看見,不必定可交互),影響首屏的最大因素爲請求,因此請求是頁面真正的殺手,通常來講咱們會作這些優化:webpack

減小請求數

① 合併樣式、腳本文件git

② 合併背景圖片github

③ CSS3圖標、Icon Fontweb

下降請求量

① 開啓GZipajax

② 優化靜態資源,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級別的拆分與頁面業務組件的拆分,便能很好的應付樣式升級的需求,這方面冗餘只要能避過,其它冗餘問題便不是問題了,有兩個規範最好遵照:

1 避免使用全局的業務類樣式,就算他建議你使用2 避免不經過接口直接操做DOM

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

資源加載

解決冗餘便拋開了歷史的包袱,是前端優化的第一步也是比較難的一步,但模塊拆分也將全站分紅了不少小的模塊,載入的資源分散會增長請求數;若是所有合併,會致使首屏加載不須要的資源,也會致使下一個頁面不能使用緩存,如何作出合理的入口資源加載規則,如何合理的善用緩存,是前端優化的第二步。

通過幾回性能優化對比,得出了一個較優的首屏資源加載方案:

① 核心框架層:mvc骨架、異步模塊加載器(require&seajs)、工具庫(zepto、underscore、延遲加載)、數據請求模塊、核心依賴UI(header組件消息類組件)

② 業務公共模塊:入口文件(require配置,初始化工做、業務公共模塊)

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

④ 全局css資源

這裏若是追求極致,libs.js、main.css與main.js能夠選擇合併,劃分結束後即可以決定靜態資源緩存策略了。

資源緩存

資源緩存是爲二次請求加速,比較經常使用的緩存技術有:

① 瀏覽器緩存

② localstorage緩存

③ application緩存

application緩存更新一塊很差把握容易出問題,因此更多的是依賴瀏覽器以及localstorage,首先說下瀏覽器級別的緩存。

時間戳更新

只要服務器配置,瀏覽器自己便具備緩存機制,若是要使用瀏覽器機制做緩存,勢必關心一個什麼時候更新資源問題,咱們通常是這樣作的:

<script type="text/javascript" src="libs.js?t=20151025"></script>

這樣作要求必須先發布js文件,才能發佈html文件,不然讀不到最新靜態文件的。一個比較尷尬的場景是libs.js是框架團隊甚至第三方公司維護的,和業務團隊的index.html是兩個團隊,互相的發佈是沒有關聯的,因此這二者的發佈順序是不能保證的,因而轉向開始使用了MD5的方式。

MD5時代

爲了解決以上問題咱們開始使用md5碼的方式爲靜態資源命名:

<script type="text/javascript" src="libs_md5_1234.js"></script>

每次框架更新便不作文件覆蓋,直接生成一個惟一的文件名作增量發佈,這個時候若是框架先發布,待業務發佈時便已經存在了最新的代碼;當業務先發布框架沒有新的時,便繼續沿用老的文件,一切都很美好,雖然業務開發偶爾會抱怨每次都要向框架拿MD5映射,直到框架一次BUG發生。

seed.js時代

忽然一天框架發現一個全局性BUG,而且立刻作出了修復,業務團隊也立刻發佈上線,但這種事情出現第二次、第三次框架這邊便壓力大了,這個時候框架層面但願業務只須要引用一個不帶緩存的seed.js,seed.js要怎麼加載是他本身的事情:

<script type="text/javascript" src="seed.js"></script>
//seed.js須要按需加載的資源<script src="libs_md5.js"></script><script src="main_md5.js"></script>

固然,因爲js加載是順序是不可控的,咱們須要爲seed.js實現一個最簡單的順序加載模塊,映射什麼的由構建工具完成,每次作覆蓋發佈便可,這樣作的缺點是額外增長一個seed.js的文件,而且要承擔模塊加載代碼的下載量。

localstorage緩存

也會有團隊將靜態資源緩存至localstorage中,以期作離線應用,可是我通常用它存json數據,沒有作過靜態資源的存儲,想要嘗試的朋友必定要作好資源更新的策略,而後localstorage的讀寫也有必定損耗,不支持的狀況還須要作降級處理,這裏便很少介紹。

Hybrid載入

若是是Hybrid的話,狀況有所不一樣,須要將公共資源打包至native中,業務類就不要打包了,不然native會愈來愈大。

服務器資源合併

以前與淘寶的一些朋友作過交流,發現他們竟然作到了零散資源在服務器端作合併的地步了......這方面咱們仍是望洋興嘆吧

工程化&前端優化

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

重複工做;如通用的流程控制機制,可擴展的UI組件、靈活的工具方法
重複優化;如下降框架層面升級帶給業務團隊的耗損、幫助業務在無感知狀況下作掉大部分優化(好比打包壓縮什麼的)
開發效率;如幫助業務團隊寫可維護的代碼、讓業務團隊方便的調試代碼(好比Hybrid調試)

構建工具

要完成前端工程化,少不了工程化工具,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還有一款工具爲分析渲染而生:

1 Show paint rectangles 顯示繪製矩形2 Show composited layer borders 顯示層的組合邊界3 Show FPS meter 顯示FPS幀頻4 Enable continuous page repainting 開啓持續繪製模式 並 檢測頁面繪製時間5 Show potential scroll bottlenecks 顯示潛在的滾動瓶頸。

show paint rectangles

開啓矩形框,便會有綠色的框將頁面中不一樣的元素框起來,若是頁面渲染便會整塊加深,舉個例子:

當點擊+號時,三塊區域產生了重繪,這裏也能夠看出,每次重繪都會影響一個塊級(Layer),連帶反應會影響周邊元素,因此一次mask全局遮蓋層的出現會致使頁面級重繪,好比這裏的loading與toast便有所不一樣:

loading因爲遮蓋mask的出現而產生了全局重繪,而toast自己是絕對定位元素隻影響了局部,這裏有一個須要注意的是,由於loading轉圈的動畫是CSS3實現的,雖然不停的再動,事實上只渲染了一次,若是採用javascript的話,便會不停重繪。

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

結合Timeline的渲染圖

若是這裏取消掉fixed元素的話:

這裏fixed元素支付工具欄滾動時候是綠的,可是一樣是fixed的header卻沒有變綠,那是由於header多了一個css屬性:

.cm-header {
    -webkit-transform: translate3d(0,0,0);
    transform: translate3d(0,0,0);
}

這個屬性會建立獨立的Layer,有效的下降了fixed屬性的性能損耗,若是header去掉此屬性的話,就不同了:

show composited layer borders

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

使用該工具能夠查看當前頁面Layer構成,這裏的+號以及header都是有本身獨立的圖層的,緣由是使用了:

transform: translate3d(-50%,-50%,0);

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

Layer的建立會消耗額外的資源,因此不能不加節制的使用,以上面的「+」來講,若是使用icon font效果也許更好。

由於渲染這個東西比較底層,須要對瀏覽器層面的瞭解更多,關於更多更全的渲染相關知識,推薦閱讀我好友的博客:

http://www.ghugo.com/

結語

今天咱們站在工程化的層面總結了前幾回性能優化的一些方法,以期在後續的項目開發中能直接繞過這些性能的問題。

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