淘寶首頁性能優化實踐

BonCorneliu 8年前發布 | 14K 次閱讀 性能優化 前端技術

上文 《一起來看看淘寶首頁的個性化》 中,帶大家看了下彌散著個性化味道的新首頁,前端面臨著:

  • 數據來源多
  • 串行請求渲染一個模塊
  • 運營數據和個性化數據匹配和管理
  • 數據兜底容災

等多個問題。本次淘寶首頁改版,雖已不再支持 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 的速度不是很快,上面元素中的的很多內容都是通過異步方式加載,而從上圖可以看出 Google 并沒有讓用戶產生等待的焦慮感。

淘寶首頁的性能優化

由于平臺限制,淘寶首頁面臨一個先天的性能缺陷,首屏的渲染需要從 7 個不同的后端取數據,這些數據請求是難以合并的,如果用戶屏幕比較大,則首屏的面積也比較大,對應的后端平臺數據接口就更多。數據是個性化內容或者為廣告內容,故請求也不能緩存。

關鍵模塊優先

不論用戶首屏的面積有多大, 保證關鍵模塊優先加載 。下面代碼片段是初始化所有模塊的核心部分:

$('.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 的標記,初始化的時候跳過此模塊;對于首屏模塊關鍵模塊,會直接進入懶加載監控:

// $box 進入瀏覽器視窗后渲染
// new Loader($box, data) ->
datalazyload.addCallback($box, function() {
  self.loadModule($box, data);
});
 
// $box 立即渲染
// new Loader($box, data, true) ->
self.loadModule($box, data);

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

非關鍵模塊統一送到 lazyQueue 隊列,沒有基于將非關鍵模塊加入到懶加載監控,這里有兩個原因:

  • 一旦加入監控,程序滾動就需要對每個模塊做計算判斷,模塊太多,這里可能存在性能損失
  • 如果關鍵模塊還沒有加載好,非關鍵模塊進入視窗就會開始渲染,這勢必會影響關鍵模塊的渲染

那么,什么時候開始加載非關鍵模塊呢?

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 的屏幕越來越多,對于這種用戶也要提供優質的視覺體驗,圖片這塊的處理并不輕松。

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

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

加上 _100x100.jpg 的參數后,會變成小尺寸:

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

(不支持 webp 格式的瀏覽器展示不出來這張圖片)

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

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

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

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 放在鉤子上:

<divclass='J_Module' tms-datakey='2483'></div>

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

<textareaclass="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 模塊數據等,可以通過調整參數,設置模塊的緩存時間,如:

io({
  url: URL,
  dataType: 'jsonp',
  cache: true,
  jsonpCallback: 'jsonp' + Math.floor(new Date / (1000 * 60)),
  success: function() {
    //...
  }
});

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

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

當然,我們也可以采用本地儲存的方式緩存這個模塊數據:

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

緩存過期時間設置為 1 天,淘寶首頁主要采用本地緩存的方式。

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

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

優化的思考角度

上文 《一起來看看淘寶首頁的個性化》 中提到幾個黃金法則:

  • 首屏一定要快
  • 滾屏一定要流暢
  • 能不加載的先別加載
  • 能不執行的先別執行
  • 漸進展現、圓滑展現

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

  • 在 1.0s 左右存在一次 painting 阻塞,可能因為一次性展示的模塊面積過大
  • 從 FPS 的柱狀圖可以看出,在 1.5s-2.0s 之間,存在幾次 Render 和 JavaScript 丟幀
  • 從多出的紅點可以看出頁面 jank 次數,也能夠定位到代碼堆棧

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

// 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);
    };
  }
})();

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

小結

代碼的性能優化是一個精細活,如果你要在一個龐大的未經優化的頁面上做性能優化,可能會面臨一次重構代碼。本文從淘寶首頁個性化引出的問題出發,從微觀到宏觀講述了頁面的優化實踐,提出了幾條可以借鑒的「黃金法則」,希望對你有所啟發,后續會繼續給大家帶來淘寶首頁穩定性保障的分享。

 

來自: http://web.jobbole.com/85551/

 

 本文由用戶 BonCorneliu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!