詳解大型網(wǎng)站的前端性能優(yōu)化思路
時(shí)間:2022-05-30 15:21:01 | 來(lái)源:網(wǎng)絡(luò)營(yíng)銷(xiāo)
時(shí)間:2022-05-30 15:21:01 來(lái)源:網(wǎng)絡(luò)營(yíng)銷(xiāo)
每個(gè)參與過(guò)開(kāi)發(fā)企業(yè)級(jí)web應(yīng)用的前端工程師或許都曾思考過(guò)前端性能優(yōu)化方面的問(wèn)題,經(jīng)驗(yàn)豐富的工程師對(duì)于前端性能優(yōu)化方法耳濡目染,基本都能一一列舉出來(lái),這些性能優(yōu)化原則大概是在7年前提出的,對(duì)于web性能優(yōu)化至今都有非常重要的指導(dǎo)意義。
對(duì)于構(gòu)建大型web應(yīng)用的團(tuán)隊(duì)來(lái)說(shuō),要堅(jiān)持貫徹這些優(yōu)化原則并不是一件十分容易的事,因?yàn)閮?yōu)化原則中很多要求與工程管理相違背,比如“把css放在頭部”和“把js放在尾部”這兩條原則,我們不能讓整個(gè)團(tuán)隊(duì)的工程師在寫(xiě)樣式和腳本引用的時(shí)候都去修改同一份的頁(yè)面文件,這會(huì)嚴(yán)重影響團(tuán)隊(duì)成員間并行開(kāi)發(fā)的效率,尤其是在團(tuán)隊(duì)有版本管理的情況下,每天要花大量的時(shí)間進(jìn)行代碼修改合并,這項(xiàng)成本是難以接受的。
因此在前端工程界,總會(huì)看到周期性的性能優(yōu)化工作,辛勤的前端工程師們每到月圓之夜就會(huì)傾巢出動(dòng)根據(jù)優(yōu)化原則做一次最佳實(shí)踐。
今天,億企邦就從一個(gè)全新的視角來(lái)思考web性能優(yōu)化與前端工程之間的關(guān)系,通過(guò)解讀百度前端集成解決方案小組(F.I.S)在打造高性能前端架構(gòu)并統(tǒng)一百度40多條前端產(chǎn)品線的過(guò)程中所經(jīng)歷的技術(shù)嘗試,揭示前端性能優(yōu)化在前端架構(gòu)及開(kāi)發(fā)工具設(shè)計(jì)層面的實(shí)現(xiàn)思路。
一、大型網(wǎng)站的性能優(yōu)化原則及分類(lèi) 先假設(shè)本文的讀者是有前端開(kāi)發(fā)經(jīng)驗(yàn)的工程師,并對(duì)企業(yè)級(jí)web應(yīng)用開(kāi)發(fā)及性能優(yōu)化有一定的思考,因此我不會(huì)重復(fù)介紹雅虎14條性能優(yōu)化原則,如果您沒(méi)有這些前續(xù)知識(shí)的,請(qǐng)移步這里來(lái)學(xué)習(xí)。
首先,我們把《雅虎十四條:網(wǎng)站前端網(wǎng)頁(yè)優(yōu)化的14條原則》中提到的優(yōu)化點(diǎn)做一次梳理,如果按照優(yōu)化方向分類(lèi)可以得到這樣一張表格:
目前,大多數(shù)前端團(tuán)隊(duì)可以利用壓縮工具很容易做到“精簡(jiǎn)javascript”這條原則,同樣的,也可以使用圖片壓縮工具對(duì)圖像進(jìn)行壓縮,實(shí)現(xiàn)“圖像優(yōu)化”原則,這兩條原則是對(duì)單個(gè)資源的處理,因此不會(huì)引起任何工程方面的問(wèn)題。
很多團(tuán)隊(duì)也通過(guò)引入代碼校驗(yàn)流程來(lái)確保實(shí)現(xiàn)“避免css表達(dá)式”和“避免重定向”原則;目前絕大多數(shù)互聯(lián)網(wǎng)公司也已經(jīng)開(kāi)啟了服務(wù)端的Gzip壓縮,并使用CDN實(shí)現(xiàn)靜態(tài)資源的緩存和快速訪問(wèn);一些技術(shù)實(shí)力雄厚的前端團(tuán)隊(duì)甚至研發(fā)出了自動(dòng)CSS Sprites工具,解決了CSS Sprites在工程維護(hù)方面的難題,使用“查找 - 替換”思路,我們似乎也可以很好的實(shí)現(xiàn)“劃分主域”原則。
我們把以上這些已經(jīng)成熟應(yīng)用到實(shí)際生產(chǎn)中的優(yōu)化手段去除掉,留下那些還沒(méi)有很好實(shí)現(xiàn)的優(yōu)化原則,再來(lái)回顧一下之前的性能優(yōu)化分類(lèi):
誠(chéng)然,不可否認(rèn)現(xiàn)在有很多頂尖的前端團(tuán)隊(duì)可以將上述還剩下的優(yōu)化原則也都一一解決,但業(yè)界大多數(shù)團(tuán)隊(duì)都還沒(méi)能很好的解決這些問(wèn)題,因此,接下來(lái)億企邦將就這些原則的解決方案做進(jìn)一步的分析與講解,從而為那些還沒(méi)有進(jìn)入前端工業(yè)化開(kāi)發(fā)的團(tuán)隊(duì)提供一些基礎(chǔ)技術(shù)建設(shè)意見(jiàn),也借此機(jī)會(huì)與業(yè)界頂尖的前端團(tuán)隊(duì)在工業(yè)化工程化方向上交流一下彼此的心得。
二、靜態(tài)資源版本更新與緩存 如上面第一條中的表格所示,在“緩存利用”分類(lèi)中保留了“添加Expires頭”和“配置ETag”兩項(xiàng),或許有些人會(huì)質(zhì)疑,明明這兩項(xiàng)只要配置了服務(wù)器的相關(guān)選項(xiàng)就可以實(shí)現(xiàn),為什么說(shuō)它們難以解決呢?
確實(shí),開(kāi)啟這兩項(xiàng)很容易,但開(kāi)啟了緩存后,我們的項(xiàng)目就開(kāi)始面臨另一個(gè)挑戰(zhàn):如何更新這些緩存?
相信大多數(shù)團(tuán)隊(duì)也找到了類(lèi)似的答案,它和“高性能網(wǎng)站建設(shè)指南”關(guān)于“添加Expires頭”所說(shuō)的原則一樣——修訂文件名。
思路沒(méi)錯(cuò),但要怎么改變鏈接呢?變成什么樣的鏈接才能有效更新緩存,又能最大限度避免那些沒(méi)有修改過(guò)的文件緩存不失效呢?
先來(lái)看看現(xiàn)在一般前端團(tuán)隊(duì)的做法:
<script type="text/javascript" src="a.js?t=20130825"></script>
或者:
<script type="text/javascript" src="a.js?v=1.0.0"></script>
大家會(huì)采用添加query的形式修改鏈接,這樣做是比較直觀的解決方案,但在訪問(wèn)量較大的網(wǎng)站,這么做可能將面臨一些新的問(wèn)題。
通常一個(gè)大型的web應(yīng)用幾乎每天都會(huì)有迭代和更新,發(fā)布新版本也就是發(fā)布新的靜態(tài)資源和頁(yè)面的過(guò)程,以上述代碼為例,假設(shè)現(xiàn)在線上運(yùn)行著index.html文件,并且使用了線上的a.js資源。
index.html的內(nèi)容為:
<script type="text/javascript" src="a.js?v=1.0.0"></script>
這次我們更新了頁(yè)面中的一些內(nèi)容,得到一個(gè)index.html文件,并開(kāi)發(fā)了新的與之匹配的a.js資源來(lái)完成頁(yè)面交互,新的index.html文件的內(nèi)容因此而變成了:
<script type="text/javascript" src="a.js?v=1.0.1"></script>
好了,現(xiàn)在要開(kāi)始將兩份新的文件發(fā)布到線上去,可以看到,a.html和a.js的資源實(shí)際上是要覆蓋線上的同名文件的,不管怎樣,在發(fā)布的過(guò)程中,index.html和a.js總有一個(gè)先后的順序,從而中間出現(xiàn)一段或大或小的時(shí)間間隔。
對(duì)于一個(gè)大型互聯(lián)網(wǎng)應(yīng)用來(lái)說(shuō)即使在一個(gè)很小的時(shí)間間隔內(nèi),都有可能出現(xiàn)新用戶訪問(wèn),而在這個(gè)時(shí)間間隔中訪問(wèn)了網(wǎng)站的用戶會(huì)發(fā)生什么情況呢?具體來(lái)說(shuō)有以下2點(diǎn):
1、如果先覆蓋index.html,后覆蓋a.js,用戶在這個(gè)時(shí)間間隙訪問(wèn),會(huì)得到新的index.html配合舊的a.js的情況,從而出現(xiàn)錯(cuò)誤的頁(yè)面。
2、如果先覆蓋a.js,后覆蓋index.html,用戶在這個(gè)間隙訪問(wèn),會(huì)得到舊的index.html配合新的a.js 的情況,從而也出現(xiàn)了錯(cuò)誤的頁(yè)面。
這就是為什么大型web應(yīng)用在版本上線的過(guò)程中經(jīng)常會(huì)較集中的出現(xiàn)前端報(bào)錯(cuò)日志的原因,也是一些互聯(lián)網(wǎng)公司選擇加班到半夜等待訪問(wèn)低峰期再上線的原因之一。
此外,由于靜態(tài)資源文件版本更新是“覆蓋式”的,而頁(yè)面需要通過(guò)修改query來(lái)更新,對(duì)于使用CDN緩存的web產(chǎn)品來(lái)說(shuō),還可能面臨CDN緩存攻擊的問(wèn)題,我們?cè)賮?lái)觀察一下前面說(shuō)的版本更新手段:
<script type="text/javascript" src="a.js?v=1.0.0"></script>
我們不難預(yù)測(cè),a.js的下一個(gè)版本是“1.0.1”,那么就可以刻意構(gòu)造一串這樣的請(qǐng)求“a.js?v=1.0.1”、“a.js?v=1.0.2”、……,讓CDN將當(dāng)前的資源緩存為“未來(lái)的版本”。
這樣當(dāng)這個(gè)頁(yè)面所用的資源有更新時(shí),即使更改了鏈接地址,也會(huì)因?yàn)镃DN的原因返回給用戶舊版本的靜態(tài)資源,從而造成頁(yè)面錯(cuò)誤,即便不是刻意制造的攻擊,在上線間隙出現(xiàn)訪問(wèn)也可能導(dǎo)致區(qū)域性的CDN緩存錯(cuò)誤。
此外,當(dāng)版本有更新時(shí),修改所有引用鏈接也是一件與工程管理相悖的事,至少我們需要一個(gè)可以“查找 - 替換”的工具來(lái)自動(dòng)化的解決版本號(hào)修改的問(wèn)題。
對(duì)付這個(gè)問(wèn)題,億企邦覺(jué)得目前來(lái)說(shuō)最優(yōu)方案就是基于文件內(nèi)容的hash版本冗余機(jī)制了,也就是說(shuō),我們希望工程師源碼是這么寫(xiě)的:
<script type="text/javascript" src="a.js"></script>
但是線上代碼是這樣的:
<script type="text/javascript" src="a_8244e91.js"></script>
其中“82244e91”這串字符是根據(jù)a.js的文件內(nèi)容進(jìn)行hash運(yùn)算得到的,只有文件內(nèi)容發(fā)生變化了才會(huì)有更改,由于版本序列是與文件名寫(xiě)在一起的,而不是同名文件覆蓋,因此不會(huì)出現(xiàn)上述說(shuō)的那些問(wèn)題,那么,這么做都有哪些好處呢?
1、線上的a.js不是同名文件覆蓋,而是文件名+h ash的冗余,所以可以先上線靜態(tài)資源,再上線html頁(yè)面,不存在間隙問(wèn)題。
2、遇到問(wèn)題回滾版本的時(shí)候,無(wú)需回滾a.js,只須回滾頁(yè)面即可。
3、由于靜態(tài)資源版本號(hào)是文件內(nèi)容的hash,因此所有靜態(tài)資源可以開(kāi)啟永久強(qiáng)緩存,只有更新了內(nèi)容的文件才會(huì)緩存失效,緩存利用率大增。
4、修改靜態(tài)資源后會(huì)在線上產(chǎn)生新的文件,一個(gè)文件對(duì)應(yīng)一個(gè)版本,因此不會(huì)受到構(gòu)造CDN緩存形式的攻擊。
雖然這種方案是相比之下最完美的解決方案,但它無(wú)法通過(guò)手工的形式來(lái)維護(hù),因?yàn)橐揽渴止さ男问絹?lái)計(jì)算和替換hash值并生成相應(yīng)的文件將是一項(xiàng)非常繁瑣且容易出錯(cuò)的工作,因此,我們需要借助工具,有了這樣的思路,我們下面就來(lái)了解一下fis是如何完成這項(xiàng)工作的。
首先,之所以有這種工具需求,完全是因?yàn)閣eb應(yīng)用運(yùn)行的根本機(jī)制決定的,web應(yīng)用所需的資源是以字面的形式通知瀏覽器下載而聚合在一起運(yùn)行的,這種資源加載策略使得web應(yīng)用從本質(zhì)上區(qū)別于傳統(tǒng)桌面應(yīng)用的版本更新方式,也是大型web應(yīng)用需要工具處理的最根本原因。
為了實(shí)現(xiàn)資源定位的字面量替換操作,前端構(gòu)建工具理論上需要識(shí)別所有資源定位的標(biāo)記,其中包括:
1、css中的@import url(path)、background:url(path)、backgournd-image:url(path)、filter 中的src。
2、js中的自定義資源定位函數(shù),在fis中我們將其規(guī)定為_(kāi)_uri(path)。
3、html中的<script src=”path”>、<link href=”path”>、<img src=”path”>、已經(jīng)embed、audio、video、object等具有資源加載功能的標(biāo)簽。
為了工程上的維護(hù)方便,我們希望工程師在源碼中寫(xiě)的是相對(duì)路徑,而工具可以將其替換為線上的絕對(duì)路徑,從而避免相對(duì)路徑定位錯(cuò)誤的問(wèn)題(比如js中需要定位圖片路徑時(shí)不能使用相對(duì)路徑的情況)。
fis有一個(gè)非常棒的資源定位系統(tǒng),它是根據(jù)用戶自己的配置來(lái)指定資源發(fā)布后的地址,然后由fis的資源定位系統(tǒng)識(shí)別文件中的定位標(biāo)記,計(jì)算內(nèi)容hash,并根據(jù)配置替換為上線后的絕對(duì)url路徑。
要想實(shí)現(xiàn)具備hash版本生成功能的構(gòu)建工具不是“查找 - 替換”這么簡(jiǎn)單的,我們考慮這樣一種情況:
由于我們的資源版本號(hào)是通過(guò)對(duì)文件內(nèi)容進(jìn)行hash運(yùn)算得到,如上圖所示,index.html中引用的a.css文件的內(nèi)容其實(shí)也包含了a.png的hash運(yùn)算結(jié)果,因此我們?cè)谛薷膇ndex.html中a.css的引用時(shí),不能直接計(jì)算a.css的內(nèi)容hash,而是要先計(jì)算出a.png的內(nèi)容hash,替換a.css中的引用,得到了a.css的最終內(nèi)容,再做hash運(yùn)算,最后替換index.html中的引用。
這意味著構(gòu)建工具需要具備“遞歸編譯”的能力,這也是為什么fis團(tuán)隊(duì)不得不放棄gruntjs等task-based系統(tǒng)的根本原因,針對(duì)前端項(xiàng)目的構(gòu)建工具必須是具備遞歸處理能力的,此外,由于文件之間的交叉引用等原因,fis構(gòu)建工具還實(shí)現(xiàn)了構(gòu)建緩存等機(jī)制,以提升構(gòu)建速度。
在解決了基于內(nèi)容hash的版本更新問(wèn)題之后,我們可以將所有前端靜態(tài)資源開(kāi)啟永久強(qiáng)緩存,每次版本發(fā)布都可以首先讓靜態(tài)資源全量上線,再進(jìn)一步上線模板或者頁(yè)面文件,再也不用擔(dān)心各種緩存和時(shí)間間隙的問(wèn)題了!
三、靜態(tài)資源管理與模板框架 讓我們?cè)賮?lái)看看前面的優(yō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í)上,使用工具在線下進(jìn)行靜態(tài)資源合并是無(wú)法解決資源按需加載的問(wèn)題的,如果解決不了按需加載,則勢(shì)必會(huì)導(dǎo)致資源的冗余。
此外,線下通過(guò)工具實(shí)現(xiàn)的資源合并通常會(huì)使得資源加載和使用的分離,比如在頁(yè)面頭部或配置文件中寫(xiě)資源引用及合并信息,而用到這些資源的html組件寫(xiě)在了頁(yè)面其他地方,這種書(shū)寫(xiě)方式在工程上非常容易引起維護(hù)不同步的問(wèn)題,導(dǎo)致使用資源的代碼刪除了,引用資源的代碼卻還在的情況。
因此,在工業(yè)上要實(shí)現(xiàn)資源合并至少要滿足如下需求:
1、確實(shí)能減少HTTP請(qǐng)求,這是基本要求(合并)。
2、在使用資源的地方引用資源(就近依賴(lài)),不使用不加載(按需)。
3、雖然資源引用不是集中書(shū)寫(xiě)的,但資源引用的代碼最終還能出現(xiàn)在頁(yè)面頭部(css)或尾部(js)。
4、能夠避免重復(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 在線上運(yùn)行的。
接下來(lái)億企邦會(huì)講述一種新的模板架構(gòu)設(shè)計(jì),用以實(shí)現(xiàn)前面說(shuō)到那些性能優(yōu)化原則,同時(shí)滿足工程開(kāi)發(fā)和維護(hù)的需要,這種架構(gòu)設(shè)計(jì)的核心思想就是:
考慮一段這樣的頁(yè)面代碼:
<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>
根據(jù)資源合并需求中的第二項(xiàng),我們希望資源引用與使用能盡量靠近,這樣將來(lái)維護(hù)起來(lái)會(huì)更容易一些,因此,理想的源碼是:
<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>
當(dāng)然,把這樣的頁(yè)面直接送達(dá)給瀏覽器用戶是會(huì)有嚴(yán)重的頁(yè)面閃爍問(wèn)題的,所以我們實(shí)際上仍然希望最終頁(yè)面輸出的結(jié)果還是如最開(kāi)始的截圖一樣,將css放在頭部輸出,這就意味著,頁(yè)面結(jié)構(gòu)需要有一些調(diào)整,并且有能力收集資源加載需求,那么我們考慮一下這樣的源碼:
<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>
在頁(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)(具體可查看億企邦《資深Web前端開(kāi)發(fā)工程師教你如何優(yōu)化CSS框架》的相關(guān)介紹)。
經(jīng)過(guò)總結(jié),我們發(fā)現(xiàn)模板層面只要實(shí)現(xiàn)三個(gè)開(kāi)發(fā)接口,既可以比較完美的實(shí)現(xiàn)目前遺留的大部分性能優(yōu)化原則,這三個(gè)接口分別是:
1、require(String id):收集資源加載需求的接口,參數(shù)是資源id。
2、widget(String template_id):加載拆分成小組件模板的接口,你可以叫它為load、component或者pagelet之類(lèi)的,總之,我們需要一個(gè)接口把一個(gè)大的頁(yè)面模板拆分成一個(gè)個(gè)的小部分來(lái)維護(hù),最后在原來(lái)的大頁(yè)面以組件為單位來(lái)加載這些小部件。
3、script(String code):收集寫(xiě)在模板中的 js 腳本,使之出現(xiàn)的頁(yè)面底部,從而實(shí)現(xiàn)性能優(yōu)化原則中的“將js放在頁(yè)面底部”原則。
實(shí)現(xiàn)了這些接口之后,一個(gè)重構(gòu)后的模板頁(yè)面的源代碼可能看起來(lái)就是這樣的了:
<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>
而最終在模板解析的過(guò)程中,資源收集與去重、頁(yè)面script收集、占位符替換操作,最終從服務(wù)端發(fā)送出來(lái)的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>
不難看出,我們目前已經(jīng)實(shí)現(xiàn)了“按需加載”,“將腳本放在底部”,“將樣式表放在頭部”三項(xiàng)優(yōu)化原則。
前面講到靜態(tài)資源在上線后需要添加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è)資源的部署路徑,可以得到這樣的一張表:
{
"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": {}
}
基于這張表,我們就很容易實(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定位,得到:
<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>
接下來(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)求一種線上的動(dòng)態(tài)資源合并服務(wù),從而實(shí)現(xiàn)減少HTTP請(qǐng)求的需求,比如:
<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>
這個(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)求,并將其緩存,以加快訪問(wèn)速度。
這種方法很巧妙,有些服務(wù)器甚至直接集成了這類(lèi)模塊來(lái)方便的開(kāi)啟此項(xiàng)服務(wù),這種做法也是大多數(shù)大型web應(yīng)用的資源合并做法,但億企邦覺(jué)得它也存在一些缺陷:
1、瀏覽器有url長(zhǎng)度限制,因此不能無(wú)限制的合并資源(具體可查看億企邦《如何以SEO的角度來(lái)優(yōu)化網(wǎng)站的URL連接地址》的相關(guān)介紹)。
2、如果用戶在網(wǎng)站內(nèi)有公共資源的兩個(gè)頁(yè)面間跳轉(zhuǎn)訪問(wèn),由于兩個(gè)頁(yè)面的combo的url不一樣導(dǎo)致用戶不能利用瀏覽器緩存來(lái)加快對(duì)公共資源的訪問(wèn)速度。
對(duì)于上述第二條缺陷,可以舉個(gè)例子來(lái)看說(shuō)明:
(1)、假設(shè)網(wǎng)站有兩個(gè)頁(yè)面A和B;
(2)、A頁(yè)面使用了a,b,c,d四個(gè)資源;
(3)、B頁(yè)面使用了a,b,e,f四個(gè)資源;
(4)、如果使用combo服務(wù),我們會(huì)得:
A頁(yè)面的資源引用為:/combo?files=a,b,c,d
B頁(yè)面的資源引用為:/combo?files=a,b,e,f
(5)、兩個(gè)頁(yè)面引用的資源是不同的url,因此瀏覽器會(huì)請(qǐng)求兩個(gè)合并后的資源文件,跨頁(yè)面訪問(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)用戶在訪問(wèn)A頁(yè)面之后再訪問(wèn)B頁(yè)面時(shí),只需要下載B頁(yè)面的第二個(gè)combo文件即可,第一個(gè)文件已經(jīng)在訪問(wèn)A頁(yè)面時(shí)緩存好了的。
基于這樣的思考,在資源表上新增了一個(gè)字段,取名為“pkg”,就是資源合并生成的新資源,表的結(jié)構(gòu)會(huì)變成:
{
"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"]
}
}
}
相比之前的表,可以看到新表中多了一個(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就變成了:
<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資源請(qǐng)求數(shù)由原來(lái)的4個(gè)減少為2個(gè),這樣的打包結(jié)果是怎么來(lái)的呢?答案是配置得到的,我們來(lái)看一下帶有打包結(jié)果的資源表的fis配置:
fis.config.set('pack', {
'pkg/util.css': [ 'bootstrap.css', 'A/A.css'],
'pkg/other.css': [ '**.css' ]
});
我們將“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)題:
1、打包的資源只是原來(lái)獨(dú)立資源的備份。打包與否不會(huì)導(dǎo)致資源的丟失,最多是沒(méi)有合并的很好而已。
2、配置可以由工程師根據(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)做到的:
“拆分初始化負(fù)載”的目標(biāo)是將頁(yè)面一開(kāi)始加載時(shí)不需要執(zhí)行的資源從所有資源中分離出來(lái),等到需要的時(shí)候再加載,工程師通常沒(méi)有耐心去區(qū)分資源的分類(lèi)情況,但我們可以利用組件化框架接口來(lái)幫助工程師管理資源的使用,還是從例子開(kāi)始思考:
<html>
<head>
<title>www.mahaixiang.cn</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給百度內(nèi)部團(tuán)隊(duì)開(kāi)發(fā)的架構(gòu)中,如果這樣書(shū)寫(xiě)代碼,頁(yè)面最終的執(zhí)行結(jié)果會(huì)變成:
<html>
<head>
<title>www.mahaixiang.cn</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系統(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的接口,用于異步加載一些資源,源碼修改為:
<html>
<head>
<title>www.mahaixiang.cn</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>
這樣書(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é)果修改為:
<html>
<head>
<title>www.mahaixiang.cn</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不會(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)能做到了:
剩下的兩項(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)品線合作基于靜態(tài)資源表、模板組件化等技術(shù)實(shí)現(xiàn)了頁(yè)面的PipeLine輸出、以及Quickling和PageCache功能,但最終效果沒(méi)有達(dá)到理想的性能優(yōu)化預(yù)期,因此這兩個(gè)方向尚在探索中,相信在不久的將來(lái)會(huì)有新的突破。
億企邦點(diǎn)評(píng): 其實(shí)在前端開(kāi)發(fā)工程管理領(lǐng)域還有很多細(xì)節(jié)值得探索和挖掘,提升前端團(tuán)隊(duì)生產(chǎn)力水平并不是一句空話,它需要我們能對(duì)前端開(kāi)發(fā)及代碼運(yùn)行有更深刻的認(rèn)識(shí),對(duì)性能優(yōu)化原則有更細(xì)致的分析與研究,億企邦一直致力于從架構(gòu)而非經(jīng)驗(yàn)的角度實(shí)現(xiàn)性能優(yōu)化原則,解決前端工程師開(kāi)發(fā)、調(diào)試、部署中遇到的工程問(wèn)題,提供組件化框架,提高代碼復(fù)用率,提供開(kāi)發(fā)工具集,提升工程師的開(kāi)發(fā)效率。
在前端工業(yè)化開(kāi)發(fā)的所有環(huán)節(jié)均有可節(jié)省的人力成本,這些成本非??捎^,相信現(xiàn)在很多大型互聯(lián)網(wǎng)公司也都有了這樣的共識(shí)。