每個參與過開發(fā)企業(yè)級web應(yīng)用的前端工程師或許都曾思考過前端性能優(yōu)化方面的問題,。我們有雅虎14條性能優(yōu)化原則,,還有兩本很經(jīng)典的性能優(yōu)化指導(dǎo)書:《高性能網(wǎng)站建設(shè)指南》,、《高性能網(wǎng)站建設(shè)進階指南》,。經(jīng)驗豐富的工程師對于前端性能優(yōu)化方法耳濡目染,,基本都能一一列舉出來,。這些性能優(yōu)化原則大概是在7年前提出的,,對于web性能優(yōu)化至今都有非常重要的指導(dǎo)意義,。
然而,,對于構(gòu)建大型web應(yīng)用的團隊來說,要堅持貫徹這些優(yōu)化原則并不是一件十分容易的事,。因為優(yōu)化原則中很多要求是與工程管理相違背的,,比如“把css放在頭部”和“把js放在尾部”這兩條原則,我們不能讓團隊的工程師在寫樣式和腳本引用的時候都去修改一個相同的頁面文件,。這樣做會嚴重影響團隊成員間并行開發(fā)的效率,,尤其是在團隊有版本管理的情況下,每天要花大量的時間進行代碼修改合并,,這項成本是難以接受的,。因此在前端工程界,總會看到周期性的性能優(yōu)化工作,,辛勤的前端工程師們每到月圓之夜就會傾巢出動根據(jù)優(yōu)化原則做一次性能優(yōu)化,。
本文從一個全新的視角來思考web性能優(yōu)化與前端工程之間的關(guān)系,通過解讀百度前端集成解決方案小組(F.I.S)在打造高性能前端架構(gòu)并統(tǒng)一百度40多條前端產(chǎn)品線的過程中所經(jīng)歷的技術(shù)嘗試,,揭示前端性能優(yōu)化在前端架構(gòu)及開發(fā)工具設(shè)計層面的實現(xiàn)思路,。
性能優(yōu)化原則及分類
筆者先假設(shè)本文的讀者是有前端開發(fā)經(jīng)驗的工程師,并對企業(yè)級web應(yīng)用開發(fā)及性能優(yōu)化有一定的思考,,因此我不會重復(fù)介紹雅虎14條性能優(yōu)化原則,。如果您沒有這些前續(xù)知識,請移步這里來學(xué)習(xí),。
首先,,我們把雅虎14條優(yōu)化原則,《高性能網(wǎng)站建設(shè)指南》以及《高性能網(wǎng)站建設(shè)進階指南》中提到的優(yōu)化點做一次梳理,,按照優(yōu)化方向分類,,可以得到這樣一張表格:
優(yōu)化方向 |
優(yōu)化手段 |
請求數(shù)量 |
合并腳本和樣式表,CSS Sprites,,拆分初始化負載,,劃分主域 |
請求帶寬 |
開啟GZip,精簡JavaScript,,移除重復(fù)腳本,,圖像優(yōu)化 |
緩存利用 |
使用CDN,使用外部JavaScript和CSS,,添加Expires頭,,減少DNS查找,配置ETag,,使AjaX可緩存 |
頁面結(jié)構(gòu) |
將樣式表放在頂部,,將腳本放在底部,盡早刷新文檔的輸出 |
代碼校驗 |
避免CSS表達式,,避免重定向 |
表格1 性能優(yōu)化原則分類
目前大多數(shù)前端團隊可以利用yui compressor或者google closure compiler等壓縮工具很容易做到“精簡Javascript”這條原則,;同樣的,也可以使用圖片壓縮工具對圖像進行壓縮,,實現(xiàn)“圖像優(yōu)化”原則,。這兩條原則是對單個資源的處理,因此不會引起任何工程方面的問題,。很多團隊也通過引入代碼校驗流程來確保實現(xiàn)“避免css表達式”和“避免重定向”原則,。目前絕大多數(shù)互聯(lián)網(wǎng)公司也已經(jīng)開啟了服務(wù)端的Gzip壓縮,并使用CDN實現(xiàn)靜態(tài)資源的緩存和快速訪問,;一些技術(shù)實力雄厚的前端團隊甚至研發(fā)出了自動CSS Sprites工具,,解決了CSS Sprites在工程維護方面的難題。使用“查找-替換”思路,,我們似乎也可以很好的實現(xiàn)“劃分主域”原則,。
我們把以上這些已經(jīng)成熟應(yīng)用到實際生產(chǎn)中的優(yōu)化手段去除掉,留下那些還沒有很好實現(xiàn)的優(yōu)化原則,。再來回顧一下之前的性能優(yōu)化分類:
優(yōu)化方向
優(yōu)化手段 |
請求數(shù)量 |
合并腳本和樣式表,,拆分初始化負載 |
請求帶寬 |
移除重復(fù)腳本 |
緩存利用 |
添加Expires頭,配置ETag,,使Ajax可緩存 |
頁面結(jié)構(gòu) |
將樣式表放在頂部,,將腳本放在底部,盡早刷新文檔的輸出 |
表格2 較難實現(xiàn)的優(yōu)化原則
現(xiàn)在有很多頂尖的前端團隊可以將上述還剩下的優(yōu)化原則也都一一解決,,但業(yè)界大多數(shù)團隊都還沒能很好的解決這些問題,。因此,本文將就這些原則的解決方案做進一步的分析與講解,,從而為那些還沒有進入前端工業(yè)化開發(fā)的團隊提供一些基礎(chǔ)技術(shù)建設(shè)意見,,也借此機會與業(yè)界頂尖的前端團隊在工業(yè)化工程化方向上交流一下彼此的心得。
靜態(tài)資源版本更新與緩存
如表格2所示,,“緩存利用”分類中保留了“添加Expires頭”和“配置ETag”兩項,。或許有些人會質(zhì)疑,,明明這兩項只要配置了服務(wù)器的相關(guān)選項就可以實現(xiàn),,為什么說它們難以解決呢?確實,,開啟這兩項很容易,,但開啟了緩存后,,我們的項目就開始面臨另一個挑戰(zhàn):如何更新這些緩存。
相信大多數(shù)團隊也找到了類似的答案,,它和《高性能網(wǎng)站建設(shè)指南》關(guān)于“添加Expires頭”所說的原則一樣——修訂文件名,。即:
最有效的解決方案是修改其所有鏈接,這樣,,全新的請求將從原始服務(wù)器下載最新的內(nèi)容,。
思路沒錯,但要怎么改變鏈接呢,?變成什么樣的鏈接才能有效更新緩存,,又能最大限度避免那些沒有修改過的文件緩存不失效呢?
先來看看現(xiàn)在一般前端團隊的做法:
或者
大家會采用添加query的形式修改鏈接,。這樣做是比較直觀的解決方案,,但在訪問量較大的網(wǎng)站,這么做可能將面臨一些新的問題,。
通常一個大型的web應(yīng)用幾乎每天都會有迭代和更新,,發(fā)布新版本也就是發(fā)布新的靜態(tài)資源和頁面的過程。以上述代碼為例,,假設(shè)現(xiàn)在線上運行著index.html文件,,并且使用了線上的a.js資源。index.html的內(nèi)容為:
這次我們更新了頁面中的一些內(nèi)容,,得到一個index.html文件,,并開發(fā)了新的與之匹配的a.js資源來完成頁面交互,新的index.html文件的內(nèi)容因此而變成了:
好了,,現(xiàn)在要開始將兩份新的文件發(fā)布到線上去,。可以看到,,index.html和a.js的資源實際上是要覆蓋線上的同名文件的,。不管怎樣,在發(fā)布的過程中,,index.html和a.js總有一個先后的順序,,從而中間出現(xiàn)一段或大或小的時間間隔。對于一個大型互聯(lián)網(wǎng)應(yīng)用來說即使在一個很小的時間間隔內(nèi),,都有可能出現(xiàn)新用戶訪問,。在這個時間間隔中,訪問了網(wǎng)站的用戶會發(fā)生什么情況呢,?
- 如果先覆蓋index.html,,后覆蓋a.js,用戶在這個時間間隙訪問,會得到新的index.html配合舊的a.js的情況,,從而出現(xiàn)錯誤的頁面,。
- 如果先覆蓋a.js,后覆蓋index.html,,用戶在這個間隙訪問,,會得到舊的index.html配合新的a.js的情況,,從而也出現(xiàn)了錯誤的頁面,。
這就是為什么大型web應(yīng)用在版本上線的過程中經(jīng)常會較集中的出現(xiàn)前端報錯日志的原因,也是一些互聯(lián)網(wǎng)公司選擇加班到半夜等待訪問低峰期再上線的原因之一,。此外,,由于靜態(tài)資源文件版本更新是“覆蓋式”的,而頁面需要通過修改query來更新,,對于使用CDN緩存的web產(chǎn)品來說,,還可能面臨CDN緩存攻擊的問題。我們再來觀察一下前面說的版本更新手段:
我們不難預(yù)測,,a.js的下一個版本是“1.0.1”,,那么就可以刻意構(gòu)造一串這樣的請求“a.js?v=1.0.1”、“a.js?v=1.0.2”,、……讓CDN將當(dāng)前的資源緩存為“未來的版本”,。這樣當(dāng)這個頁面所用的資源有更新時,即使更改了鏈接地址,,也會因為CDN的原因返回給用戶舊版本的靜態(tài)資源,,從而造成頁面錯誤。即便不是刻意制造的攻擊,,在上線間隙出現(xiàn)訪問也可能導(dǎo)致區(qū)域性的CDN緩存錯誤,。
此外,當(dāng)版本有更新時,,修改所有引用鏈接也是一件與工程管理相悖的事,,至少我們需要一個可以“查找-替換”的工具來自動化的解決版本號修改的問題。
對付這個問題,,目前來說最優(yōu)方案就是基于文件內(nèi)容的hash版本冗余機制了,。也就是說,我們希望工程師源碼是這么寫的:
:
但是線上代碼是這樣的:
其中”_82244e91”這串字符是根據(jù)a.js的文件內(nèi)容進行hash運算得到的,,只有文件內(nèi)容發(fā)生變化了才會有更改,。由于版本序列是與文件名寫在一起的,而不是同名文件覆蓋,,因此不會出現(xiàn)上述說的那些問題,。同時,這么做還有其他的好處:
- 線上的a.js不是同名文件覆蓋,而是文件名+hash的冗余,,所以可以先上線靜態(tài)資源,,再上線html頁面,不存在間隙問題,;
- 遇到問題回滾版本的時候,,無需回滾a.js,只須回滾頁面即可,;
- 由于靜態(tài)資源版本號是文件內(nèi)容的hash,,因此所有靜態(tài)資源可以開啟永久強緩存,只有更新了內(nèi)容的文件才會緩存失效,,緩存利用率大增,;
- 修改靜態(tài)資源后會在線上產(chǎn)生新的文件,一個文件對應(yīng)一個版本,,因此不會受到構(gòu)造CDN緩存形式的攻擊
雖然這種方案是相比之下最完美的解決方案,,但它無法通過手工的形式來維護,因為要依靠手工的形式來計算和替換hash值,,并生成相應(yīng)的文件,。這將是一項非常繁瑣且容易出錯的工作,因此我們需要借助工具,。我們下面來了解一下fis是如何完成這項工作的,。
首先,之所以有這種工具需求,,完全是由web應(yīng)用運行的根本機制決定的:web應(yīng)用所需的資源是以字面的形式通知瀏覽器下載而聚合在一起運行的,。這種資源加載策略使得web應(yīng)用從本質(zhì)上區(qū)別于傳統(tǒng)桌面應(yīng)用的版本更新方式。為了實現(xiàn)資源定位的字面量替換操作,,前端構(gòu)建工具理論上需要識別所有資源定位的標記,,其中包括:
- css中的@import url(path)、background:url(path),、backgournd-image:url(path),、filter中的src
- js中的自定義資源定位函數(shù),在fis中我們將其規(guī)定為__uri(path),。
- html中的<script src=”path”>,、<link href=”path”>、<imgsrc=”path”>,、已經(jīng)embed,、audio、video,、object等具有資源加載功能的標簽,。
為了工程上的維護方便,,我們希望工程師在源碼中寫的是相對路徑,而工具可以將其替換為線上的絕對路徑,,從而避免相對路徑定位錯誤的問題(比如js中需要定位圖片路徑時不能使用相對路徑的情況),。
fis的資源定位設(shè)計思想
fis有一個非常棒的資源定位系統(tǒng),它是根據(jù)用戶自己的配置來指定資源發(fā)布后的地址,,然后由fis的資源定位系統(tǒng)識別文件中的定位標記,,計算內(nèi)容hash,并根據(jù)配置替換為上線后的絕對url路徑,。
要想實現(xiàn)具備hash版本生成功能的構(gòu)建工具不是“查找-替換”這么簡單的,。我們考慮這樣一種情況:
資源引用關(guān)系
由于我們的資源版本號是通過對文件內(nèi)容進行hash運算得到,如上圖所示,,index.html中引用的a.css文件的內(nèi)容其實也包含了a.png的hash運算結(jié)果,,因此我們在修改index.html中a.css的引用時,,不能直接計算a.css的內(nèi)容hash,,而是要先計算出a.png的內(nèi)容hash,替換a.css中的引用,,得到了a.css的最終內(nèi)容,,再做hash運算,最后替換index.html中的引用,。
這意味著構(gòu)建工具需要具備“遞歸編譯”的能力,,這也是為什么fis團隊不得不放棄gruntjs等task-based系統(tǒng)的根本原因。針對前端項目的構(gòu)建工具必須是具備遞歸處理能力的,。此外,,由于文件之間的交叉引用等原因,fis構(gòu)建工具還實現(xiàn)了構(gòu)建緩存等機制,,以提升構(gòu)建速度,。
在解決了基于內(nèi)容hash的版本更新問題之后,我們可以將所有前端靜態(tài)資源開啟永久強緩存,,每次版本發(fā)布都可以首先讓靜態(tài)資源全量上線,,再進一步上線模板或者頁面文件,再也不用擔(dān)心各種緩存和時間間隙的問題了,!
在本系列的下一部分,,我們將介紹靜態(tài)資源管理與模板框架的思路和用法。