每個參與過開發企業級web應用的前端工程師或許都曾思考過前端性能優化方面的問題。我們有雅虎14條性能優化原則,還有兩本很經典的性能優化指導書:《高性能網站建設指南》、《高性能網站建設進階指南》。經驗豐富的工程師對于前端性能優化方法耳濡目染,基本都能一一列舉出來。這些性能優化原則大概是在7年前提出的,對于web性能優化至今都有非常重要的指導意義。
然而,對于構建大型web應用的團隊來說,要堅持貫徹這些優化原則并不是一件十分容易的事。因為優化原則中很多要求是與工程管理相違背的,比如 把css放在頭部 和 把js放在尾部 這兩條原則,我們不能讓團隊的工程師在寫樣式和腳本引用的時候都去修改一個相同的頁面文件。這樣做會嚴重影響團隊成員間并行開發的效率,尤其是在團隊有版本管理的情況下,每天要花大量的時間進行代碼修改合并,這項成本是難以接受的。因此在前端工程界,總會看到周期性的性能優化工作,辛勤的前端工程師們每到月圓之夜就會傾巢出動根據優化原則做一次性能優化。
# 性能優化是一個工程問題
本文將從一個全新的視角來思考web性能優化與前端工程之間的關系,揭示前端性能優化在前端架構及開發工具設計層面的實現思路。
性能優化原則及分類
po主先假設本文的讀者是有前端開發經驗的工程師,并對企業級web應用開發及性能優化有一定的思考,因此我不會重復介紹雅虎14條性能優化原則。如果您沒有這些前續知識,請移步 這里 來學習。
首先,我們把雅虎14條優化原則,《高性能網站建設指南》以及《高性能網站建設進階指南》中提到的優化點做一次梳理,按照優化方向分類,可以得到這樣一張表格:
優化方向 優化手段
請求數量 合并腳本和樣式表,CSS Sprites,拆分初始化負載,劃分主域
請求帶寬 開啟GZip,精簡JavaScript,移除重復腳本,圖像優化
緩存利用 使用CDN,使用外部JavaScript和CSS,添加Expires頭,
減少DNS查找,配置ETag,使AjaX可緩存
頁面結構 將樣式表放在頂部,將腳本放在底部,盡早刷新文檔的輸出
代碼校驗 避免CSS表達式,避免重定向
目前大多數前端團隊可以利用 yui compressor 或者 google closure compiler 等壓縮工具很容易做到 精簡Javascript這條原則;同樣的,也可以使用圖片壓縮工具對圖像進行壓縮,實現 圖像優化 原則。這兩條原則是對單個資源的處理,因此不會引起任何工程方面的問題。很多團隊也通過引入代碼校驗流程來確保實現 避免css表達式 和 避免重定向 原則。目前絕大多數互聯網公司也已經開啟了服務端的Gzip壓縮,并使用CDN實現靜態資源的緩存和快速訪問;一些技術實力雄厚的前端團隊甚至研發出了自動CSS Sprites工具,解決了CSS Sprites在工程維護方面的難題。使用“查找-替換”思路,我們似乎也可以很好的實現 劃分主域 原則。
我們把以上這些已經成熟應用到實際生產中的優化手段去除掉,留下那些還沒有很好實現的優化原則。再來回顧一下之前的性能優化分類:
優化方向 優化手段
請求數量 合并腳本和樣式表,拆分初始化負載
請求帶寬 移除重復腳本
緩存利用 添加Expires頭,配置ETag,使Ajax可緩存
頁面結構 將樣式表放在頂部,將腳本放在底部,盡早刷新文檔的輸出
代碼校驗 避免CSS表達式,避免重定向
有很多頂尖的前端團隊可以將上述還剩下的優化原則也都一一解決,但業界大多數團隊都還沒能很好的解決這些問題。因此,本文將就這些原則的解決方案做進一步的分析與講解,從而為那些還沒有進入前端工業化開發的團隊提供一些基礎技術建設意見,也借此機會與業界頂尖的前端團隊在工業化工程化方向上交流一下彼此的心得。
靜態資源版本更新與緩存
緩存利用 分類中保留了 添加Expires頭 和 配置ETag 兩項。或許有些人會質疑,明明這兩項只要配置了服務器的相關選項就可以實現,為什么說它們難以解決呢?確實,開啟這兩項很容易,但開啟了緩存后,我們的項目就開始面臨另一個挑戰: 如何更新這些緩存?
相信大多數團隊也找到了類似的答案,它和《高性能網站建設指南》關于“添加Expires頭”所說的原則一樣——修訂文件名。即:
最有效的解決方案是修改其所有鏈接,這樣,全新的請求將從原始服務器下載最新的內容。
思路沒錯,但要怎么改變鏈接呢?變成什么樣的鏈接才能有效更新緩存,又能最大限度避免那些沒有修改過的文件緩存不失效呢?
先來看看現在一般前端團隊的做法:
ps: 也有團隊采用構建版本號為靜態資源請求添加query,它們在本質上是沒有區別的,在此就不贅述了。
接下來,項目升級,比如頁面上的html結構發生變化,對應還要修改 a.js 這個文件,得到的構建結果如下:
為了觸發用戶瀏覽器的緩存更新,我們需要更改靜態資源的url地址,如果采用構建信息(時間戳、版本號等)作為url修改的依據,如上述代碼所示,我們只修改了一個a.js文件,但再次構建會讓所有請求都更改了url地址,用戶再度訪問頁面那些沒有修改過的靜態資源的(b.js,b.js,c.js,d.js,e.js)的瀏覽器緩存也一同失效了。
使用構建信息作為靜態資源更新標記會導致每次構建發布后所有靜態資源都被迫更新,瀏覽器緩存利用率降低,給性能帶來傷害。
此外,采用添加query的方式來清除緩存還有一個弊端,就是 覆蓋式發布 的上線問題。
采用query更新緩存的方式實際上要覆蓋線上文件的,index.html和a.js總有一個先后的順序,從而中間出現一段或大或小的時間間隔。尤其是當頁面是后端渲染的模板的時候,靜態資源和模板是部署在不同的機器集群上的,上線的過程中,靜態資源和頁面文件的部署時間間隔可能會非常長,對于一個大型互聯網應用來說即使在一個很小的時間間隔內,都有可能出現新用戶訪問。在這個時間間隔中,訪問了網站的用戶會發生什么情況呢?
如果先覆蓋index.html,后覆蓋a.js,用戶在這個時間間隙訪問,會得到新的index.html配合舊的a.js的情況,從而出現錯誤的頁面。
如果先覆蓋a.js,后覆蓋index.html,用戶在這個間隙訪問,會得到舊的index.html配合新的a.js的情況,從而也出現了錯誤的頁面。
這就是為什么大型web應用在版本上線的過程中經常會較集中的出現前端報錯日志的原因,也是一些互聯網公司選擇加班到半夜等待訪問低峰期再上線的原因之一。
對于靜態資源緩存更新的問題,目前來說最優方案就是 基于文件內容的hash版本冗余機制 了。也就是說,我們希望項目源碼是這么寫的:
<script type="text/javascript" src="a.js"></script>
發布后代碼變成
<script type="text/javascript" src="a_8244e91.js"></script>
也就是a.js發布出來后被修改了文件名,產生一個新文件,并不是覆蓋已有文件。其中”_82244e91”這串字符是根據a.js的文件內容進行hash運算得到的,只有文件內容發生變化了才會有更改。由于將文件發布為帶有hash的新文件,而不是同名文件覆蓋,因此不會出現上述說的那些問題。同時,這么做還有其他的好處:
上線的a.js不是同名文件覆蓋,而是文件名+hash的冗余,所以可以先上線靜態資源,再上線html頁面,不存在間隙問題;
遇到問題回滾版本的時候,無需回滾a.js,只須回滾頁面即可;
由于靜態資源版本號是文件內容的hash,因此所有靜態資源可以開啟永久強緩存,只有更新了內容的文件才會緩存失效,緩存利用率大增;
以文件內容的hash值為依據生產新文件的非覆蓋式發布策略是解決靜態資源緩存更新最有效的手段。
雖然這種方案是相比之下最完美的解決方案,但它無法通過手工的形式來維護,因為要依靠手工的形式來計算和替換hash值,并生成相應的文件,將是一項非常繁瑣且容易出錯的工作,因此我們需要借助工具來處理。
用grunt來實現md5功能是非常困難的,因為grunt只是一個task管理器,而md5計算需要構建工具具有遞歸編譯的能,而不是簡單的任務調度。考慮這樣的例子:
由于我們的資源版本號是通過對文件內容進行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中的引用。
計算index.html中引用的a.css文件的url過程:
1. 壓縮a.png后計算其內容的md5值
2. 將a.png的md5寫入a.css,再壓縮a.css,計算其內容的md5值
3. 將a.css的md5值寫入到index.html中
grunt等task-based的工具是很難在task之間協作處理這樣的需求的。
在解決了基于內容hash的版本更新問題之后,我們可以將所有前端靜態資源開啟永久強緩存,每次版本發布都可以首先讓靜態資源全量上線,再進一步上線模板或者頁面文件,再也不用擔心各種緩存和時間間隙的問題了!
來源:http://div.io/topic/371
- PHP技術文章
- PHP中session和cookie的區別
- php設計模式(一):簡介及創建型模式
- php設計模式結構型模式
- Php設計模式(三):行為型模式
- 十款最出色的 PHP 安全開發庫中文詳細介紹
- 12個提問頻率最高的PHP面試題
- PHP 語言需要避免的 10 大誤區
- PHP 死鎖問題分析
- 致PHP路上的“年輕人”
- PHP網站常見安全漏洞,及相應防范措施總結
- 各開源框架使用與設計總結(一)
- 數據庫的本質、概念及其應用實踐(二)
- PHP導出MySQL數據到Excel文件(fputcsv)
- PHP中14種排序算法評測
- 深入理解PHP原理之--echo的實現
- PHP性能分析相關的函數
- PHP 性能分析10則
- 10 位頂級 PHP 大師的開發原則
- 30條爆笑的程序員梗 PHP是最好的語言
- PHP底層的運行機制與原理
- PHP 性能分析與實驗——性能的宏觀分析
- PHP7 性能翻倍關鍵大揭露
- 鳥哥:寫在PHP7發布之際一些話
- PHP與MySQL通訊那點事
- Php session內部執行流程的再次剖析
- 關于 PHP 中的 Class 的幾點個人看法
- PHP Socket 編程過程詳解
- PHP過往及現在及變革
- PHP吉祥物大象的由來
- PHP生成靜態頁面的方法
- 吊炸天的 PHP 7 ,你值得擁有!
- PHP開發中文件操作疑難問答
- MongoDB PHP Driver的連接處理解析
- PHP 雜談《重構-改善既有代碼的設計》之二 對象
- 在php中判斷一個請求是ajax請求還是普通請求的方法
- 使用HAProxy、PHP、Redis和MySQL支撐10億請求每周架構細節
- HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、Web Services 是什么?
- 重構-改善既有代碼的設計
- PHP場景中getshell防御思路分享
- 移動互聯時代,你看看除了PHP你還會些什么
- 安卓系統上搭建本地php服務器環境
- PHP中常見的緩存技術!
- PHP里10個鮮為人知但卻非常有用的函數
- 成為一名PHP專家其實并不難
- PHP 命令行?是的,您可以!
- PHP開發提高效率技巧
- PHP八大安全函數解析
- PHP實現四種基本排序算法
- PHP開發中的中文編碼問題
- php.get.post
- php發送get、post請求的6種方法簡明總結
- 中高級PHP開發者應該掌握哪些技術?
- 前端開發
- web前端知識體系大全
- 前端工程與性能優化(下)
- 前端工程與性能優化(上)
- 2016 年技術發展方向
- Web應用檢查清單
- 如何成為一名優秀的web前端工程師
- 前端組件化開發實踐
- 移動端H5頁面高清多屏適配方案
- 2015前端框架何去何從
- 從前端看“百度遷徙”的技術實現(一)
- 從前端看“百度遷徙”的技術實現(二)
- 前端路上的旅行
- 大公司里怎樣開發和部署前端代碼?
- 5個經典的前端面試問題
- 前端工程師新手必讀
- 手機淘寶前端的圖片相關工作流程梳理
- 一個自動化的前端項目實現(附源碼)
- 前端代碼異常日志收集與監控
- 15年雙11手淘前端技術總結 - H5性能最佳實踐
- 深入理解javascript原型和閉包系列
- 一切都是對象
- 函數和對象的關系
- prototype原型
- 隱式原型
- instanceof
- 繼承
- 原型的靈活性
- 簡述【執行上下文】上
- 簡述【執行上下文】下
- this
- 執行上下文棧
- 簡介【作用域】
- 【作用域】和【上下文環境】
- 從【自由變量】到【作用域鏈】
- 閉包
- 完結
- 補充:上下文環境和作用域的關系
- Linux私房菜