前端工程與性能優化

分享自https://github.com/fouber/blog/issues/3javascript

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

高性能網站建設指南

然而,對於構建大型web應用的團隊來講,要堅持貫徹這些優化原則並非一件十分容易的事。由於優化原則中不少要求是與工程管理相違背的,好比 把css放在頭部 和 把js放在尾部 這兩條原則,咱們不能讓團隊的工程師在寫樣式和腳本引用的時候都去修改一個相同的頁面文件。這樣作會嚴重影響團隊成員間並行開發的效率,尤爲是在團隊有版本管理的狀況下,天天要花大量的時間進行代碼修改合併,這項成本是難以接受的。所以在前端工程界,總會看到週期性的性能優化工做,辛勤的前端工程師們每到月圓之夜就會傾巢出動根據優化原則作一次性能優化。css

性能優化是一個工程問題html

本文將從一個全新的視角來思考web性能優化與前端工程之間的關係,揭示前端性能優化在前端架構及開發工具設計層面的實現思路。前端

性能優化原則及分類

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

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

優化方向 優化手段
請求數量 合併腳本和樣式表,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在工程維護方面的難題。使用「查找-替換」思路,咱們彷佛也能夠很好的實現 劃分主域 原則。git

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

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

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

靜態資源版本更新與緩存

緩存利用 分類中保留了 添加Expires頭 和 配置ETag 兩項。或許有些人會質疑,明明這兩項只要配置了服務器的相關選項就能夠實現,爲何說它們難以解決呢?確實,開啓這兩項很容易,但開啓了緩存後,咱們的項目就開始面臨另外一個挑戰: 如何更新這些緩存?

相信大多數團隊也找到了相似的答案,它和《高性能網站建設指南》關於「添加Expires頭」所說的原則同樣——修訂文件名。即:

最有效的解決方案是修改其全部連接,這樣,全新的請求將從原始服務器下載最新的內容。

思路沒錯,但要怎麼改變連接呢?變成什麼樣的連接纔能有效更新緩存,又能最大限度避免那些沒有修改過的文件緩存不失效呢?

先來看看如今通常前端團隊的作法:

<h1>hello world</h1>

<script type="text/javascript" src="a.js?t=201404231123"></script>
<script type="text/javascript" src="b.js?t=201404231123"></script>
<script type="text/javascript" src="c.js?t=201404231123"></script>
<script type="text/javascript" src="d.js?t=201404231123"></script>
<script type="text/javascript" src="e.js?t=201404231123"></script>

ps: 也有團隊採用構建版本號爲靜態資源請求添加query,它們在本質上是沒有區別的,在此就不贅述了。

接下來,項目升級,好比頁面上的html結構發生變化,對應還要修改 a.js 這個文件,獲得的構建結果以下:

<header>hello world</header>

<script type="text/javascript" src="a.js?t=201404231826"></script>
<script type="text/javascript" src="b.js?t=201404231826"></script>
<script type="text/javascript" src="c.js?t=201404231826"></script>
<script type="text/javascript" src="d.js?t=201404231826"></script>
<script type="text/javascript" src="e.js?t=201404231826"></script>

爲了觸發用戶瀏覽器的緩存更新,咱們須要更改靜態資源的url地址,若是採用構建信息(時間戳、版本號等)做爲url修改的依據,如上述代碼所示,咱們只修改了一個a.js文件,但再次構建會讓全部請求都更改了url地址,用戶再度訪問頁面那些沒有修改過的靜態資源的(b.js,b.js,c.js,d.js,e.js)的瀏覽器緩存也一同失效了。

使用構建信息做爲靜態資源更新標記會致使每次構建發佈後全部靜態資源都被迫更新,瀏覽器緩存利用率下降,給性能帶來傷害。

此外,採用添加query的方式來清除緩存還有一個弊端,就是 覆蓋式發佈 的上線問題。

覆蓋式發佈

採用query更新緩存的方式實際上要覆蓋線上文件的,index.html和a.js總有一個前後的順序,從而中間出現一段或大或小的時間間隔。尤爲是當頁面是後端渲染的模板的時候,靜態資源和模板是部署在不一樣的機器集羣上的,上線的過程當中,靜態資源和頁面文件的部署時間間隔可能會很是長,對於一個大型互聯網應用來講即便在一個很小的時間間隔內,都有可能出現新用戶訪問。在這個時間間隔中,訪問了網站的用戶會發生什麼狀況呢?

  1. 若是先覆蓋index.html,後覆蓋a.js,用戶在這個時間間隙訪問,會獲得新的index.html配合舊的a.js的狀況,從而出現錯誤的頁面。
  2. 若是先覆蓋a.js,後覆蓋index.html,用戶在這個間隙訪問,會獲得舊的index.html配合新的a.js的狀況,從而也出現了錯誤的頁面。

這就是爲何大型web應用在版本上線的過程當中常常會較集中的出現前端報錯日誌的緣由,也是一些互聯網公司選擇加班到半夜等待訪問低峯期再上線的緣由之一。

對於靜態資源緩存更新的問題,目前來講最優方案就是 基於文件內容的hash版本冗餘機制 了。也就是說,咱們但願項目源碼是這麼寫的:

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

發佈後代碼變成

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

也就是a.js發佈出來後被修改了文件名,產生一個新文件,並非覆蓋已有文件。其中」_82244e91」這串字符是根據a.js的文件內容進行hash運算獲得的,只有文件內容發生變化了纔會有更改。因爲將文件發佈爲帶有hash的新文件,而不是同名文件覆蓋,所以不會出現上述說的那些問題。同時,這麼作還有其餘的好處:

  1. 上線的a.js不是同名文件覆蓋,而是文件名+hash的冗餘,因此能夠先上線靜態資源,再上線html頁面,不存在間隙問題;
  2. 遇到問題回滾版本的時候,無需回滾a.js,只須回滾頁面便可;
  3. 因爲靜態資源版本號是文件內容的hash,所以全部靜態資源能夠開啓永久強緩存,只有更新了內容的文件纔會緩存失效,緩存利用率大增;

以文件內容的hash值爲依據生產新文件的非覆蓋式發佈策略是解決靜態資源緩存更新最有效的手段。

雖然這種方案是相比之下最完美的解決方案,但它沒法經過手工的形式來維護,由於要依靠手工的形式來計算和替換hash值,並生成相應的文件,將是一項很是繁瑣且容易出錯的工做,所以咱們須要藉助工具來處理。

用grunt來實現md5功能是很是困難的,由於grunt只是一個task管理器,而md5計算須要構建工具具備遞歸編譯的能,而不是簡單的任務調度。考慮這樣的例子:

md5計算過程

因爲咱們的資源版本號是經過對文件內容進行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中的引用。

計算index.html中引用的a.css文件的url過程:
1. 壓縮a.png後計算其內容的md5值
2. 將a.png的md5寫入a.css,再壓縮a.css,計算其內容的md5值
3. 將a.css的md5值寫入到index.html中

grunt等task-based的工具是很難在task之間協做處理這樣的需求的。

在解決了基於內容hash的版本更新問題以後,咱們能夠將全部前端靜態資源開啓永久強緩存,每次版本發佈均可以首先讓靜態資源全量上線,再進一步上線模板或者頁面文件,不再用擔憂各類緩存和時間間隙的問題了!

靜態資源管理與模塊化框架

解決了靜態資源緩存問題以後,讓咱們再來看看前面的優化原則表還剩些什麼:

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

很不幸,剩下的優化原則都不是使用工具就能很好實現的。或許有人會辯駁:「我用某某工具能夠實現腳本和樣式表合併」。嗯,必須認可,使用工具進行資源合併並替換引用或許是一個不錯的辦法,但在大型web應用,這種方式有一些很是嚴重的缺陷,來看一個很熟悉的例子 :

第一天

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

次日

工程師根據「減小HTTP請求」的優化原則合併了資源

第三天

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

第四天

C模塊再也不須要了,註釋掉吧!代碼1秒鐘搞定,但C資源一般不敢輕易剔除

後來

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

這個例子來自 Facebook靜態網頁資源的管理和優化@Velocity China 2010

事實上,使用工具在線下進行靜態資源合併是沒法解決資源按需加載的問題的。若是解決不了按需加載,則必會致使資源的冗餘;此外,線下經過工具實現的資源合併一般會使得資源加載和使用的分離,好比在頁面頭部或配置文件中寫資源引用及合併信息,而用到這些資源的html組件寫在了頁面其餘地方,這種書寫方式在工程上很是容易引發維護不一樣步的問題,致使使用資源的代碼刪除了,引用資源的代碼卻還在的狀況。所以,在工業上要實現資源合併至少要知足以下需求:

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

將以上要求綜合考慮,不難發現,單純依靠前端技術或者工具處理是很難達到這些理想要求的。

接下來我會講述一種新的模板架構設計,用以實現前面說到那些性能優化原則,同時知足工程開發和維護的須要,這種架構設計的核心思想就是:

基於依賴關係表的靜態資源管理系統與模塊化框架設計

考慮一段這樣的頁面代碼:

<html>
<head>
    <title>page</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> content of module a </div>
    <div> content of module b </div>
    <div> content of module c </div>
</body>
</html>

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

<html>
<head>
    <title>page</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="a.css"/>
    <div> content of module a </div>

    <link rel="stylesheet" type="text/css" href="b.css"/>
    <div> content of module b </div>

    <link rel="stylesheet" type="text/css" href="c.css"/>
    <div> content of module c </div>
</body>
</html>

固然,把這樣的頁面直接送達給瀏覽器用戶是會有嚴重的頁面閃爍問題的,因此咱們實際上仍然但願最終頁面輸出的結果仍是如最開始的截圖同樣,將css放在頭部輸出。這就意味着,頁面結構須要有一些調整,而且有能力收集資源加載需求,那麼咱們考慮一下這樣的源碼(以php爲例):

<html>
<head>
    <title>page</title>
    <!--[ CSS LINKS PLACEHOLDER ]-->
</head>
<body>
    <?php require_static('a.css'); ?>
    <div> content of module a </div>

    <?php require_static('b.css'); ?>
    <div> content of module b </div>

    <?php require_static('c.css'); ?>
    <div> content of module c </div>
</body>
</html>

在頁面的頭部插入一個html註釋 <!--[CSS LINKS PLACEHOLDER]--> 做爲佔位,而將原來字面書寫的資源引用改爲模板接口 require_static 調用,該接口負責收集頁面所需資源。

require_static接口實現很是簡單,就是準備一個數組,收集資源引用,而且能夠去重。最後在頁面輸出的前一刻,咱們將require_static在運行時收集到的 a.cssb.cssc.css 三個資源拼接成html標籤,替換掉註釋佔位 <!--[CSS LINKS PLACEHOLDER]-->,從而獲得咱們須要的頁面結構。

通過實踐總結,能夠發現模板層面只要實現三個開發接口,就能夠比較完美的實現目前遺留的大部分性能優化原則,這三個接口分別是:

  1. require_static(res_id):收集資源加載需求的接口,參數是靜態資源id。
  2. load_widget(wiget_id):加載拆分紅小組件模板的接口。你能夠叫它爲widget、component或者pagelet之類的。總之,咱們須要一個接口把一個大的頁面模板拆分紅一個個的小部分來維護,最後在原來的頁面中以組件爲單位來加載這些小部件。
  3. script(code):收集寫在模板中的js腳本,使之出現的頁面底部,從而實現性能優化原則中的 將js放在頁面底部 原則。

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

<html>
<head>
    <title>page</title>
    <?php require_static('jquery.js'); ?>
    <?php require_static('bootstrap.css'); ?>
    <?php require_static('bootstrap.js'); ?>
    <!--[ CSS LINKS PLACEHOLDER ]-->
</head>
<body>
    <?php load_widget('a'); ?>
    <?php load_widget('b'); ?>
    <?php load_widget('c'); ?>
    <!--[ SCRIPTS PLACEHOLDER ]-->
</body>
</html>

而最終在模板解析的過程當中,資源收集與去重、頁面script收集、佔位符替換操做,最終從服務端發送出來的html代碼爲:

<html>
<head>
    <title>page</title>
    <link rel="stylesheet" type="text/css" href="bootstrap.css"/>
    <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> content of module a </div>
    <div> content of module b </div>
    <div> content of module c </div>
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="bootstrap.js"></script>
    <script type="text/javascript" src="a.js"></script>
    <script type="text/javascript" src="b.js"></script>
    <script type="text/javascript" src="c.js"></script>
</body>
</html>

不難看出,咱們目前已經實現了 按需加載將腳本放在底部將樣式表放在頭部 三項優化原則。

前面講到靜態資源在上線後須要添加hash戳做爲版本標識,那麼這種使用模板語言來收集的靜態資源該如何實現這項功能呢?

答案是:靜態資源依賴關係表。

考慮這樣的目錄結構:

project
    ├── widget
    │   ├── a
    │   │   ├── a.css
    │   │   ├── a.js
    │   │   └── a.php
    │   ├── b
    │   │   ├── b.css
    │   │   ├── b.js
    │   │   └── b.php
    │   └── c
    │       ├── c.css
    │       ├── c.js
    │       └── c.php
    ├── bootstrap.css
    ├── bootstrap.js
    ├── index.php
    └── jquery.js

若是咱們可使用工具掃描整個project目錄,而後建立一張資源表,同時記錄每一個資源的部署路徑,獲得這樣的一張表:

{
    "res" : {
        "widget/a/a.css" : "/widget/a/a_1688c82.css",
        "widget/a/a.js"  : "/widget/a/a_ac3123s.js",
        "widget/b/b.css" : "/widget/b/b_52923ed.css",
        "widget/b/b.js"  : "/widget/b/b_a5cd123.js",
        "widget/c/c.css" : "/widget/c/c_03cab13.css",
        "widget/c/c.js"  : "/widget/c/c_bf0ae3f.js",
        "jquery.js"      : "/jquery_9151577.js",
        "bootstrap.css"  : "/bootstrap_f5ba12d.css",
        "bootstrap.js"   : "/bootstrap_a0b3ef9.js"
    },
    "pkg" : {}
}

基於這張表,咱們就很容易實現 require_static(file_id)load_widget(widget_id) 這兩個模板接口了。以load_widget爲例:

function load_widget($id){
    //從json文件中讀取資源表
    $map = load_map();
    //查找靜態資源
    $filename = 'widget/' . $id . '/' . $id;
    //查找js文件
    $js = $filename . '.js';
    if(isset($map['res'][$js])) {
        //若是有對應的js資源,就收集起來
        collect_js_static($map['res'][$js]);
    }
    //查找css文件
    $css = $filename . '.css';
    if(isset($map['res'][$css])) {
        //若是有對應的css資源,就收集起來
        collect_css_static($map['res'][$css]);
    }
    include $filename . '.php';
}

利用查表來解決md5戳的問題,這樣,咱們的頁面最終送達給用戶的結果就是這樣的:

<html>
<head>
    <title>page</title>
    <link rel="stylesheet" type="text/css" href="/bootstrap_f5ba12d.css"/>
    <link rel="stylesheet" type="text/css" href="/widget/a/a_1688c82.css"/>
    <link rel="stylesheet" type="text/css" href="/widget/b/b_52923ed.css"/>
    <link rel="stylesheet" type="text/css" href="/widget/c/c_03cab13.css"/>
</head>
<body>
    <div> content of module a </div>
    <div> content of module b </div>
    <div> content of module c </div>
    <script type="text/javascript" src="/jquery_9151577.js"></script>
    <script type="text/javascript" src="/bootstrap_a0b3ef9.js"></script>
    <script type="text/javascript" src="/widget/a/a_ac3123s.js"></script>
    <script type="text/javascript" src="/widget/b/b_a5cd123.js"></script>
    <script type="text/javascript" src="/widget/c/c_bf0ae3f.js"></script>
</body>
</html>

接下來,咱們討論基於表的設計思想上是如何實現靜態資源合併的。或許有些團隊使用過combo服務,也就是咱們在最終拼接生成頁面資源引用的時候,並非生成多個獨立的link標籤,而是將資源地址拼接成一個url路徑,請求一種線上的動態資源合併服務,從而實現減小HTTP請求的需求,好比前面的例子,稍做調整便可獲得這樣的結果:

<html>
<head>
    <title>page</title>
    <link rel="stylesheet" type="text/css" href="/??bootstrap_f5ba12d.css,widget/a/a_1688c82.css,widget/b/b_52923ed.css,widget/c/c_03cab13.css"/>
</head>
<body>
    <div> content of module a </div>
    <div> content of module b </div>
    <div> content of module c </div>
    <script type="text/javascript" src="/??jquery_9151577.js,bootstrap_a0b3ef9.js,widget/a/a_ac3123s.js,widget/b/b_a5cd123.js,widget/c/c_bf0ae3f.js"></script>
</body>
</html>

這個 /??file1,file2,file3,… 的url請求響應就是動態combo服務提供的,它的原理很簡單,就是根據url找到對應的多個文件,合併成一個文件來響應請求,並將其緩存,以加快訪問速度。

這種方法很巧妙,有些服務器甚至直接集成了這類模塊來方便的開啓此項服務,這種作法也是大多數大型web應用的資源合併作法。但它也存在一些缺陷:

  1. 瀏覽器有url長度限制,所以不能無限制的合併資源。
  2. 若是用戶在網站內有公共資源的兩個頁面間跳轉訪問,因爲兩個頁面的combo的url不同致使用戶不能利用瀏覽器緩存來加快對公共資源的訪問速度。
  3. 若是combo的url中任何一個文件發生改變,都會致使整個url緩存失效,從而致使瀏覽器緩存利用率下降。

對於上述第二條缺陷,能夠舉個例子來看說明:

  • 假設網站有兩個頁面A和B
  • A頁面使用了a,b,c,d四個資源
  • B頁面使用了a,b,e,f四個資源
  • 若是使用combo服務,咱們會得:
    • A頁面的資源引用爲:/??a,b,c,d
    • B頁面的資源引用爲:/??a,b,e,f
  • 兩個頁面引用的資源是不一樣的url,所以瀏覽器會請求兩個合併後的資源文件,跨頁面訪問沒能很好的利用a、b這兩個資源的緩存。

很明顯,若是combo服務能聰明的知道A頁面使用的資源引用爲 /??a,b 和 /??c,d,而B頁面使用的資源引用爲 /??a,b 和 /??e,f就行了。這樣當用戶在訪問A頁面以後再訪問B頁面時,只須要下載B頁面的第二個combo文件便可,第一個文件已經在訪問A頁面時緩存好了的。

基於這樣的思考,咱們在資源表上新增了一個字段,取名爲 pkg,就是資源合併生成的新資源,表的結構會變成:

{
    "res" : {
        "widget/a/a.css" : "/widget/a/a_1688c82.css",
        "widget/a/a.js"  : "/widget/a/a_ac3123s.js",
        "widget/b/b.css" : "/widget/b/b_52923ed.css",
        "widget/b/b.js"  : "/widget/b/b_a5cd123.js",
        "widget/c/c.css" : "/widget/c/c_03cab13.css",
        "widget/c/c.js"  : "/widget/c/c_bf0ae3f.js",
        "jquery.js"      : "/jquery_9151577.js",
        "bootstrap.css"  : "/bootstrap_f5ba12d.css",
        "bootstrap.js"   : "/bootstrap_a0b3ef9.js"
    },
    "pkg" : {
        "p0" : {
            "url" : "/pkg/lib_cef213d.js",
            "has" : [ "jquery.js", "bootstrap.js" ]
        },
        "p1" : {
            "url" : "/pkg/lib_afec33f.css",
            "has" : [ "bootstrap.css" ]
        },
        "p2" : {
            "url" : "/pkg/widgets_22feac1.js",
            "has" : [
                "widget/a/a.js",
                "widget/b/b.js",
                "widget/c/c.js"
            ]
        },
        "p3" : {
            "url" : "/pkg/widgets_af23ce5.css",
            "has" : [
                "widget/a/a.css",
                "widget/b/b.css",
                "widget/c/c.css"
            ]
        }
    }
}

相比以前的表,能夠看到新表中多了一個pkg字段,而且記錄了打包後的文件所包含的獨立資源。這樣,咱們從新設計一下 require_static、load_widget 這兩個模板接口,實現這樣的邏輯:

在查表的時候,若是一個靜態資源有pkg字段,那麼就去加載pkg字段所指向的打包文件,不然加載資源自己。

好比執行require_static('bootstrap.js'),查表得知bootstrap.js被打包在了p1中,所以取出p1包的url/pkg/lib_cef213d.js,而且記錄頁面已加載了 jquery.js 和 bootstrap.js 兩個資源。這樣一來,以前的模板代碼執行以後獲得的html就變成了:

<html>
<head>
    <title>page</title>
    <link rel="stylesheet" type="text/css" href="/pkg/lib_afec33f.css"/>
    <link rel="stylesheet" type="text/css" href="/pkg/widgets_af23ce5.css"/>
</head>
<body>
    <div> content of module a </div>
    <div> content of module b </div>
    <div> content of module c </div>
    <script type="text/javascript" src="/pkg/lib_cef213d.js"></script>
    <script type="text/javascript" src="/pkg/widgets_22feac1.js"></script>
</body>
</html>

雖然這種策略請求有4個,不如combo形式的請求少,但可能在統計上是性能更好的方案。因爲兩個lib打包的文件修改的可能性很小,所以這兩個請求的緩存利用率會很是高,每次項目發佈後,用戶須要從新下載的靜態資源可能要比combo請求節省不少帶寬。

性能優化既是一個工程問題,又是一個統計問題。優化性能時若是隻關注一個頁面的首次加載是很片面的。還應該考慮全站頁面間跳轉、項目迭代後更新資源等狀況下的優化策略。

此時,咱們又引入了一個新的問題:如何決定哪些文件被打包?

從經驗來看,項目初期能夠採用人工配置的方式來指定打包狀況,好比:

{
    "pack" : {
        "lib.js"      : [ "jquery.js", "bootstrap.js" ],
        "lib.css"     : "bootstrap.css",
        "widgets.js"  : "widget/**.js",
        "widgets.css" : "widget/**.css"
    }
}

但隨着系統規模的增大,人工配置會帶來很是高的維護成本,此時須要一個輔助系統,經過分析線上訪問日誌和靜態資源組合加載狀況來自動生成這份配置文件,系統設計如圖:

靜態資源分析系統

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

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

拆分初始化負載 的目標是將頁面一開始加載時不須要執行的資源從全部資源中分離出來,等到須要的時候再加載。工程師一般沒有耐心去區分資源的分類狀況,但咱們能夠利用組件化框架接口來幫助工程師管理資源的使用。仍是從例子開始思考,若是咱們有一個js文件是用戶交互後才須要加載的,會怎樣呢:

<html>
<head>
    <title>page</title>
    <?php require_static('jquery.js'); ?>
    <?php require_static('bootstrap.css'); ?>
    <?php require_static('bootstrap.js'); ?>
    <!--[ CSS LINKS PLACEHOLDER ]-->
</head>
<body>
    <?php load_widget('a'); ?>
    <?php load_widget('b'); ?>
    <?php load_widget('c'); ?>

    <?php script('start'); ?>
    <script>
        $(document.body).click(function(){
            require.async('dialog.js', function(dialog){
                dialog.show('you catch me!');
            });
        });
    </script>
    <?php script('end'); ?>

    <!--[ SCRIPTS PLACEHOLDER ]-->
</body>
</html>

很明顯,dialog.js 這個文件咱們不須要在初始化的時候就加載,所以它應該在後續的交互中再加載,但文件都加了md5戳,咱們如何能在瀏覽器環境中知道加載的url呢?

答案就是:把靜態資源表的一部分輸出在頁面上,供前端模塊化框架加載靜態資源。

我就很少解釋代碼的執行過程了,你們看到完整的html輸出就能理解是怎麼回事了:

<html>
<head>
    <title>page</title>
    <link rel="stylesheet" type="text/css" href="/pkg/lib_afec33f.css"/>
    <link rel="stylesheet" type="text/css" href="/pkg/widgets_af23ce5.css"/>
</head>
<body>
    <div> content of module a </div>
    <div> content of module b </div>
    <div> content of module c </div>
    <script type="text/javascript" src="/pkg/lib_cef213d.js"></script>
    <script type="text/javascript" src="/pkg/widgets_22feac1.js"></script>
    <script>
        //將靜態資源表輸出在前端頁面中
        require.config({
            res : {
                'dialog.js' : '/dialog_fa3df03.js'
            }
        });
    </script>
    <script>
        $(document.body).click(function(){
            //require.async接口查表肯定加載資源的url
            require.async('dialog.js', function(dialog){
                dialog.show('you catch me!');
            });
        });
    </script>
</body>
</html>

dialog.js不會在頁面以script src的形式輸出,而是變成了資源註冊,這樣,當頁面點擊觸發require.async執行的時候,async函數纔會查表找到資源的url並加載它,加載完畢後觸發回調函數。

以上框架示例我實現了一個java-jsp版的,有興趣的同窗請看這裏:https://github.com/fouber/fis-java-jsp

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

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

剩下的兩項優化原則要作到並不容易,真正可緩存的Ajax在現實開發中比較少見,而 儘早刷新文檔的輸出 原則facebook在2010年的velocity上 提到過,就是BigPipe技術。當時facebook團隊還講到了Quickling和PageCache兩項技術,其中的PageCache算是比較完全的實現Ajax可緩存的優化原則了。因爲篇幅關係,就不在此展開了,後續還會撰文詳細解讀這兩項技術。

總結

其實在前端開發工程管理領域還有不少細節值得探索和挖掘,提高前端團隊生產力水平並非一句空話,它須要咱們能對前端開發及代碼運行有更深入的認識,對性能優化原則有更細緻的分析與研究。在前端工業化開發的全部環節均有可節省的人力成本,這些成本很是可觀,相信如今不少大型互聯網公司也都有了這樣的共識。

本文只是將這個領域中很小的一部分知識的展開討論,拋磚引玉,但願能爲業界相關領域的工做者提供一些不同的思路。

相關文章
相關標籤/搜索