1. 程式人生 > >php網站速度效能優化(轉)

php網站速度效能優化(轉)

一個網站的訪問開啟速度至關重要,特別是首頁的開啟載入過慢是致命性的,本文介紹關於php網站效能優化方面的實戰案例:淘寶首頁載入速度優化實踐 。想必很多人都已經看到了新版的淘寶首頁,它與以往不太一樣,這一版頁面中四處彌散著個性化的味道,由於獨特的個性化需求,前端也面臨各方面的技術挑戰:

 

  • 資料來源多

  • 序列請求渲染一個模組

  • 運營資料和個性化資料匹配和管理

  • 資料兜底容災

本次淘寶首頁改版,雖已不再支援 IE6 和 IE7 等低版本的古董瀏覽器,但依然存在多個影響首頁效能的因素:

  • 依賴系統過多,資料的請求分為三塊,其一是靜態資源(如 js/css/image/iconfont 等);其二是推到 CDN 的靜態資料(如運營填寫的資料、前端配置資訊等);其三是後端介面,不同的模組對應不同的業務,而且頁面中還有不少的廣告內容,粗略估計頁面剛載入時首屏發出的介面請求就有 8 個,滾到最底下,得發出 20 多個請求。

  • 無法直接輸出首屏資料,首屏很多資料是通過非同步請求獲取的,由於系統限制,這些請求不可避免,而且請求個數較多,十分影響首屏時間。

  • 模組過多,為了能夠在後臺隔離運營之間填寫資料的許可權,模組必須做細粒度的拆分,如下圖所示:
    多模組的拆分
    一個簡單的模組必須拆分成多個行業小模組,頁面中其他位置也是如此,而且這些被拆分出來的模組還不一定會展現出來,需要讓演算法告訴前端展示哪些模組。

  • 圖片過多,翻頁往下滾動,很明顯看到,頁面整屏整屏的圖片,有些圖片是運營填寫,有些圖片由個性化介面提供,這些圖片都沒有固定的尺寸。

網頁效能衡量指標

網頁效能衡量指標有很多,倘若能夠把握關鍵的幾個,集中優化,效能自然也就上去了。

FPS

最能反映頁面效能的一個指標是 FPS(frame per second),一般系統設定螢幕的重新整理率為 60fps,當頁面元素動畫、滾動或者漸變時繪製速率小於 60,就會不流暢,小於 24 就會卡頓,小於 12 基本認定卡爆了。

1 幀的時長約 16ms,除去系統上下文切換開銷,每一幀中只留給我們 10ms 左右的程式處理時間,如果一段指令碼的處理時間超過 10ms,那麼這一幀就可以被認定為丟失,如果處理時間超過 26ms,可以認定連續兩幀丟失,依次類推。我們不能容忍頁面中多次出現連續丟失五六幀的情況,也就是說必須想辦法分拆執行時間超過 80ms 的程式碼程式,這個工作並不輕鬆。

頁面在剛開始載入的時候,需要初始化很多程式,也可能有大量耗時的 DOM 操作,所以前 1s 的必要操作會導致幀率很低,我們可以忽略。當然,這是對 PC 而言,Mobile 內容少,無論是 DOM 還是 JS 指令碼量都遠小於 PC,1s 可能就有點長了。

DOMContentLoaded 和 Load

DOM 載入並且解析完成才會觸發 DOMContentLoaded 事件,倘若原始碼輸出的內容過多,客戶端解析 DOM 的時間也會響應加長,不要小看這裡的解析時間,如果 DOM 數量增加 2000 個並且巢狀層級較深,解析時間也會相應增加 50-200ms,這個消耗對大多數頁面來說其實是沒必要的,保證首屏輸出即可,後續的內容只保留鉤子,利用 JS 動態渲染。

Load 時間可以用來衡量首屏載入中,客戶端接受的資訊總量,如果在首屏中充滿了大尺寸圖片或者客戶端與後端建立連線次數較多,Load 時間也會相應被拖長。

流暢度

流暢度是對 FPS 的視覺反饋,FPS 值越高,視覺呈現越流暢。為了保障頁面的載入速度,很多內容不會在頁面開啟的時候全部載入到客戶端。這裡提到的流暢度是等待過程中的視覺緩衝,如下方是 Google Plus 頁面的一個效果圖:

Google Plus Item

牆內訪問 google 的速度不是很快,上面元素中的的很多內容都是通過非同步方式載入,而從上圖可以看出 Google 並沒有讓使用者產生等待的焦慮感。

淘寶首頁的效能優化

由於平臺限制,淘寶首頁面臨一個先天的效能缺陷,首屏的渲染需要從 7 個不同的後端取資料,這些資料請求是難以合併的,如果使用者螢幕比較大,則首屏的面積也比較大,對應的後端平臺數據介面就更多。資料是個性化內容或者為廣告內容,故請求也不能快取。

關鍵模組優先

不論使用者首屏的面積有多大,保證關鍵模組優先載入。下面程式碼片段是初始化所有模組的核心部分:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

$('.J_Module').each(function(mod) {  var $mod = $(mod);  var name = $mod.attr('tms');  var data = $mod.attr('tms-data');  if($mod.hasClass('tb-pass')) {

    Reporter.send({

      msg: "跳過模組 " + name

    });    return;

  // 保證首屏模組先載入

  if (/promo|tmall|tanx|notice|member/.test(name)) {

    window.requestNextAnimationFrame(function(){      // 最後一個引數為 Force, 強制渲染, 不懶載入處理

      new Loader($mod, data, /tanx/.test(name));

    });

  } else {    // 剩下的模組進入懶載入佇列    lazyQueue.push({

      $mod: $mod,

      data: data,

      force: /fixedtool|decorations|bubble/.test(name)

    });

  }

});

TMS 輸出的模組都會包含一個 .J_Module 鉤子,並且會預先載入 js 和 css 檔案。

對於無 JS 內容的模組,會預先打上 tb-pass 的標記,初始化的時候跳過此模組;對於首屏模組關鍵模組,會直接進入懶載入監控:

1

2

3

// $box 進入瀏覽器視窗後渲染// new Loader($box, data) ->datalazyload.addCallback($box, function() {

  self.loadModule($box, data);

});// $box 立即渲染// new Loader($box, data, true) ->self.loadModule($box, data);

除必須立即載入的模組外,關鍵模組被加到懶載入監控,原因是,部分使用者進入頁面就可能急速往下拖拽頁面,此時,沒必要渲染這些首屏模組。

非關鍵模組統一送到 lazyQueue 佇列,沒有基於將非關鍵模組加入到懶載入監控,這裡有兩個原因:

  • 一旦加入監控,程式滾動就需要對每個模組做計算判斷,模組太多,這裡可能存在效能損失

  • 如果關鍵模組還沒有載入好,非關鍵模組進入視窗就會開始渲染,這勢必會影響關鍵模組的渲染

那麼,什麼時候開始載入非關鍵模組呢?

1

2

3

4

5

6

7

8

9

10

11

12

var lazyLoaded = false;function runLazyQueue() {  if(lazyLoaded) {    return;

  }

  lazyLoaded = true;

  $(window).detach("mousemove scroll mousedown touchstart touchmove keydown resize onload", runLazyQueue);  var module;  while (module = lazyQueue.shift()) {    ~function(m){      // 保證在瀏覽器空閒時間處理 JS 程式, 保證不阻塞

      window.requestNextAnimationFrame(function() {        new Loader(m.$mod, m.data, m.force);

      });

    }(module);

  }

}

$(window).on("mousemove scroll mousedown touchstart touchmove keydown resize onload", runLazyQueue);// 擔心未觸發 onload 事件, 5s 之後執行懶載入佇列window.requestNextAnimationFrame(function() {

  runLazyQueue();

}, 5E3);

上面的程式碼應該十分清晰,兩種請求下會開始將非關鍵模組加入懶載入監控:

  • 當頁面中觸發 mousemove scroll mousedown touchstart touchmove keydown resize onload 這些事件的時候,說明使用者開始與頁面互動了,程式必須開始載入。

  • 如果使用者沒有互動,但是頁面已經 onload 了,程式當然不能浪費這個絕佳的空檔機會,趁機載入內容;經測試,部分情況下,onload 事件沒有觸發(原因尚不知),所以還設定了一個超時載入,5s 之後,不論頁面載入情況如何,都會將剩下的非關鍵模組加入到懶載入監控。

懶執行,有互動才執行

如果說上面的優化叫做懶載入,那麼這裡的優化可以稱之為懶執行。

首頁上有幾個模組是包含互動的,如頭條區域的 tab ,便民服務的浮層和主題市場的浮層,部分使用者進入頁面可能根本不會使用這些功能,所以程式上並沒有對這些模組做徹底的初始化,而是等到使用者 hover 到這個模組上再執行全部邏輯。

更懶的執行,重新整理頁面才執行

首屏中有兩個次要請求,一個是主題市場的 hot 標,將使用者最常逛的三個類目打標;第二個是個人中心的背景,不同的城市會展示不同的背景圖片,這裡需要請求拿到城市資訊。

這兩處的渲染策略都是,在程式的 idle(空閒)時期,或者 window.onload 十秒之後去請求,然後將請求的結果快取到本地,當用戶第二次訪問淘寶首頁時能夠看到效果。這是一種更懶的執行,使用者重新整理頁面才看得到.這種優化是產品能夠接受,也是技術上合理的優化手段。

圖片尺寸的控制和懶載入

不論圖片連結的來源是運營填寫還是介面輸出,都難以保證圖片具備恰當的寬高,加上如今 retina 的螢幕越來越多,對於這種使用者也要提供優質的視覺體驗,圖片這塊的處理並不輕鬆。

<img src='//g.alicdn.com/s.gif' src='//g.alicdn.com/real/path/to/img.png' />

阿里 CDN 是支援對圖片尺寸做壓縮處理的,如下圖為 200×200 尺寸的圖片:

加上 _100x100.jpg 的引數後,會變成小尺寸:

我們知道 webp 格式的圖片比對應的 jpg 要小三分之一,如上圖加上 _.webp 引數後:

視覺效果並沒有什麼折扣,但是圖片體積縮小了三分之一,圖片越大,節省的越明顯。顯然,淘寶首頁的所有圖片都做了如上的限制,針對坑位大小對圖片做壓縮處理,只是這裡需要注意的是,運營填寫的圖片可能已經是壓縮過的,如:

1

$img = '//g.alicdn.com/real/path/to/img.png_400x400.jpg';<img src='{{$img}}_100x100jpg_.webp' />

上面這種情況,圖片是不會正確展示的。首頁對所有的圖片的懶載入都做了統一的函式處理:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

src = src.replace(/\s/g, '');var arr;if (/(_\d{2,}x\d{2,}\w*?\.(?:jpg|png)){2,}/.test(src) && src.indexOf('_!!') == -1) {

  arr = src.split('_');  if (arr[arr.length - 1] == '.webp') {

    src = [arr[0], arr[arr.length - 2], arr[arr.length - 1]].join('_');

  } else {

    src = [arr[0], arr[arr.length - 1]].join('_');

  }

}if (src.indexOf('_!!') > -1) {

  src = src.replace(/((_\d{2,}x\d{2,}[\w\d]*?|_co0)\.(jpg|png))+/, '$1');

}

WebP.isSupport(function(isSupportWebp) {  // https 協議訪問存在問題 IE8,去 schema

  if (/^http:/.test(src)) {

    src = src.slice(5);

  // 支援 webp 格式,並且 host 以 taobaocdn 和 alicdn 結尾,並且不是 s.gif 圖片

  if (isSupportWebp && /(taobaocdn|alicdn)\.com/.test(src) && (src.indexOf('.jpg') ||

    src.indexOf('.png')) && !/webp/.test(src) && !ignoreWebP && !/\/s\.gif$/.test(src)) {

    src += '_.webp';

  }

  $img.attr('src', src);

});

模組去鉤子,走配置

TMS 的模組在輸出的時候會將資料的 id 放在鉤子上:

1

<p class='J_Module' tms-datakey='2483'></p>

如果模組是非同步展示的,可以通過 tms-datakey 找到模組資料,而首頁的個性化是從幾十上百個模組中通過演算法選出幾個,如果把這些模組鉤子全部輸出來,雖說取資料方便了很多,卻存在大量的冗餘,對此的優化策略是:將資料格式相同的模組單獨拿出來,新建頁面作為資料頁。所以可以在原始碼中看到好幾段這樣的配置資訊:

1

<textarea class="tb-hide">[{"backup":"false","baseid":"1","mid":"222726","name":"iFashion","per":"false","tid":"3","uid":"1000"},{"backup":"false","baseid":"3","mid":"222728","name":"美妝秀","per":"false","tid":"3","uid":"1001"},{"backup":"false","baseid":"4","mid":"222729","name":"愛逛街","per":"false","tid":"4","uid":"1002"},{"backup":"false","baseid":"2","mid":"222727","name":"全球購","per":"false","tid":"4","uid":"1003"}]</textarea>

減少了大量的原始碼以及對 DOM 的解析。

低頻修改模組,快取請求

有一些模組資料是很少被修改的,比如介面的兜底資料、阿里 APP 模組資料等,可以通過調整引數,設定模組的快取時間,如:

1

2

3

4

5

6

7

io({

  url: URL,

  dataType: 'jsonp',

  cache: true,

  jsonpCallback: 'jsonp' + Math.floor(new Date / (1000 * 60)),

  success: function() {    //...  }

});

Math.floor(new Date / (1000 * 60)) 這個數值在一分鐘內是不會發生變化的,也就是說將這個請求在本地快取一分鐘,對於低頻修改模組,快取時間可以設定為一天,即:

 

1

Math.floor(new Date / (1000 * 60 * 60 * 24))

當然,我們也可以採用本地儲存的方式快取這個模組資料:

1

offline.setItem('cache-moduleName', JSON.stringify(data), 1000 * 60 * 60 * 24);

快取過期時間設定為 1 天,淘寶首頁主要採用本地快取的方式。

使用緩動效果減少等待的焦急感

這方面的優化不是很多,但是也有一點效果,很多模組的展示並不是乾巴巴的 .show(),而是通過動畫效果,緩動呈現,這方面的優化推薦使用 CSS3 屬性去控制,效能消耗會少很多。

優化的思考角度

頁面優化的切入點很多,我們不一定能夠面面俱到,但是對於一個承載較大流量的頁面來說,下面幾條必須有效執行:

  • 首屏一定要快

  • 滾屏一定要流暢

  • 能不載入的先別加載

  • 能不執行的先別執行

  • 漸進展現、圓滑展現

當然,效能優化的切入角度不僅僅是上幾個方面,對照 Chrome 的 Timeline 柱狀圖和折線圖,我們還可以找到很多優化的點,如:

淘寶首頁 Chrome Timeline

  • 在 1.0s 左右存在一次 painting 阻塞,可能因為一次性展示的模組面積過大

  • 從 FPS 的柱狀圖可以看出,在 1.5s-2.0s 之間,存在幾次 Render 和 JavaScript 丟幀

  • 從多出的紅點可以看出頁面 jank 次數,也能夠定位到程式碼堆疊

在優化的過程中需要更多地思考,如何讓阻塞的指令碼分批執行,如何將長時間執行的指令碼均勻地分配到時間線上。這些優化都體現在程式碼的細節上,巨集觀上的處理難以有明顯的效果。當然,在巨集觀上,淘寶首頁也有一個明顯的優化:\

1

2

3

4

5

6

7

8

9

10

11

12

13

// https://gist.github.com/miksago/3035015#file-raf-js(function() {  var lastTime = 0;  var vendors = ['ms', 'moz', 'webkit', 'o'];  for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {

    window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];

    window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];

  if (!window.requestAnimationFrame) {

    window.requestAnimationFrame = function(callback, element) {      var currTime = new Date().getTime();      var timeToCall = Math.max(0, 16 - (currTime - lastTime));      var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);

      lastTime = currTime + timeToCall;      return id;

    };

  if (!window.cancelAnimationFrame) {

    window.cancelAnimationFrame = function(id) {

      clearTimeout(id);

    };

  }

})();

這段程式碼基本保證每個模組的初始化都是在瀏覽器空閒時期,減少了很多不必要的丟幀。這個優化也可以被應用到每個模組的細節程式碼之中,不過優化難度會更高。

小結

程式碼的效能優化是一個精細活,如果你要在一個龐大的未經優化的頁面上做效能優化,可能會面臨一次重構程式碼。本文從php網站效能優化引出的問題出發,依實戰淘寶首頁速度優化提升實戰為案例,從微觀到巨集觀講述了頁面的優化實踐,提出了幾條可以借鑑的優化標準,希望對你有所啟發。優化的細節點描述的不夠完善也不夠全面,但是都是值得去優化的方向。