淘寶首頁性能優化實踐


想必很多人都已經看到了新版的淘寶首頁,它與以往不太一樣,這一版頁面中四處彌散着個性化的味道,由於獨特的個性化需求,前端也面臨各方面的技術挑戰:

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

本次淘寶首頁改版,雖已不再支持 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 個不同的后端取數據,這些數據請求是難以合並的,如果用戶屏幕比較大,則首屏的面積也比較大,對應的后端平台數據接口就更多。數據是個性化內容或者為廣告內容,故請求也不能緩存。

關鍵模塊優先

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

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

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

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

200x200

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

100x100

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

100x100 webp

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

$img = '//g.alicdn.com/real/path/to/img.png_400x400.jpg';

<img src='{{$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 放在鈎子上:

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

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

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

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 柱狀圖和折線圖,我們還可以找到很多優化的點,如:

淘寶首頁 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); }; } })();

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

小結

代碼的性能優化是一個精細活,如果你要在一個龐大的未經優化的頁面上做性能優化,可能會面臨一次重構代碼。本文從淘寶首頁個性化引出的問題出發,從 微觀到宏觀講述了頁面的優化實踐,提出了幾條可以借鑒的優化標准,希望對你有所啟發。優化的細節點描述的不夠完善也不夠全面,但是都是值得去優化的方向。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM