前端project與性能優化(長文)

原文連接:http://fex.baidu.com/blog/2014/03/fis-optimize/javascript

  每個參與過開發企業級 web 應用的前端project師也許都曾思考過前端性能優化方面的問題。咱們有雅虎 14 條性能優化原則。還有兩本很是經典的性能優化指導書:《高性能站點建設指南》、《高性能站點建設指南》。經驗豐富的project師對於前端性能優化方法耳濡目染。基本都能一一列舉出來。這些性能優化原則大概是在 7 年前提出的。對於 web 性能優化至今都有很重要的指導意義。php

  然而,對於構建大型 web 應用的團隊來講,要堅持貫徹這些優化原則並不是一件十分easy的事。因爲優化原則中很是多要求與project管理相違背。比方「把 css 放在頭部」和「把 js 放在尾部」這兩條原則,咱們不能讓整個團隊的project師在寫樣式和腳本引用的時候都去改動同一份的頁面文件。css

這會嚴重影響團隊成員間並行開發的效率,尤爲是在團隊有版本號管理的狀況下。天天要花大量的時間進行代碼改動合併。這項成本是難以接受的。html

所以在前端project界,總會看到週期性的性能優化工做,辛勤的前端project師們每到月圓之夜就會傾巢出動依據優化原則作一次最佳實踐。前端

  本文從一個全新的視角來思考 web 性能優化與前端project之間的關係。經過解讀百度前端集成解決方式小組(F.I.S)在打造高性能前端架構並統一百度 40 多條前端產品線的過程當中所經歷的技術嘗試。揭示前端性能優化在前端架構及開發工具設計層面的實現思路。java

  性能優化原則及分類python

  筆者先若是本文的讀者是有前端開發經驗的project師,並對企業級 web 應用開發及性能優化有必定的思考。所以我不會反覆介紹雅虎 14 條性能優化原則,若是您沒有這些前續知識的,請移步這裏來學習。jquery

  首先,咱們把雅虎 14 條優化原則。《高性能站點建設指南》以及《高性能站點建設進階指南》中提到的優化點作一次梳理,假設依照優化方向分類可以獲得這樣一張表格:  git

優化方向 優化手段
請求數量 合併腳本和樣式表,CSS Sprites,拆分初始化負載。劃分主域
請求帶寬 開啓 GZip。精簡 JavaScript,移除反覆腳本,圖像優化
緩存利用 使用 CDN。使用外部 JavaScript 和 CSS。加入 Expires 頭,下降 DNS 查找。配置 ETag,使 AjaX 可緩存
頁面結構 將樣式表放在頂部,將腳本放在底部,儘早刷新文檔的輸出
代碼校驗 避免 CSS 表達式。避免重定向

  眼下大多數前端團隊可以利用 yui compressor 或者 google closure compiler 等壓縮工具很是easy作到「精簡 javascript 」這條原則。相同的,也可以使用圖片壓縮工具對圖像進行壓縮,實現「圖像優化」原則,這兩條原則是對單個資源的處理,所以不會引發不論什麼project方面的問題。很是多團隊也經過引入代碼校驗流程來確保實現「避免 css 表達式」和「避免重定向」原則。眼下絕大多數互聯網公司也已經開啓了服務端的 Gzip 壓縮,並使用 CDN 實現靜態資源的緩存和高速訪問。一些技術實力雄厚的前端團隊甚至研發出了本身主動 CSS Sprites 工具。攻克了 CSS Sprites 在project維護方面的難題。使用「查找 - 替換」思路,咱們彷佛也可以很是好的實現「劃分主域」原則。github

  咱們把以上這些已經成熟應用到實際生產中的優化手段去除掉,留下那些尚未很是好實現的優化原則,再來回想一下以前的性能優化分類:  

優化方向 優化手段
請求數量 合併腳本和樣式表。拆分初始化負載
請求帶寬 移除反覆腳本
緩存利用 加入 Expires 頭。配置 ETag,使 Ajax 可緩存
頁面結構 將樣式表放在頂部,將腳本放在底部,儘早刷新文檔的輸出

  誠然,不能否認現在有很是多頂尖的前端團隊可以將上述還剩下的優化原則也都一一解決,但業界大多數團隊都還沒能很是好的解決這些問題。所以接下來本文將就這些原則的解決方式作進一步的分析與解說,從而爲那些尚未進入前端工業化開發的團隊提供一些基礎技術建設意見,也藉此機會與業界頂尖的前端團隊在工業化project化方向上交流一下彼此的心得。

  靜態資源版本號更新與緩存

  如表格 2 所看到的,在「緩存利用」分類中保留了「加入 Expires 頭」和「配置 ETag 」兩項,也許有些人會質疑,明明這兩項僅僅要配置了server的相關選項就可以實現。爲何說它們難以解決呢?確實,開啓這兩項很是easy,但開啓了緩存後,咱們的項目就開始面臨還有一個挑戰:怎樣更新這些緩存。

  相信大多數團隊也找到了相似的答案。它和《高性能站點建設指南》關於「加入 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 總有一個前後的順序,從而中間出現一段或大或小的時間間隔。

對於一個大型互聯網應用來講即便在一個很是小的時間間隔內,都有可能出現新用戶訪問,而在這個時間間隔中訪問了站點的用戶會發生什麼狀況呢:

  1. 假設先覆蓋 index.html。後覆蓋 a.js,用戶在這個時間間隙訪問。會獲得新的 index.html 配合舊的 a.js 的狀況,從而出現錯誤的頁面。
  2. 假設先覆蓋 a.js,後覆蓋 index.html,用戶在這個間隙訪問,會獲得舊的 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 緩存錯誤。

  此外。當版本號有更新時,改動所有引用連接也是一件與project管理相悖的事,至少咱們需要一個可以「查找 - 替換」的工具來本身主動化的解決版本號號改動的問題。

  對付這個問題,眼下來講最優方案就是基於文件內容的 hash 版本號冗餘機制 了。也就是說,咱們但願project師源代碼是這麼寫的:

<script type="text/javascript" src="a.js"></script>

  但是線上代碼是這種:

<script type="text/javascript" src="a_8244e91.js"></script>

  當中」_82244e91 」這串字符是依據 a.js 的文件內容進行 hash 運算獲得的。僅僅有文件內容發生變化了纔會有更改。由於版本號序列是與文件名稱寫在一塊兒的。而不是同名文件覆蓋,所以不會出現上述說的那些問題。那麼這麼作都有哪些優勢呢?

  1. 線上的 a.js 不是同名文件覆蓋,而是文件名稱 +h ash 的冗餘。因此可以先上線靜態資源,再上線 html 頁面。不存在間隙問題;
  2. 遇到問題回滾版本號的時候,無需回滾 a.js,僅僅須回滾頁面就能夠;
  3. 由於靜態資源版本號號是文件內容的 hash。所以所有靜態資源可以開啓永久強緩存,僅僅有更新了內容的文件纔會緩存失效,緩存利用率大增;
  4. 改動靜態資源後會在線上產生新的文件,一個文件相應一個版本號,所以不會受到構造 CDN 緩存形式的攻擊

  儘管這種方案是相比之下最完美的解決方式,但它沒法經過手工的形式來維護,因爲要依靠手工的形式來計算和替換 hash 值並生成對應的文件將是一項很繁瑣且easy出錯的工做。

所以。咱們需要藉助工具。有了這種思路,咱們如下就來了解一下 fis 是怎樣完畢這項工做的。

  首先。之因此有這樣的工具需求,全然是因爲 web 應用執行的根本機制決定的:web 應用所需的資源是以字面的形式通知瀏覽器下載而聚合在一塊兒執行的。

這樣的資源載入策略使得 web 應用從本質上差異於傳統桌面應用的版本號更新方式。也是大型 web 應用需要工具處理的最根本緣由。

爲了實現資源定位的字面量替換操做。前端構建工具理論上需要識別所有資源定位的標記。當中包含:

  • css 中的@import url(path)、background:url(path)、backgournd-image:url(path)、filter 中的 src
  • js 中的本身定義資源定位函數,在 fis 中咱們將其規定爲__uri(path)。
  • html 中的<script src=」 path 」><link href=」 path 」><img src=」 path 」>、已經 embed、audio、video、object 等具備資源載入功能的標籤。

  爲了project上的維護方便。咱們但願project師在源代碼中寫的是相對路徑。而工具可以將其替換爲線上的絕對路徑,從而避免相對路徑定位錯誤的問題(比方 js 中需要定位圖片路徑時不能使用相對路徑的狀況)。

image2

  fis 有一個很棒的資源定位系統,它是依據用戶本身的配置來指定資源公佈後的地址。而後由 fis 的資源定位系統識別文件裏的定位標記,計算內容 hash,並依據配置替換爲上線後的絕對 url 路徑。

  要想實現具有 hash 版本號生成功能的構建工具不是「查找 - 替換」這麼簡單的,咱們考慮這樣一種狀況:

image3

  由於咱們的資源版本是經過對文件內容進行 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 應用,這樣的方式有一些很是嚴重的缺陷,來看一個很是熟悉的樣例:

image4

  某個 web 產品頁面有 A、B、C 三個資源

image5

  project師依據「下降 HTTP 請求」的優化原則合併了資源

image6

  產品經理要求 C 模塊按需出現,此時 C 資源已出現多餘的可能

image7

  C 模塊再也不需要了,凝視掉吧!但 C 資源一般不敢輕易剔除

  不知不覺中。性能優化變成了性能惡化……

  其實,使用工具在線下進行靜態資源合併是沒法解決資源按需載入的問題的。

假設解決不了按需載入。則勢必會致使資源的冗餘。此外,線下經過工具實現的資源合併通常會使得資源載入和使用的分離,比方在頁面頭部或配置文件裏寫資源引用及合併信息,而用到這些資源的 html 組件寫在了頁面其它地方,這樣的書寫方式在project上很easy引發維護不一樣步的問題,致使使用資源的代碼刪除了,引用資源的代碼卻還在的狀況。所以。在工業上要實現資源合併至少要知足例如如下需求:

  1. 確實能下降 HTTP 請求。這是基本要求(合併)
  2. 在使用資源的地方引用資源(就近依賴),不使用不載入(按需)
  3. 儘管資源引用不是集中書寫的,但資源引用的代碼終於還能出現在頁面頭部(css)或尾部(js)
  4. 能夠避免反覆載入資源(去重)

  將以上要求綜合考慮。不難發現,單純依靠前端技術或者工具處理的是很是難達到這些理想要求的。現代大型 web 應用所展現的頁面絕大多數都是使用服務端動態語言拼接生成的。有的產品使用模板引擎,比方 smarty、velocity,有的則乾脆直接使用動態語言,比方 php、python。無論使用哪一種方式實現。前端project師開發的 html 絕大多數終於都不是以靜態的 html 在線上執行的,接下來我會講述一種新的模板架構設計,用以實現前面說到那些性能優化原則。同一時候知足project開發和維護的需要,這樣的架構設計的核心思想就是:

  考慮一段這種頁面代碼:

<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>

  依據資源合併需求中的第二項,咱們但願資源引用與使用能儘可能靠近,這樣未來維護起來會更easy一些。所以,理想的源代碼是:

<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 接口實現很easy,就是準備一個數組,收集資源引用。並且可以去重。

最後在頁面輸出的前一刻,咱們將 require 在執行時收集到的「 A.css 」、「 B.css 」、「 C.css 」三個資源拼接成 html 標籤。替換掉凝視佔位「<!--[CSS LINKS PLACEHOLDER]-->」,從而獲得咱們需要的頁面結構。

  通過 fis 團隊的總結,咱們發現模板層面僅僅要實現三個開發接口,既可以比較完美的實現眼下遺留的大部分性能優化原則,這三個接口各自是:

  1. require(String id):收集資源載入需求的接口。參數是資源 id。

  2. widget(String template_id):載入拆分紅小組件模板的接口。你可以叫它爲 load、component 或者 pagelet 之類的。總之,咱們需要一個接口把一個大的頁面模板拆分紅一個個的小部分來維護,最後在原來的大頁面以組件爲單位來載入這些小部件。
  3. script(String code):收集寫在模板中的 js 腳本。使之出現的頁面底部,從而實現性能優化原則中的「將 js 放在頁面底部」原則。

  實現了這些接口以後,一個重構後的模板頁面的源碼可能看起來就是這種了:

<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 戳做爲版本號標識,那麼這樣的使用模板語言來收集的靜態資源該怎樣實現這項功能呢?答案是:靜態資源依賴關係表。

若是前面講到的模板源碼所相應的文件夾結構爲下圖所看到的:

image9

  那麼咱們可以使用工具掃描整個 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": {}
}

  基於這張表。咱們就很是easy實現 {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 服務提供的,它的原理很是easy,就是依據 get 請求的 files 參數找到相應的多個文件,合併成一個文件來響應請求。並將其緩存,以加快訪問速度。

  這樣的方法很是巧妙。有些server甚至直接集成了這類模塊來方便的開啓此項服務,這樣的作法也是大多數大型 web 應用的資源合併作法。

但它也存在一些缺陷:

  1. 瀏覽器有 url 長度限制。所以不能無限制的合併資源。
  2. 假設用戶在站點內有公共資源的兩個頁面間跳轉訪問,由於兩個頁面的 combo 的 url 不同致使用戶不能利用瀏覽器緩存來加快對公共資源的訪問速度。

  對於上述第二條缺陷。可以舉個樣例來看說明:

  • 若是站點有兩個頁面 A 和 B
  • A 頁面使用了 a,b。c。d 四個資源
  • B 頁面使用了 a,b。e。f 四個資源
  • 若是使用 combo 服務。咱們會得:
  • A 頁面的資源引用爲:/combo?files=a,b,c,d
  • B 頁面的資源引用爲:/combo?

    files=a,b,e,f

  • 兩個頁面引用的資源是不一樣的 url,所以瀏覽器會請求兩個合併後的資源文件。跨頁面訪問沒能很是好的利用 a、b 這兩個資源的緩存。

  很是明顯。假設 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 在前面講到的兩點技術上的不足而設計的。但也不難發現這樣的打包策略是需要配置的。這就意味着維護成本的添加。

但好在它有兩個優點可以必定程度上彌補這個問題:

  1. 打包的資源僅僅是原來獨立資源的備份。打包與否不會致使資源的丟失,最可能是沒有合併的很是好而已。

  2. 配置可以由project師依據經驗人工維護。也可以由統計日誌生成,這爲性能優化自適應站點設計提供了很是好的基礎。

  關於第二點。fis 有這樣輔助系統來支持自適應打包算法:

image10

  至此,咱們經過基於表的靜態資源管理系統和三個模板接口實現了幾個重要的性能優化原則,現在咱們再來回想一下前面的性能優化原則分類表,剔除掉已經作到了的。看看還剩下哪些沒作到的:  

優化方向 優化手段
請求數量 拆分初始化負載
請求帶寬 拆分初始化負載
緩存利用 使 Ajax 可緩存
頁面結構 儘早刷新文檔的輸出

  「拆分初始化負載」的目標是將頁面一開始載入時不需要運行的資源從所有資源中分離出來,等到需要的時候再載入。project師一般沒有耐心去區分資源的分類狀況,但咱們可以利用組件化框架接口來幫助project師管理資源的使用。仍是從樣例開始思考:

<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 的形式輸出。而是變成了資源註冊。這樣,當頁面點擊button觸發 require.async 運行的時候,async 函數纔會查表找到資源的 url 並載入它,載入完成後觸發回調函數。

  到眼下爲止,咱們又以架構的形式實現了一項優化原則(拆分初始化負載),回想咱們的優化分類表,現在僅有兩項沒能作到了:  

優化方向 優化手段
緩存利用 使 Ajax 可緩存
頁面結構 儘早刷新文檔的輸出

  剩下的兩項優化原則要作到並不easy。真正可緩存的 Ajax 在現實開發中比較少見。而儘早刷新文檔的輸出的狀況 facebook 在 2010 年的 velocity 上提到過。就是 BigPipe 技術。

當時 facebook 團隊還講到了 Quickling 和 PageCache 兩項技術,當中的 PageCache 算是比較完全的實現 Ajax 可緩存的優化原則了。fis 團隊也曾與某產品線合做基於靜態資源表、模板組件化等技術實現了頁面的 PipeLine 輸出、以及 Quickling 和 PageCache 功能。但終於效果沒有達到理想的性能優化預期,所以這兩個方向尚在探索中,相信在不久的未來會有新的突破。

  總結

  事實上在前端開發project管理領域還有很是多細節值得探索和挖掘,提高前端團隊生產力水平並不是一句空話,它需要咱們能對前端開發及代碼執行有更深入的認識。對性能優化原則有更仔細的分析與研究。fis 團隊一直致力於從架構而非經驗的角度實現性能優化原則;解決前端project師開發、調試、部署中遇到的project問題。提供組件化框架,提升代碼複用率;提供開發工具集,提高project師的開發效率。

在前端工業化開發的所有環節均有可節省的人力成本,這些成本很是可觀。相信現在很是多大型互聯網公司也都有了這種共識。本文僅僅是將這個領域中很是小的一部分知識的展開討論,拋磚引玉。但願能爲業界相關領域的工做者提供一些不同的思路。歡迎關注fis項目,對本文有不論什麼意見或建議都可以在 fis 開源項目中進行反饋和討論。

相關文章
相關標籤/搜索