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

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
