每一個參與過開發企業級web應用的前端工程師或許都曾思考過前端性能優化方面的問題。咱們有雅虎14條性能優化原則,還有兩本很經典的性能優化指導書:《高性能網站建設指南》、《高性能網站建設進階指南》。經驗豐富的工程師對於前端性能優化方法耳濡目染,基本都能一一列舉出來。這些性能優化原則大概是在7年前提出的,對於web性能優化至今都有很是重要的指導意義。css
然而,對於構建大型web應用的團隊來講,要堅持貫徹這些優化原則並非一件十分容易的事。由於優化原則中不少要求是與工程管理相違背的,好比「把css放在頭部」和「把js放在尾部」這兩條原則,咱們不能讓團隊的工程師在寫樣式和腳本引用的時候都去修改一個相同的頁面文件。這樣作會嚴重影響團隊成員間並行開發的效率,尤爲是在團隊有版本管理的狀況下,天天要花大量的時間進行代碼修改合併,這項成本是難以接受的。所以在前端工程界,總會看到週期性的性能優化工做,辛勤的前端工程師們每到月圓之夜就會傾巢出動根據優化原則作一次性能優化。html
本文從一個全新的視角來思考web性能優化與前端工程之間的關係,經過解讀百度前端集成解決方案小組(F.I.S)在打造高性能前端架構並統一百度40多條前端產品線的過程當中所經歷的技術嘗試,揭示前端性能優化在前端架構及開發工具設計層面的實現思路。前端
筆者先假設本文的讀者是有前端開發經驗的工程師,並對企業級web應用開發及性能優化有必定的思考,所以我不會重複介紹雅虎14條性能優化原則。若是您沒有這些前續知識,請移步這裏來學習。git
首先,咱們把雅虎14條優化原則,《高性能網站建設指南》以及《高性能網站建設進階指南》中提到的優化點作一次梳理,按照優化方向分類,能夠獲得這樣一張表格:github
優化方向web |
優化手段瀏覽器 |
請求數量緩存 |
合併腳本和樣式表,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總有一個前後的順序,從而中間出現一段或大或小的時間間隔。對於一個大型互聯網應用來講即便在一個很小的時間間隔內,都有可能出現新用戶訪問。在這個時間間隔中,訪問了網站的用戶會發生什麼狀況呢?
這就是爲何大型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運算獲得的,只有文件內容發生變化了纔會有更改。因爲版本序列是與文件名寫在一塊兒的,而不是同名文件覆蓋,所以不會出現上述說的那些問題。同時,這麼作還有其餘的好處:
雖然這種方案是相比之下最完美的解決方案,但它沒法經過手工的形式來維護,由於要依靠手工的形式來計算和替換hash值,並生成相應的文件。這將是一項很是繁瑣且容易出錯的工做,所以咱們須要藉助工具。咱們下面來了解一下fis是如何完成這項工做的。
首先,之因此有這種工具需求,徹底是由web應用運行的根本機制決定的:web應用所需的資源是以字面的形式通知瀏覽器下載而聚合在一塊兒運行的。這種資源加載策略使得web應用從本質上區別於傳統桌面應用的版本更新方式。爲了實現資源定位的字面量替換操做,前端構建工具理論上須要識別全部資源定位的標記,其中包括:
爲了工程上的維護方便,咱們但願工程師在源碼中寫的是相對路徑,而工具能夠將其替換爲線上的絕對路徑,從而避免相對路徑定位錯誤的問題(好比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的版本更新問題以後,咱們能夠將全部前端靜態資源開啓永久強緩存,每次版本發佈均可以首先讓靜態資源全量上線,再進一步上線模板或者頁面文件,不再用擔憂各類緩存和時間間隙的問題了!