每一個參與過開發企業級 web 應用的前端工程師或許都曾思考過前端性能優化方面的問題。咱們有雅虎 14 條性能優化原則,還有兩本很經典的性能優化指導書:《高性能網站建設指南》、《高性能網站建設進階指南》。經驗豐富的工程師對於前端性能優化方法耳濡目染,基本都能一一列舉出來。這些性能優化原則大概是在 7 年前提出的,對於 web 性能優化至今都有很是重要的指導意義。javascript
然而,對於構建大型 web 應用的團隊來講,要堅持貫徹這些優化原則並非一件十分容易的事。由於優化原則中不少要求與工程管理相違背,好比「把 css 放在頭部」和「把 js 放在尾部」這兩條原則,咱們不能讓整個團隊的工程師在寫樣式和腳本引用的時候都去修改同一份的頁面文件。這會嚴重影響團隊成員間並行開發的效率,尤爲是在團隊有版本管理的狀況下,天天要花大量的時間進行代碼修改合併,這項成本是難以接受的。所以在前端工程界,總會看到週期性的性能優化工做,辛勤的前端工程師們每到月圓之夜就會傾巢出動根據優化原則作一次最佳實踐。php
本文從一個全新的視角來思考 web 性能優化與前端工程之間的關係,經過解讀百度前端集成解決方案小組(F.I.S)在打造高性能前端架構並統一百度 40 多條前端產品線的過程當中所經歷的技術嘗試,揭示前端性能優化在前端架構及開發工具設計層面的實現思路。css
性能優化原則及分類html
筆者先假設本文的讀者是有前端開發經驗的工程師,並對企業級 web 應用開發及性能優化有必定的思考。所以我不會重複介紹雅虎 14 條性能優化原則,若是您沒有這些前續知識的,請移步這裏來學習。前端
首先,咱們把雅虎 14 條優化原則,《高性能網站建設指南》以及《高性能網站建設進階指南》中提到的優化點作一次梳理,若是按照優化方向分類能夠獲得這樣一張表格: java
優化方向 | 優化手段 |
---|---|
請求數量 | 合併腳本和樣式表,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 在工程維護方面的難題。使用「查找 - 替換」思路,咱們彷佛也能夠很好的實現「劃分主域」原則。python
咱們把以上這些已經成熟應用到實際生產中的優化手段去除掉,留下那些尚未很好實現的優化原則,再來回顧一下以前的性能優化分類: jquery
優化方向 | 優化手段 |
---|---|
請求數量 | 合併腳本和樣式表,拆分初始化負載 |
請求帶寬 | 移除重複腳本 |
緩存利用 | 添加 Expires 頭,配置 ETag,使 Ajax 可緩存 |
頁面結構 | 將樣式表放在頂部,將腳本放在底部,儘早刷新文檔的輸出 |
誠然,不能否認如今有不少頂尖的前端團隊能夠將上述還剩下的優化原則也都一一解決,但業界大多數團隊都還沒能很好的解決這些問題,所以接下來本文將就這些原則的解決方案作進一步的分析與講解,從而爲那些尚未進入前端工業化開發的團隊提供一些基礎技術建設意見,也藉此機會與業界頂尖的前端團隊在工業化工程化方向上交流一下彼此的心得。git
靜態資源版本更新與緩存github
如表格 2 所示,在「緩存利用」分類中保留了「添加 Expires 頭」和「配置 ETag 」兩項,或許有些人會質疑,明明這兩項只要配置了服務器的相關選項就能夠實現,爲何說它們難以解決呢?確實,開啓這兩項很容易,但開啓了緩存後,咱們的項目就開始面臨另外一個挑戰:如何更新這些緩存。
相信大多數團隊也找到了相似的答案,它和《高性能網站建設指南》關於「添加 Expires 頭」所說的原則同樣——修訂文件名。即:
思路沒錯,但要怎麼改變連接呢?變成什麼樣的連接纔能有效更新緩存,又能最大限度避免那些沒有修改過的文件緩存不失效呢?
先來看看如今通常前端團隊的作法:
<script type="text/javascript" src="a.js?t=20130825"></script>
或者
<script type="text/javascript" src="a.js?v=1.0.0"></script>
你們會採用添加 query 的形式修改連接。這樣作是比較直觀的解決方案,但在訪問量較大的網站,這麼作可能將面臨一些新的問題。
一般一個大型的 web 應用幾乎天天都會有迭代和更新,發佈新版本也就是發佈新的靜態資源和頁面的過程。以上述代碼爲例,假設如今線上運行着 index.html 文件,而且使用了線上的 a.js 資源。index.html 的內容爲:
<script type="text/javascript" src="a.js?v=1.0.0"></script>
此次咱們更新了頁面中的一些內容,獲得一個 index.html 文件,並開發了新的與之匹配的 a.js 資源來完成頁面交互,新的 index.html 文件的內容所以而變成了:
<script type="text/javascript" src="a.js?v=1.0.1"></script>
好了,如今要開始將兩份新的文件發佈到線上去。能夠看到,a.html 和 a.js 的資源其實是要覆蓋線上的同名文件的。無論怎樣,在發佈的過程當中,index.html 和 a.js 總有一個前後的順序,從而中間出現一段或大或小的時間間隔。對於一個大型互聯網應用來講即便在一個很小的時間間隔內,都有可能出現新用戶訪問,而在這個時間間隔中訪問了網站的用戶會發生什麼狀況呢:
這就是爲何大型 web 應用在版本上線的過程當中常常會較集中的出現前端報錯日誌的緣由,也是一些互聯網公司選擇加班到半夜等待訪問低峯期再上線的緣由之一。此外,因爲靜態資源文件版本更新是「覆蓋式」的,而頁面須要經過修改 query 來更新,對於使用 CDN 緩存的 web 產品來講,還可能面臨 CDN 緩存攻擊的問題。咱們再來觀察一下前面說的版本更新手段:
<script type="text/javascript" src="a.js?v=1.0.0"></script>
咱們不難預測,a.js 的下一個版本是「 1.0.1 」,那麼就能夠刻意構造一串這樣的請求「 a.js?v=1.0.1 」、「 a.js?v=1.0.2 」、……讓 CDN 將當前的資源緩存爲「將來的版本」。這樣當這個頁面所用的資源有更新時,即便更改了連接地址,也會由於 CDN 的緣由返回給用戶舊版本的靜態資源,從而形成頁面錯誤。即使不是刻意製造的攻擊,在上線間隙出現訪問也可能致使區域性的 CDN 緩存錯誤。
此外,當版本有更新時,修改全部引用連接也是一件與工程管理相悖的事,至少咱們須要一個能夠「查找 - 替換」的工具來自動化的解決版本號修改的問題。
對付這個問題,目前來講最優方案就是基於文件內容的 hash 版本冗餘機制 了。也就是說,咱們但願工程師源碼是這麼寫的:
<script type="text/javascript" src="a.js"></script>
可是線上代碼是這樣的:
<script type="text/javascript" src="a_8244e91.js"></script>
其中」_82244e91 」這串字符是根據 a.js 的文件內容進行 hash 運算獲得的,只有文件內容發生變化了纔會有更改。因爲版本序列是與文件名寫在一塊兒的,而不是同名文件覆蓋,所以不會出現上述說的那些問題。那麼這麼作都有哪些好處呢?
雖然這種方案是相比之下最完美的解決方案,但它沒法經過手工的形式來維護,由於要依靠手工的形式來計算和替換 hash 值並生成相應的文件將是一項很是繁瑣且容易出錯的工做。所以,咱們須要藉助工具。有了這樣的思路,咱們下面就來了解一下 fis 是如何完成這項工做的。
首先,之因此有這種工具需求,徹底是由於 web 應用運行的根本機制決定的:web 應用所需的資源是以字面的形式通知瀏覽器下載而聚合在一塊兒運行的。這種資源加載策略使得 web 應用從本質上區別於傳統桌面應用的版本更新方式,也是大型 web 應用須要工具處理的最根本緣由。爲了實現資源定位的字面量替換操做,前端構建工具理論上須要識別全部資源定位的標記,其中包括:
<script src=」 path 」>
、<link href=」 path 」>
、<img src=」 path 」>
、已經 embed、audio、video、object 等具備資源加載功能的標籤。爲了工程上的維護方便,咱們但願工程師在源碼中寫的是相對路徑,而工具能夠將其替換爲線上的絕對路徑,從而避免相對路徑定位錯誤的問題(好比 js 中須要定位圖片路徑時不能使用相對路徑的狀況)。
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 的版本更新問題以後,咱們能夠將全部前端靜態資源開啓永久強緩存,每次版本發佈均可以首先讓靜態資源全量上線,再進一步上線模板或者頁面文件,不再用擔憂各類緩存和時間間隙的問題了!
靜態資源管理與模板框架
讓咱們再來看看前面的優化原則表還剩些什麼:
優化方向 | 優化手段 |
---|---|
請求數量 | 合併腳本和樣式表,拆分初始化負載 |
請求帶寬 | 移除重複腳本 |
緩存利用 | 使 Ajax 可緩存 |
頁面結構 | 將樣式表放在頂部,將腳本放在底部,儘早刷新文檔的輸出 |
很不幸,剩下的優化原則都不是使用工具就能很好實現的。或許有人會辯駁:「我用某某工具能夠實現腳本和樣式表合併」。嗯,必須認可,使用工具進行資源合併並替換引用或許是一個不錯的辦法,但在大型 web 應用,這種方式有一些很是嚴重的缺陷,來看一個很熟悉的例子:
某個 web 產品頁面有 A、B、C 三個資源
工程師根據「減小 HTTP 請求」的優化原則合併了資源
產品經理要求 C 模塊按需出現,此時 C 資源已出現多餘的可能
C 模塊再也不須要了,註釋掉吧!但 C 資源一般不敢輕易剔除
不知不覺中,性能優化變成了性能惡化……
事實上,使用工具在線下進行靜態資源合併是沒法解決資源按需加載的問題的。若是解決不了按需加載,則勢必會致使資源的冗餘;此外,線下經過工具實現的資源合併一般會使得資源加載和使用的分離,好比在頁面頭部或配置文件中寫資源引用及合併信息,而用到這些資源的 html 組件寫在了頁面其餘地方,這種書寫方式在工程上很是容易引發維護不一樣步的問題,致使使用資源的代碼刪除了,引用資源的代碼卻還在的狀況。所以,在工業上要實現資源合併至少要知足以下需求:
將以上要求綜合考慮,不難發現,單純依靠前端技術或者工具處理的是很難達到這些理想要求的。現代大型 web 應用所展現的頁面絕大多數都是使用服務端動態語言拼接生成的。有的產品使用模板引擎,好比 smarty、velocity,有的則乾脆直接使用動態語言,好比 php、python。不管使用哪一種方式實現,前端工程師開發的 html 絕大多數最終都不是以靜態的 html 在線上運行的,接下來我會講述一種新的模板架構設計,用以實現前面說到那些性能優化原則,同時知足工程開發和維護的須要,這種架構設計的核心思想就是:
考慮一段這樣的頁面代碼:
<html> <head> <title>hello world</title> <link rel="stylesheet" type="text/css" href="A.css"> <link rel="stylesheet" type="text/css" href="B.css"> <link rel="stylesheet" type="text/css" href="C.css"> </head> <body> <div>html of A</div> <div>html of B</div> <div>html of C</div> </body> </html>
根據資源合併需求中的第二項,咱們但願資源引用與使用能儘可能靠近,這樣未來維護起來會更容易一些,所以,理想的源碼是:
<html> <head> <title>hello world</title> </head> <body> <link rel="stylesheet" type="text/css" href="A.css"><div>html of A</div> <link rel="stylesheet" type="text/css" href="B.css"><div>html of B</div> <link rel="stylesheet" type="text/css" href="C.css"><div>html of C</div> </body> </html>
固然,把這樣的頁面直接送達給瀏覽器用戶是會有嚴重的頁面閃爍問題的,因此咱們實際上仍然但願最終頁面輸出的結果仍是如最開始的截圖同樣,將 css 放在頭部輸出。這就意味着,頁面結構須要有一些調整,而且有能力收集資源加載需求,那麼咱們考慮一下這樣的源碼:
<html> <head> <title>hello world</title> <!--[CSS LINKS PLACEHOLDER]--> </head> <body> {require name="A.css"}<div>html of A</div> {require name="B.css"}<div>html of B</div> {require name="C.css"}<div>html of C</div> </body> </html>
在頁面的頭部插入一個 html 註釋「<!--[CSS LINKS PLACEHOLDER]-->
」做爲佔位,而將原來字面書寫的資源引用改爲模板接口(require)調用,該接口負責收集頁面所需資源。require 接口實現很是簡單,就是準備一個數組,收集資源引用,而且能夠去重。最後在頁面輸出的前一刻,咱們將 require 在運行時收集到的「 A.css 」、「 B.css 」、「 C.css 」三個資源拼接成 html 標籤,替換掉註釋佔位「<!--[CSS LINKS PLACEHOLDER]-->
」,從而獲得咱們須要的頁面結構。
通過 fis 團隊的總結,咱們發現模板層面只要實現三個開發接口,既能夠比較完美的實現目前遺留的大部分性能優化原則,這三個接口分別是:
實現了這些接口以後,一個重構後的模板頁面的源代碼可能看起來就是這樣的了:
<html> <head> <title>hello world</title> <!--[CSS LINKS PLACEHOLDER]--> {require name="jquery.js"} {require name="bootstrap.css"} </head> <body> {require name="A/A.css"}{widget name="A/A.tpl"} {script}console.log('A loaded'){/script} {require name="B/B.css"}{widget name="B/B.tpl"} {require name="C/C.css"}{widget name="C/C.tpl"} <!--[SCRIPTS PLACEHOLDER]--> </body> </html>
而最終在模板解析的過程當中,資源收集與去重、頁面 script 收集、佔位符替換操做,最終從服務端發送出來的 html 代碼爲:
<html> <head> <title>hello world</title> <link rel="stylesheet" type="text/css" href="bootstrap.css"> <link rel="stylesheet" type="text/css" href="A/A.css"> <link rel="stylesheet" type="text/css" href="B/B.css"> <link rel="stylesheet" type="text/css" href="C/C.css"> </head> <body> <div>html of A</div> <div>html of B</div> <div>html of C</div> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript">console.log('A loaded');</script> </body> </html>
不難看出,咱們目前已經實現了「按需加載」,「將腳本放在底部」,「將樣式表放在頭部」三項優化原則。
前面講到靜態資源在上線後須要添加 hash 戳做爲版本標識,那麼這種使用模板語言來收集的靜態資源該如何實現這項功能呢?答案是:靜態資源依賴關係表。假設前面講到的模板源代碼所對應的目錄結構爲下圖所示:
那麼咱們可使用工具掃描整個 project 目錄,而後建立一張資源表,同時記錄每一個資源的部署路徑,能夠獲得這樣的一張表:
{ "res": { "A/A.css": { "uri": "/A/A_1688c82.css", "type": "css" }, "B/B.css": { "uri": "/B/B_52923ed.css", "type": "css" }, "C/C.css": { "uri": "/C/C_6dda653.css", "type": "css" }, "bootstrap.css": { "uri": "bootstrap_08f2256.css", "type": "css" }, "jquery.js": { "uri": "jquery_9155343.css", "type": "js" }, }, "pkg": {} }
基於這張表,咱們就很容易實現 {require name=」 id 」} 這個模板接口了。只須查表便可。好比執行{require name=」 jquery.js 」},查表獲得它的 url 是「/jquery_9151577.js 」,聲明一個數組收集起來就行了。這樣,整個頁面執行完畢以後,收集資源加載需求,並替換頁面的佔位符,便可實現資源的 hash 定位,獲得:
<html> <head> <title>hello world</title> <link rel="stylesheet" type="text/css" href="bootstrap_08f2256.css"> <link rel="stylesheet" type="text/css" href="A/A_1688c82.css"> <link rel="stylesheet" type="text/css" href="B/B_52923ed.css"> <link rel="stylesheet" type="text/css" href="C/C_6dda653.css"> </head> <body> <div>html of A</div> <div>html of B</div> <div>html of C</div> <script type="text/javascript" src="jquery_9155343.js"></script> <script type="text/javascript">console.log('A loaded');</script> </body> </html>
接下來,咱們討論如何在基於表的設計思想上是如何實現靜態資源合併的。或許有些團隊使用過 combo 服務,也就是咱們在最終拼接生成頁面資源引用的時候,並非生成多個獨立的 link 標籤,而是將資源地址拼接成一個 url 路徑,請求一種線上的動態資源合併服務,從而實現減小 HTTP 請求的需求,好比:
<html> <head> <title>hello world</title> <link rel="stylesheet" type="text/css" href="/combo?files=bootstrap_08f2256.css,A/A_1688c82.css,B/B_52923ed.css,C/C_6dda653.css"> </head> <body> <div>html of A</div> <div>html of B</div> <div>html of C</div> <script type="text/javascript" src="jquery_9155343.js"></script> <script type="text/javascript">console.log('A loaded');</script> </body> </html>
這個「/combo?files=file1,file2,file3,…」的 url 請求響應就是動態 combo 服務提供的,它的原理很簡單,就是根據 get 請求的 files 參數找到對應的多個文件,合併成一個文件來響應請求,並將其緩存,以加快訪問速度。
這種方法很巧妙,有些服務器甚至直接集成了這類模塊來方便的開啓此項服務,這種作法也是大多數大型 web 應用的資源合併作法。但它也存在一些缺陷:
對於上述第二條缺陷,能夠舉個例子來看說明:
很明顯,若是 combo 服務能聰明的知道 A 頁面使用的資源引用爲「/combo?files=a,b 」和「/combo?files=c,d 」,而 B 頁面使用的資源引用爲「/combo?files=a,b 」,「/combo?files=e,f 」就行了。這樣當用戶在訪問 A 頁面以後再訪問 B 頁面時,只須要下載 B 頁面的第二個 combo 文件便可,第一個文件已經在訪問 A 頁面時緩存好了的。
基於這樣的思考,fis 在資源表上新增了一個字段,取名爲「 pkg 」,就是資源合併生成的新資源,表的結構會變成:
{ "res": { "A/A.css": { "uri": "/A/A_1688c82.css", "type": "css" }, "B/B.css": { "uri": "/B/B_52923ed.css", "type": "css" }, "C/C.css": { "uri": "/C/C_6dda653.css", "type": "css" }, "bootstrap.css": { "uri": "bootstrap_08f2256.css", "type": "css" }, "jquery.js": { "uri": "jquery_9155343.css", "type": "js" }, }, "pkg": { "p0": { "uri": "/pkg/utils_b967346.css", "type": "css", "has": ["bootstrap.css", "A/A.css"] }, "p1": { "uri": "/pkg/others_0d4552a.css", "type": "css", "has": ["B/B.css", "C/C.css"] } } }
相比以前的表,能夠看到新表中多了一個 pkg 字段,而且記錄了打包後的文件所包含的獨立資源。這樣,咱們從新設計一下{require name=」 id 」}這個模板接口:在查表的時候,若是一個靜態資源有 pkg 字段,那麼就去加載 pkg 字段所指向的打包文件,不然加載資源自己。好比執行{require name=」 bootstrap.css 」},查表得知 bootstrap.css 被打包在了「 p0 」中,所以取出 p0 包的 url 「/pkg/utils_b967346.css 」,而且記錄頁面已加載了「 bootstrap.css 」和「 A/A.css 」兩個資源。這樣一來,以前的模板代碼執行以後獲得的 html 就變成了:
<html> <head> <title>hello world</title> <link rel="stylesheet" type="text/css" href="pkg/utils_b967346.css"> <link rel="stylesheet" type="text/css" href="pkg/others_0d4552a.css"> </head> <body> <div>html of A</div> <div>html of B</div> <div>html of C</div> <script type="text/javascript" src="jquery_9155343.js"></script> <script type="text/javascript">console.log('A loaded');</script> </body> </html>
css 資源請求數由原來的 4 個減小爲 2 個。這樣的打包結果是怎麼來的呢?答案是配置獲得的。咱們來看一下帶有打包結果的資源表的 fis 配置:
fis.config.set('pack', { 'pkg/util.css': [ 'bootstrap.css', 'A/A.css'], 'pkg/other.css': [ '**.css' ] });
咱們將「 bootstrap.css 」、「 A/A.css 」打包在一塊兒,其餘 css 另外打包,從而生成兩個打包文件,當頁面須要打包文件中的資源時,模塊框架就會收集並計算出最優的資源加載結果,從而解決靜態資源合併的問題。
這樣作的緣由是爲了彌補 combo 在前面講到的兩點技術上的不足而設計的。但也不難發現這種打包策略是須要配置的,這就意味着維護成本的增長。但好在它有兩個優點能夠必定程度上彌補這個問題:
關於第二點,fis 有這樣輔助系統來支持自適應打包算法:
至此,咱們經過基於表的靜態資源管理系統和三個模板接口實現了幾個重要的性能優化原則,如今咱們再來回顧一下前面的性能優化原則分類表,剔除掉已經作到了的,看看還剩下哪些沒作到的:
優化方向 | 優化手段 |
---|---|
請求數量 | 拆分初始化負載 |
請求帶寬 | 拆分初始化負載 |
緩存利用 | 使 Ajax 可緩存 |
頁面結構 | 儘早刷新文檔的輸出 |
「拆分初始化負載」的目標是將頁面一開始加載時不須要執行的資源從全部資源中分離出來,等到須要的時候再加載。工程師一般沒有耐心去區分資源的分類狀況,但咱們能夠利用組件化框架接口來幫助工程師管理資源的使用。仍是從例子開始思考:
<html> <head> <title>hello world</title> {require name="jquery.js"} </head> <body> <button id="myBtn">Click Me</button> {script} $('#myBtn').click(function(){ var dialog = require('dialog/dialog.js'); dialog.alert('you catch me!'); }); {/script} <!--[SCRIPTS PLACEHOLDER]--> </body> </html>
在 fis 給百度內部團隊開發的架構中,若是這樣書寫代碼,頁面最終的執行結果會變成:
<html> <head> <title>hello world</title> </head> <body> <button id="myBtn">Click Me</button> <script type="text/javascript" src="/jquery_9151577.js"></script> <script type="text/javascript" src="/dialog/dialog_ae8c228.js"></script> <script type="text/javascript"> $('#myBtn').click(function(){ var dialog = require('dialog/dialog.js'); dialog.alert('you catch me!'); }); </script> <!--[SCRIPTS PLACEHOLDER]--> </body> </html>
fis 系統會分析頁面中 require(id)函數的調用,並將依賴關係記錄到資源表對應資源的 deps 字段中,從而在頁面渲染查表時能夠加載依賴的資源。但此時 dialog.js 是以 script 標籤的形式同步加載的,這樣會在頁面初始化時出現資源的浪費。所以,fis 團隊提供了 require.async 的接口,用於異步加載一些資源,源碼修改成:
<html> <head> <title>hello world</title> {require name="jquery.js"} </head> <body> <button id="myBtn">Click Me</button> {script} $('#myBtn').click(function() { require.async('dialog/dialog.js', function( dialog ) { dialog.alert('you catch me!'); }); }); {/script} <!--[SCRIPTS PLACEHOLDER]--> </body> </html>
這樣書寫以後,fis 系統會在表裏以 async 字段來標準資源依賴關係是異步的。fis 提供的靜態資源管理系統會將頁面輸出的結果修改成:
<html> <head> <title>hello world</title> </head> <body> <button id="myBtn">Click Me</button> <script type="text/javascript" src="/jquery_9151577.js"></script> <script type="text/javascript" src="/dialog/dialog_ae8c228.js"></script> <script type="text/javascript"> $('#myBtn').click(function() { require.async('dialog/dialog.js', function( dialog ) { dialog.alert('you catch me!'); }); }); </script> <!--[SCRIPTS PLACEHOLDER]--> </body> </html>
dialog.js 不會在頁面以 script src 的形式輸出,而是變成了資源註冊,這樣,當頁面點擊按鈕觸發 require.async 執行的時候,async 函數纔會查表找到資源的 url 並加載它,加載完畢後觸發回調函數。
到目前爲止,咱們又以架構的形式實現了一項優化原則(拆分初始化負載),回顧咱們的優化分類表,如今僅有兩項沒能作到了:
優化方向 | 優化手段 |
---|---|
緩存利用 | 使 Ajax 可緩存 |
頁面結構 | 儘早刷新文檔的輸出 |
剩下的兩項優化原則要作到並不容易,真正可緩存的 Ajax 在現實開發中比較少見,而儘早刷新文檔的輸出的狀況 facebook 在 2010 年的 velocity 上提到過,就是 BigPipe 技術。當時 facebook 團隊還講到了 Quickling 和 PageCache 兩項技術,其中的 PageCache 算是比較完全的實現 Ajax 可緩存的優化原則了。fis 團隊也曾與某產品線合做基於靜態資源表、模板組件化等技術實現了頁面的 PipeLine 輸出、以及 Quickling 和 PageCache 功能,但最終效果沒有達到理想的性能優化預期,所以這兩個方向尚在探索中,相信在不久的未來會有新的突破。
總結
其實在前端開發工程管理領域還有不少細節值得探索和挖掘,提高前端團隊生產力水平並非一句空話,它須要咱們能對前端開發及代碼運行有更深入的認識,對性能優化原則有更細緻的分析與研究。fis 團隊一直致力於從架構而非經驗的角度實現性能優化原則;解決前端工程師開發、調試、部署中遇到的工程問題;提供組件化框架,提升代碼複用率;提供開發工具集,提高工程師的開發效率。在前端工業化開發的全部環節均有可節省的人力成本,這些成本很是可觀,相信如今不少大型互聯網公司也都有了這樣的共識。本文只是將這個領域中很小的一部分知識的展開討論,拋磚引玉,但願能爲業界相關領域的工做者提供一些不同的思路。歡迎關注fis項目,對本文有任何意見或建議均可以在 fis 開源項目中進行反饋和討論。