• <noscript id="eom2a"><optgroup id="eom2a"></optgroup></noscript>
    <tt id="eom2a"><small id="eom2a"></small></tt>
    <input id="eom2a"></input>
  • <div id="eom2a"><small id="eom2a"></small></div>
    <td id="eom2a"><small id="eom2a"></small></td>
  • 您的位置:知識庫 ? Web前端

    前端工程與性能優化

    來源: fexbaidu  發布時間: 2014-07-22 07:55  閱讀: 9487 次  推薦: 33   原文鏈接   [收藏]  

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

      然而,對于構建大型 web 應用的團隊來說,要堅持貫徹這些優化原則并不是一件十分容易的事。因為優化原則中很多要求與工程管理相違背,比如“把 css 放在頭部”和“把 js 放在尾部”這兩條原則,我們不能讓整個團隊的工程師在寫樣式和腳本引用的時候都去修改同一份的頁面文件。這會嚴重影響團隊成員間并行開發的效率,尤其是在團隊有版本管理的情況下,每天要花大量的時間進行代碼修改合并,這項成本是難以接受的。因此在前端工程界,總會看到周期性的性能優化工作,辛勤的前端工程師們每到月圓之夜就會傾巢出動根據優化原則做一次最佳實踐。

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

      性能優化原則及分類

      筆者先假設本文的讀者是有前端開發經驗的工程師,并對企業級 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 可緩存
    頁面結構 將樣式表放在頂部,將腳本放在底部,盡早刷新文檔的輸出

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

      靜態資源版本更新與緩存

      如表格 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 總有一個先后的順序,從而中間出現一段或大或小的時間間隔。對于一個大型互聯網應用來說即使在一個很小的時間間隔內,都有可能出現新用戶訪問,而在這個時間間隔中訪問了網站的用戶會發生什么情況呢:

    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 緩存錯誤。

      此外,當版本有更新時,修改所有引用鏈接也是一件與工程管理相悖的事,至少我們需要一個可以“查找 - 替換”的工具來自動化的解決版本號修改的問題。

      對付這個問題,目前來說最優方案就是基于文件內容的 hash 版本冗余機制 了。也就是說,我們希望工程師源碼是這么寫的:

    <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 值并生成相應的文件將是一項非常繁瑣且容易出錯的工作。因此,我們需要借助工具。有了這樣的思路,我們下面就來了解一下 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 等具有資源加載功能的標簽。

      為了工程上的維護方便,我們希望工程師在源碼中寫的是相對路徑,而工具可以將其替換為線上的絕對路徑,從而避免相對路徑定位錯誤的問題(比如 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

      工程師根據“減少 HTTP 請求”的優化原則合并了資源

    image6

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

    image7

      C 模塊不再需要了,注釋掉吧!但 C 資源通常不敢輕易剔除

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

      事實上,使用工具在線下進行靜態資源合并是無法解決資源按需加載的問題的。如果解決不了按需加載,則勢必會導致資源的冗余;此外,線下通過工具實現的資源合并通常會使得資源加載和使用的分離,比如在頁面頭部或配置文件中寫資源引用及合并信息,而用到這些資源的 html 組件寫在了頁面其他地方,這種書寫方式在工程上非常容易引起維護不同步的問題,導致使用資源的代碼刪除了,引用資源的代碼卻還在的情況。因此,在工業上要實現資源合并至少要滿足如下需求:

    1. 確實能減少 HTTP 請求,這是基本要求(合并)
    2. 在使用資源的地方引用資源(就近依賴),不使用不加載(按需)
    3. 雖然資源引用不是集中書寫的,但資源引用的代碼最終還能出現在頁面頭部(css)或尾部(js)
    4. 能夠避免重復加載資源(去重)

      將以上要求綜合考慮,不難發現,單純依靠前端技術或者工具處理的是很難達到這些理想要求的。現代大型 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 團隊的總結,我們發現模板層面只要實現三個開發接口,既可以比較完美的實現目前遺留的大部分性能優化原則,這三個接口分別是:

    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": {}
    }

      基于這張表,我們就很容易實現 {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 應用的資源合并做法。但它也存在一些缺陷:

    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. 配置可以由工程師根據經驗人工維護,也可以由統計日志生成,這為性能優化自適應網站設計提供了非常好的基礎。

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

    image10

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

    優化方向優化手段
    請求數量 拆分初始化負載
    請求帶寬 拆分初始化負載
    緩存利用 使 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 開源項目中進行反饋和討論。

    33
    1
    標簽:前端

    Web前端熱門文章

      Web前端最新文章

        最新新聞

          熱門新聞

            黄色网_免费在线黄色电影_黄色成人快播电影_伦理电影_黄色片