[TOC]
## 一 、什么是首屏?
以800x600像素尺寸為標準,當瀏覽器加載頁面后看到第一眼的顯示內容為首屏。而從開始加載到瀏覽器頁面顯示高度達到600像素且此區域有內容顯示的時間為首屏顯示時間。
以京東首頁為例:
當我們打開京東時,第一眼看到的內容即為首屏內容,也就是如上圖的內容。
## 二、為什么要做首屏優化
一個頁面的“總加載時間”要比“首屏時間”長,但對于最終用戶體驗而言,當內容充滿首屏的區域時,用戶就可以看到網站的主要內容并可以進行各自的選擇了。首屏時間的快與慢,直接影響到了用戶對網站的認知度。
所以首屏時間的長短對于用戶的滯留時間的長短、用戶轉化率都尤為重要。
## 三、性能指標
### 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 并沒有讓用戶產生等待的焦慮感。
## 三、優化方案
### 京東
#### 緩存腳本和數據
打開京東的網站(不要滾動鼠標和鍵盤),右鍵查看源代碼會發現京東首頁的DOM樹出奇的簡單,頁面DOM中多含有mod_lazyload的類
~~~
<div class="J_f J_lazyload J_f_lift mod_lazyload need_ani chn" id="portal_8" data-backup="basic_8" data-source="cms:basic_8" data-tpl="portal_tpl">
~~~
再看localStorage

尤其是觀察location下面的鍵值對,會發現它們的值中多存在一串完整的類似于html的內容

由上面的結構我們可知jd.com已經將它們的頁面結構放到了localstorage,不難想象這只是它頁面中的其中一個模塊的內容。
把需要請求的路徑寫在 dom 上(例如:data-tpl="elevator_tpl"),用戶滾動時,一旦該模塊進入了視窗,則請求 dom 上對應的 data-tpl 地址,拿到渲染這個模塊所需要的腳本和數據,不過這中間還有一層本地緩存 localstorage,如果在本地緩存中匹配到了對應的 hash string 內容,則直接渲染,否則請求到數據之后更新本地緩存。localstorage中的 version 會在頁面加載時候,與后端文件 hash相對比,hash不變直接取localstorage中的內容(當然也可以使用cookie判斷版本)。
這里其實存在兩個請求,一個請求是加載數據和腳本,而這里的內容是:
為啥不在返回的內容中直接把腳本也輸出出來?為了讓數據充分緩存下了不少功夫。數據的變化頻率比較高,如果數據和初始化腳本包裝在一起,雖然節約了一個請求,但一旦數據變化,整個腳本都得重新加載,而將數據和腳本分離,腳本可以長期緩存在本地,單獨請求數據,這個量會小很多。直接改變上面的 version 版本號便可以讓瀏覽器重新請求最新腳本。
從上面可以看出,任何一個模塊的改動,在前端只會引起一個較小的加載變化,加上 http 的緩存策略,服務器的壓力也是很小的。
#### CSS
為了求快,首頁是沒有css外鏈的,這樣會再發起多次請求。
頁面切分為模塊化加載,對應模塊下的css交給js或jsonp請求返回。
<br>
#### JS加載
京東采用請求的方式減少了與服務器交互的時間
~~~
<script src="//misc.360buyimg.com/??/jdf/lib/jquery-1.6.4.js,/jdf/2.0.0/ui/ui/1.0.0/ui.js,/mtd/pc/index/gb/lib.min.js,/mtd/pc/base/1.0.0/base.js,/mtd/pc/common/js/o2_ua.js,/mtd/pc/index/home/index.min.js,/mtd/pc/index/home/init.min.js"></script>
~~~
<br>
#### js如何執行?
懶執行,有交互才執行,有興趣的可以看看小胡子哥的[淘寶首頁性能優化實踐](http://www.barretlee.com/blog/2016/04/01/optimization-in-taobao-homepage/)這篇文章
<br>
#### 圖片如何處理?
圖片在其他屏(非首屏)都采用懶加載的模式,這樣既能節省流量,也能減少請求數或延遲請求數。
<br>
#### 服務器
* 少量靜態文件的域名,圖片與iconfont均是放在同一個域下,減少DNS的解析事件,最好做到域名收斂
* 模塊化接口的支持
* 首屏內容最好做到靜態緩存
<br>
<br>
### 淘寶
#### 關鍵模塊優先加載
不論用戶首屏的面積有多大,保證關鍵模塊優先加載。下面代碼片段是初始化所有模塊的核心部分:
~~~
$('.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 十秒之后去請求,然后將請求的結果緩存到本地,當用戶第二次訪問淘寶首頁時能夠看到效果。這是一種更懶的執行,用戶刷新頁面才看得到.這種優化是產品能夠接受,也是技術上合理的優化手段。
#### 圖片尺寸的控制和懶加載
~~~
<img src='//g.alicdn.com/s.gif' data-src='//g.alicdn.com/real/path/to/img.png' />
~~~
阿里 CDN 是支持對圖片尺寸做壓縮處理的,如下圖為 200x200 尺寸的圖片:

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

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

100x100 webp(不支持 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 天,淘寶首頁主要采用本地緩存的方式。
#### 從Chrome中找出優化點

* 在 1.0s 左右存在一次 painting 阻塞,可能因為一次性展示的模塊面積過大
* 從 FPS 的柱狀圖可以看出,在 1.5s-2.0s 之間,存在幾次 Render 和 JavaScript 丟幀
* 從多出的紅點可以看出頁面 jank 次數,也能夠定位到代碼堆棧
在優化的過程中需要更多地思考,如何讓阻塞的腳本分批執行,如何將長時間執行的腳本均勻地分配到時間線上。這些優化都體現在代碼的細節上,宏觀上的處理難以有明顯的效果。當然,在宏觀上,淘寶首頁也有一個明顯的優化:
~~~
// //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);
};
}
})();
~~~
這段代碼基本保證每個模塊的初始化都是在瀏覽器空閑時期,減少了很多不必要的丟幀。這個優化也可以被應用到每個模塊的細節代碼之中,不過優化難度會更高。
## 參考資料
[淘寶首頁性能優化實踐](https://www.barretlee.com/blog/2016/04/01/optimization-in-taobao-homepage/)
[關于首屏性能優化的總結【原創】](https://www.cnblogs.com/jingh/p/6531105.html)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼