本系列文章從一個(gè)全新的視角來(lái)思考web性能優(yōu)化與前端工程之間的關(guān)系,通過(guò)解讀百度前端集成解決方案小組(F.I.S)在打造高性能前端架構(gòu)并統(tǒng)一百度40多條前端產(chǎn)品線(xiàn)的過(guò)程中所經(jīng)歷的技術(shù)嘗試,揭示前端性能優(yōu)化在前端架構(gòu)及開(kāi)發(fā)工具設(shè)計(jì)層面的實(shí)現(xiàn)思路。
靜態(tài)資源管理與模板框架
讓我們?cè)賮?lái)看看前面的優(yōu)化原則表還剩些什么:
?
優(yōu)化方向 |
優(yōu)化手段
?
|
請(qǐng)求數(shù)量 |
合并腳本和樣式表,拆分初始化負(fù)載 |
請(qǐng)求帶寬 |
移除重復(fù)腳本 |
緩存利用 |
使Ajax可緩存 |
頁(yè)面結(jié)構(gòu) |
將樣式表放在頂部,將腳本放在底部,盡早刷新文檔的輸出 |
很不幸,剩下的優(yōu)化原則都不是使用工具就能很好實(shí)現(xiàn)的。或許有人會(huì)辯駁:“我用某某工具可以實(shí)現(xiàn)腳本和樣式表合并”。嗯,必須承認(rèn),使用工具進(jìn)行資源合并并替換引用或許是一個(gè)不錯(cuò)的辦法,但在大型web應(yīng)用,這種方式有一些非常嚴(yán)重的缺陷,來(lái)看一個(gè)很熟悉的例子:
某個(gè)web產(chǎn)品頁(yè)面有A、B、C三個(gè)資源
工程師根據(jù)“減少HTTP請(qǐng)求”的優(yōu)化原則合并了資源
產(chǎn)品經(jīng)理要求C模塊按需出現(xiàn),此時(shí)C資源已出現(xiàn)多余的可能
C模塊不再需要了,注釋掉吧!但C資源通常不敢輕易剔除
不知不覺(jué)中,性能優(yōu)化變成了性能惡化……
事實(shí)上,使用工具在線(xiàn)下進(jìn)行靜態(tài)資源合并是無(wú)法解決資源按需加載的問(wèn)題的。如果解決不了按需加載,則勢(shì)必會(huì)導(dǎo)致資源的冗余;此外,線(xiàn)下通過(guò)工具實(shí)現(xiàn)的資源合并通常會(huì)使得資源加載和使用的分離,比如在頁(yè)面頭部或配置文件中寫(xiě)資源引用及合并信息,而用到這些資源的html組件寫(xiě)在了頁(yè)面其他地方,這種書(shū)寫(xiě)方式在工程上非常容易引起維護(hù)不同步的問(wèn)題,導(dǎo)致使用資源的代碼刪除了,引用資源的代碼卻還在的情況。因此,在工業(yè)上要實(shí)現(xiàn)資源合并至少要滿(mǎn)足如下需求:
- 確實(shí)能減少HTTP請(qǐng)求,這是基本要求(合并)
- 在使用資源的地方引用資源(就近依賴(lài)),不使用不加載(按需)
- 雖然資源引用不是集中書(shū)寫(xiě)的,但資源引用的代碼最終還能出現(xiàn)在頁(yè)面頭部(css)或尾部(js)
- 能夠避免重復(fù)加載資源(去重)
將以上要求綜合考慮,不難發(fā)現(xiàn),單純依靠前端技術(shù)或者工具處理是很難達(dá)到這些理想要求的。現(xiàn)代大型web應(yīng)用所展示的頁(yè)面絕大多數(shù)都是使用服務(wù)端動(dòng)態(tài)語(yǔ)言拼接生成的。有的產(chǎn)品使用模板引擎,比如smarty、velocity,有的則干脆直接使用動(dòng)態(tài)語(yǔ)言,比如php、python。無(wú)論使用哪種方式實(shí)現(xiàn),前端工程師開(kāi)發(fā)的html絕大多數(shù)最終都不是以靜態(tài)的html在線(xiàn)上運(yùn)行的。
接下來(lái)我會(huì)講述一種新的模板架構(gòu)設(shè)計(jì),用以實(shí)現(xiàn)前面說(shuō)到那些性能優(yōu)化原則,同時(shí)滿(mǎn)足工程開(kāi)發(fā)和維護(hù)的需要,這種架構(gòu)設(shè)計(jì)的核心思想就是:
基于依賴(lài)關(guān)系表的靜態(tài)資源管理系統(tǒng)與模板框架設(shè)計(jì)
考慮一段這樣的頁(yè)面代碼:
根據(jù)資源合并需求中的第二項(xiàng),我們希望資源引用與使用能盡量靠近,這樣將來(lái)維護(hù)起來(lái)會(huì)更容易一些,因此,理想的源碼是:
當(dāng)然,把這樣的頁(yè)面直接送達(dá)給瀏覽器用戶(hù)是會(huì)有嚴(yán)重的頁(yè)面閃爍問(wèn)題的,所以我們實(shí)際上仍然希望最終頁(yè)面輸出的結(jié)果還是如最開(kāi)始的截圖一樣,將css放在頭部輸出。這就意味著,頁(yè)面結(jié)構(gòu)需要有一些調(diào)整,并且有能力收集資源加載需求,那么我們考慮一下這樣的源碼:
在頁(yè)面的頭部插入一個(gè)html注釋“<!--[CSS LINKS PLACEHOLDER]-->”作為占位,而將原來(lái)字面書(shū)寫(xiě)的資源引用改成模板接口(require)調(diào)用,該接口負(fù)責(zé)收集頁(yè)面所需資源。require接口實(shí)現(xiàn)非常簡(jiǎn)單,就是準(zhǔn)備一個(gè)數(shù)組,收集資源引用,并且可以去重。最后在頁(yè)面輸出的前一刻,我們將require在運(yùn)行時(shí)收集到的“A.css”、“B.css”、“C.css”三個(gè)資源拼接成html標(biāo)簽,替換掉注釋占位“<!--[CSS LINKS PLACEHOLDER]-->”,從而得到我們需要的頁(yè)面結(jié)構(gòu)。
經(jīng)過(guò)fis團(tuán)隊(duì)的總結(jié),我們發(fā)現(xiàn)模板層面只要實(shí)現(xiàn)三個(gè)開(kāi)發(fā)接口,既可以比較完美的實(shí)現(xiàn)目前遺留的大部分性能優(yōu)化原則,這三個(gè)接口分別是:
- require(String id):收集資源加載需求的接口,參數(shù)是資源id。
- widget(String template_id):加載拆分成小組件模板的接口。你可以叫它為load、component或者pagelet之類(lèi)的。總之,我們需要一個(gè)接口把一個(gè)大的頁(yè)面模板拆分成一個(gè)個(gè)的小部分來(lái)維護(hù),最后在原來(lái)的大頁(yè)面以組件為單位來(lái)加載這些小部件。
- script(String code):收集寫(xiě)在模板中的js腳本,使之出現(xiàn)的頁(yè)面底部,從而實(shí)現(xiàn)性能優(yōu)化原則中的“將js放在頁(yè)面底部”原則。
實(shí)現(xiàn)了這些接口之后,一個(gè)重構(gòu)后的模板頁(yè)面的源代碼可能看起來(lái)就是這樣的了:
而最終在模板解析的過(guò)程中,資源收集與去重、頁(yè)面script收集、占位符替換操作,最終從服務(wù)端發(fā)送出來(lái)的html代碼為:
不難看出,我們目前已經(jīng)實(shí)現(xiàn)了“按需加載”,“將腳本放在底部”,“將樣式表放在頭部”三項(xiàng)優(yōu)化原則。
前面講到靜態(tài)資源在上線(xiàn)后需要添加hash戳作為版本標(biāo)識(shí),那么這種使用模板語(yǔ)言來(lái)收集的靜態(tài)資源該如何實(shí)現(xiàn)這項(xiàng)功能呢?答案是: 靜態(tài)資源依賴(lài)關(guān)系表 。
假設(shè)前面講到的模板源代碼所對(duì)應(yīng)的目錄結(jié)構(gòu)為下圖所示:
那么我們可以使用工具掃描整個(gè)project目錄,然后創(chuàng)建一張資源表,同時(shí)記錄每個(gè)資源的部署路徑,可以得到這樣的一張表:
基于這張表,我們就很容易實(shí)現(xiàn) {require name=”id”} 這個(gè)模板接口了。只須查表即可。比如執(zhí)行{require name=”jquery.js”},查表得到它的url是“/jquery_9151577.js”,聲明一個(gè)數(shù)組收集起來(lái)就好了。這樣,整個(gè)頁(yè)面執(zhí)行完畢之后,收集資源加載需求,并替換頁(yè)面的占位符,即可實(shí)現(xiàn)資源的hash定位,得到:
接下來(lái),我們討論如何在基于表的設(shè)計(jì)思想上是如何實(shí)現(xiàn)靜態(tài)資源合并的。或許有些團(tuán)隊(duì)使用過(guò)combo服務(wù),也就是我們?cè)谧罱K拼接生成頁(yè)面資源引用的時(shí)候,并不是生成多個(gè)獨(dú)立的link標(biāo)簽,而是將資源地址拼接成一個(gè)url路徑,請(qǐng)求一種線(xiàn)上的動(dòng)態(tài)資源合并服務(wù),從而實(shí)現(xiàn)減少HTTP請(qǐng)求的需求,比如:
這個(gè)“/combo?files=file1,file2,file3,…”的url請(qǐng)求響應(yīng)就是動(dòng)態(tài)combo服務(wù)提供的,它的原理很簡(jiǎn)單,就是根據(jù)get請(qǐng)求的files參數(shù)找到對(duì)應(yīng)的多個(gè)文件,合并成一個(gè)文件來(lái)響應(yīng)請(qǐng)求,并將其緩存,以加快訪(fǎng)問(wèn)速度。
這種方法很巧妙,有些服務(wù)器甚至直接集成了這類(lèi)模塊來(lái)方便的開(kāi)啟此項(xiàng)服務(wù),這種做法也是大多數(shù)大型web應(yīng)用的資源合并做法。但它也存在一些缺陷:
- 瀏覽器有url長(zhǎng)度限制,因此不能無(wú)限制的合并資源。
- 如果用戶(hù)在網(wǎng)站內(nèi)有公共資源的兩個(gè)頁(yè)面間跳轉(zhuǎn)訪(fǎng)問(wèn),由于兩個(gè)頁(yè)面的combo的url不一樣導(dǎo)致用戶(hù)不能利用瀏覽器緩存來(lái)加快對(duì)公共資源的訪(fǎng)問(wèn)速度。
對(duì)于上述第二條缺陷,可以舉個(gè)例子來(lái)看說(shuō)明:
- 假設(shè)網(wǎng)站有兩個(gè)頁(yè)面A和B
- A頁(yè)面使用了a,b,c,d四個(gè)資源
- B頁(yè)面使用了a,b,e,f四個(gè)資源
-
如果使用combo服務(wù),我們會(huì)得:
- A頁(yè)面的資源引用為:/combo?files=a,b,c,d
- B頁(yè)面的資源引用為:/combo?files=a,b,e,f
- 兩個(gè)頁(yè)面引用的資源是不同的url,因此瀏覽器會(huì)請(qǐng)求兩個(gè)合并后的資源文件,跨頁(yè)面訪(fǎng)問(wèn)沒(méi)能很好的利用a、b這兩個(gè)資源的緩存。
很明顯,如果combo服務(wù)能聰明的知道A頁(yè)面使用的資源引用為“/combo?files=a,b”和“/combo?files=c,d”,而B(niǎo)頁(yè)面使用的資源引用為“/combo?files=a,b”,“/combo?files=e,f”就好了。這樣當(dāng)用戶(hù)在訪(fǎng)問(wèn)A頁(yè)面之后再訪(fǎng)問(wèn)B頁(yè)面時(shí),只需要下載B頁(yè)面的第二個(gè)combo文件即可,第一個(gè)文件已經(jīng)在訪(fǎng)問(wèn)A頁(yè)面時(shí)緩存好了的。
基于這樣的思考,fis在資源表上新增了一個(gè)字段,取名為“pkg”,就是資源合并生成的新資源,表的結(jié)構(gòu)會(huì)變成:
相比之前的表,可以看到新表中多了一個(gè)pkg字段,并且記錄了打包后的文件所包含的獨(dú)立資源。這樣,我們重新設(shè)計(jì)一下{require name=”id”}這個(gè)模板接口: 在查表的時(shí)候,如果一個(gè)靜態(tài)資源有 pkg 字段,那么就去加載pkg 字段所指向的打包文件,否則加載資源本身 。比如執(zhí)行{require name=”bootstrap.css”},查表得知bootstrap.css被打包在了“p0”中,因此取出p0包的url“/pkg/utils_b967346.css”,并且記錄頁(yè)面已加載了“bootstrap.css”和“A/A.css”兩個(gè)資源。這樣一來(lái),之前的模板代碼執(zhí)行之后得到的html就變成了:
css資源請(qǐng)求數(shù)由原來(lái)的4個(gè)減少為2個(gè)。
這樣的打包結(jié)果是怎么來(lái)的呢?答案是 配置得到 的。
我們來(lái)看一下帶有打包結(jié)果的資源表的fis配置:
我們將“bootstrap.css”、“A/A.css”打包在一起,其他css另外打包,從而生成兩個(gè)打包文件,當(dāng)頁(yè)面需要打包文件中的資源時(shí),模塊框架就會(huì)收集并計(jì)算出最優(yōu)的資源加載結(jié)果,從而解決靜態(tài)資源合并的問(wèn)題。
這樣做的原因是為了彌補(bǔ)combo在前面講到的兩點(diǎn)技術(shù)上的不足而設(shè)計(jì)的。但也不難發(fā)現(xiàn)這種打包策略是需要配置的,這就意味著維護(hù)成本的增加。但好在它有兩個(gè)優(yōu)勢(shì)可以一定程度上彌補(bǔ)這個(gè)問(wèn)題:
- 打包的資源只是原來(lái)獨(dú)立資源的備份。打包與否不會(huì)導(dǎo)致資源的丟失,最多是沒(méi)有合并的很好而已。
- 配置可以由工程師根據(jù)經(jīng)驗(yàn)人工維護(hù),也可以由統(tǒng)計(jì)日志生成,這為性能優(yōu)化自適應(yīng)網(wǎng)站設(shè)計(jì)提供了非常好的基礎(chǔ)。
關(guān)于第二點(diǎn),fis有這樣輔助系統(tǒng)來(lái)支持自適應(yīng)打包算法:
至此,我們通過(guò)基于表的靜態(tài)資源管理系統(tǒng)和三個(gè)模板接口實(shí)現(xiàn)了幾個(gè)重要的性能優(yōu)化原則,現(xiàn)在我們?cè)賮?lái)回顧一下前面的性能優(yōu)化原則分類(lèi)表,剔除掉已經(jīng)做到了的,看看還剩下哪些沒(méi)做到的:
優(yōu)化方向 |
優(yōu)化手段 |
請(qǐng)求數(shù)量 |
拆分初始化負(fù)載 |
緩存利用 |
使Ajax可緩存 |
頁(yè)面結(jié)構(gòu) |
盡早刷新文檔的輸出 |
?
“拆分初始化負(fù)載”的目標(biāo)是將頁(yè)面一開(kāi)始加載時(shí)不需要執(zhí)行的資源從所有資源中分離出來(lái),等到需要的時(shí)候再加載。工程師通常沒(méi)有耐心去區(qū)分資源的分類(lèi)情況,但我們可以利用組件化框架接口來(lái)幫助工程師管理資源的使用。還是從例子開(kāi)始思考:
模板源代碼
在fis給百度內(nèi)部團(tuán)隊(duì)開(kāi)發(fā)的架構(gòu)中,如果這樣書(shū)寫(xiě)代碼,頁(yè)面最終的執(zhí)行結(jié)果會(huì)變成:
模板運(yùn)行后輸出的html代碼
fis系統(tǒng)會(huì)分析頁(yè)面中require(id)函數(shù)的調(diào)用,并將依賴(lài)關(guān)系記錄到資源表對(duì)應(yīng)資源的 deps 字段中,從而在頁(yè)面渲染查表時(shí)可以加載依賴(lài)的資源。但此時(shí)dialog.js是以script標(biāo)簽的形式同步加載的,這樣會(huì)在頁(yè)面初始化時(shí)出現(xiàn)資源的浪費(fèi)。因此,fis團(tuán)隊(duì)提供了require.async的接口,用于異步加載一些資源,源碼修改為:
這樣書(shū)寫(xiě)之后,fis系統(tǒng)會(huì)在表里以 async 字段來(lái)標(biāo)準(zhǔn)資源依賴(lài)關(guān)系是異步的。fis提供的靜態(tài)資源管理系統(tǒng)會(huì)將頁(yè)面輸出的結(jié)果修改為:
dialog.js不會(huì)在頁(yè)面以script src的形式輸出,而是變成了資源注冊(cè),這樣,當(dāng)頁(yè)面點(diǎn)擊按鈕觸發(fā)require.async執(zhí)行的時(shí)候,async函數(shù)才會(huì)查表找到資源的url并加載它,加載完畢后觸發(fā)回調(diào)函數(shù)。
到目前為止,我們又以架構(gòu)的形式實(shí)現(xiàn)了一項(xiàng)優(yōu)化原則(拆分初始化負(fù)載),回顧我們的優(yōu)化分類(lèi)表,現(xiàn)在僅有兩項(xiàng)沒(méi)能做到了:
優(yōu)化方向 |
優(yōu)化手段 |
緩存利用 |
使Ajax可緩存 |
頁(yè)面結(jié)構(gòu) |
盡早刷新文檔的輸出 |
?
剩下的兩項(xiàng)優(yōu)化原則要做到并不容易,真正可緩存的Ajax在現(xiàn)實(shí)開(kāi)發(fā)中比較少見(jiàn),而盡早刷新文檔的輸出的情況facebook在2010年的velocity上提到過(guò),就是BigPipe技術(shù)。當(dāng)時(shí)facebook團(tuán)隊(duì)還講到了Quickling和PageCache兩項(xiàng)技術(shù),其中的PageCache算是比較徹底的實(shí)現(xiàn)Ajax可緩存的優(yōu)化原則了。fis團(tuán)隊(duì)也曾與某產(chǎn)品線(xiàn)合作基于靜態(tài)資源表、模板組件化等技術(shù)實(shí)現(xiàn)了頁(yè)面的PipeLine輸出、以及Quickling和PageCache功能,但最終效果沒(méi)有達(dá)到理想的性能優(yōu)化預(yù)期,因此這兩個(gè)方向尚在探索中,相信在不久的將來(lái)會(huì)有新的突破。
?
本文只是將這個(gè)領(lǐng)域中很小的一部分知識(shí)的展開(kāi)討論,拋磚引玉,希望能為業(yè)界相關(guān)領(lǐng)域的工作者提供一些不一樣的思路。歡迎關(guān)注 fis項(xiàng)目 ,對(duì)本文有任何意見(jiàn)或建議都可以在fis開(kāi)源項(xiàng)目中進(jìn)行反饋和討論。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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