本文由雲+社群發表
作者:思衍Jax
天下武功,唯 (wei) 快(fu) 不(bu) 破(po)。
隨著近幾年的前端技術的高速發展,越來越多的團隊使用 React、Vue 等 SPA 框架作為其主要的技術棧。以 React 應用為例,從效能角度,其最重要的指標可能就是首屏渲染所花費的時間了。那麼今天,我們要給大家分享的一個把優化做到極致的故事。
我們的目標是讓 H5 的頁面也能夠擁有 Native 般的體驗,如果你還在尋求什麼技術能夠讓老闆虎軀一震(拯救你的KPI),那麼這篇文章或許能夠幫助到你。
企鵝輔導課程詳情頁是什麼
企鵝輔導詳情頁課程詳情頁是騰訊旗下企鵝輔導 APP 中最重要頁面之一,也是流量最大的頁面之一,所以它的開啟速度也是至關重要的。
這是一個使用 React
編寫的 H5 頁面,執行於多端,包括: 企鵝輔導APP
、手機 QQ
、手機瀏覽器
。
架構演變
純非同步渲染
我們知道當前主流的 SPA 的應用的預設渲染方式都是這樣的:
在這種情況下,從載入頁面到使用者看到頁面(首屏渲染所花費的時間)就是上圖中灰色邊框區域所包括的時間。
這是最慢的一種方式,就算 CGI 夠快,最少要花費 1S 到 2S 左右的時間了。
接著我們簡單優化一下:
- 把靜態資源快取起來,這樣下次使用者開啟的時候就不用從網路請求了。
- 第 ④ 步拉取 CGI 這個動作是否可以提前呢?我們可以在請求 HTML 之後,先通過一段 JS 指令碼去請求 CGI 資料,後面第 ④ 步的時候,就可以直接拿到資料了,這就是 CGI 預載入。
怎麼做到呢?我們的方案是統一封裝 Request 請求工具,在用 Webpack 打包的時候,會往頁面頂部注入一段 預載入 CGI 的 JS 程式碼,維護一個CGI 與 DATA 對應 MAP,後面發請求的時候,先去 MAP 裡取值,如果有值的話直接拿出來,沒有的話則發起HTTP 請求。(具體請查閱我們團隊開源的 Preload 工具)
這種模式還有一些其他的優化的方法:
- 在 HTML 內實現 Loading 態或者骨架屏;
- 去掉外聯 css;
- 使用動態 polyfill;
- 使用 SplitChunksPlugin 拆分公共程式碼;
- 正確地使用 Webpack 4.0 的 Tree Shaking;
- 使用動態 import,切分頁面程式碼,減小首屏 JS 體積;
- 編譯到 ES2015+,提高程式碼執行效率,減小體積;
- 使用 lazyload 和 placeholder 提升載入體驗。
效果如下圖所示:
非同步渲染直出同構
在非同步的模式下,除了上述優化,我們還在端內(企鵝輔導 APP、手機 QQ)內做了離線包快取(騰訊手Q方面獨立研發出來的針對手機端優化的方案,簡而言之就是將靜態資源快取在手機 APP 內),經過我們的資料測試,首屏渲染大概能夠達到秒開(1s左右) 的效果。
-w300但對有著效能極致追求的我們來說,肯定是不會滿意的。
繼續優化,最容易、最大眾的套路肯定就是直出(服務端渲染)了。
現在直出的方案已經有很多很多種,這裡也不多做介紹了,如果您想了解更多關於服務端渲染的方案,請參考這篇文章。
直出針對首屏時間的優化效果是非常明顯的,經過我們的測試,資料大概能夠提升**25%**左右。
直出之後的效果如下圖:
直出同構可以看到對於首屏來說,沒有了**【載入中...】**的等待時間,視覺體驗提升了不少。
PWA 直出
PWA針對上述、常見的直出應用來說,我們能夠優化的點在哪裡呢?讓我們來詳細分析一波,這也是今天我們要給大家分享的重點。
首先看看直出應用各個環節的耗時表 (本地環境 2018款 iMac):
過程名稱 | 過程花費 |
---|---|
Node 內 CGI 拉取 | 300 ms |
RenderToString | 20 ms |
網路耗時 | 10 ms |
前端HTML渲染 | 30 ms |
從上面的表中我們看出,直出渲染的耗時的大頭還是在 CGI 介面的拉取上。
我們現在提出兩個問題:
- CGI 介面的資料是否可以快取 ?
- HTML 又是否可以快取 ?
一、介面的動靜分離
動態資訊這個頁面的介面資料中,有一些資料,是實時變動的, 比如:當前還剩多少個名額、此時此刻課程的價格、使用者是否購買過這個課程等。
這些資料的特性決定了這個資料介面不能夠被快取。(假設將其快取,那麼就會存在可能使用者進來看到當前還剩下10個名額,其實課程已經賣光了的情況)
為了這個時間耗時的大頭,我們做了CGI介面的動靜分離。
將與使用者態、當前時間沒有關聯的資料(比如
課程標題
、課程上課的時間
、試聽模組的地址
等)放在一個介面(靜態介面),其他變化的資料放在另一個介面(動態介面)。
那麼可以使用靜態的介面來做服務端渲染,好處是第一比較快(少了動態的資訊,而且後臺也可以做快取),第二 Node 直出可以做快取了。
二、直出 Redis 快取
這樣我們就可以將那部分靜態的、不會經常變動的資料用來直出 HTML,然後將這個 HTML 檔案快取到 Redis 中。
客戶端請求此網頁,Node 端接受到請求之後,先去 Redis 裡拿快取的 HTML,如果 Redis 快取沒有命中,則拉取靜態的 CGI 介面渲染出 HTML存入 Redis。
客戶端拿到 HTML 之後,會立刻渲染,然後再用 JS 去請求動態的資料,渲染到相應的地方。
做完之後我們可以看到優化效果的提升是非常非常明顯的:
直接從 262ms 提升到了 16ms !(本地環境),簡直飛一般的感覺,媽媽再也不用擔心領導看耗時了。
三、PWA 直出快取
關於什麼是 PWA ,以及如何使用,請移步這篇文章。
做了 Node 端直出的 HTML 快取之後,我們接著優化,接著思考,是否可以在客戶端也快取 HTML,這樣連網路延時這部分消耗也省掉呢。
答案就是使用 PWA 在客戶端做離線快取,將我們直出的 HTML 快取在客戶端,每次使用者請求的時候,直接從 PWA 離線快取裡取出對應的直出頁面(HTML)響應給使用者,響應之後緊接著請求 Node 服務更新本地的 PWA 快取。(如下圖所示)
核心程式碼:
self.addEventListener("fetch", event => {
// TODO other logic (maybe fetch filter)
// core logic
event.respondWith(
caches.open(cacheName).then(function(cache) {
return cache.match(cacheCourseUrl).then(function(response) {
var fetchPromise = fetch(cacheCourseUrl).then(function(
networkResponse
) {
if (networkResponse.status === 200) {
cache.put(cacheCourseUrl, networkResponse.clone());
}
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
複製程式碼
廢話不多說,先看效果對比 (左 PWA 直出;右 離線包):
duibi從上圖可以看出,使用了 PWA 直出快取之後,首屏渲染基本是毫秒開,可以說與 Native 並肩了。
經過我們的資料測試,使用 PWA 直出快取,首屏渲染的時間最好可以到400ms左右級別:
PWA 直出細節優化
一、防頁面跳動
因為對介面進行了動靜分離,使用靜態介面直出頁面,然後在客戶端拉取動態資料渲染完。這就可能會導致頁面的抖動(比如詳情頁中的試聽模組,是在客戶端渲染的)。
因為高度改變了,視覺上就會出現抖動(具體可以參考上面章節直出時候的 GIF 截圖)。
要去掉頁面抖動的情況,就必須保證容器的高度在直出時候已經存在了。
比如這個試聽模組,其實這個封面圖和試聽按鈕是可以在服務端渲染出來的,而後面的 Video 模組則必須要在客戶度渲染(騰訊雲 Tcplayer)。
所以這裡可以拆分成:(試聽封面 + 按鈕 + 時間)服務端渲染 + 底層 Video(客戶端渲染)。
有些需要在客戶端計算高度的容器(表現為常放在 ComponentDidMount 裡計算),如果它們依賴客戶端環境(比如依賴當前系統是安卓還是 IOS),就導致他們肯定不能放在服務端直接渲染出來,這又怎麼辦呢?
這裡我們的做法,是將這些計算放在 HTML body 之前,通過內聯的指令碼嵌入,計算出當前環境,給 body 加上一個特定的類(class),然後在這個特定的類下面的元素,就可以通過 css 給予特定的樣式。比如下面程式碼:
/*
* 因為在不同的手機 APP 環境內,頁面的 padding 是不一樣的。
* 我們要在頁面渲染完之前加上相應的 padding
*/
var REGEXP_FUDAO_APP = /EducationApp/;
if (
typeof navigator !== "undefined" &&
REGEXP_FUDAO_APP.test(navigator.userAgent)
) {
if (/Android/i.test(navigator.userAgent)) {
document.body.classList.add("androidFudaoApp");
} else if (/iPhone|iPad|iPod|iOS/i.test(navigator.userAgent)) {
if (window.screen.width === 375 && window.screen.height === 812) {
document.body.classList.add("iphoneXFudaoApp");
} else {
document.body.classList.add("iosFudaoApp");
}
}
}
.androidFudaoApp .tt {
padding-top: 48px;
background-position-y: 84px;
}
.iphoneXFudaoApp .tt {
padding-top: 88px;
background-position-y: 124px;
}
.iosFudaoApp .tt {
padding-top: 64px;
background-position-y: 100px;
}
複製程式碼
然後把這段程式碼通過構建插入到頁面 body 之前。
-w500防抖動優化效果如下 (左優化完,右未優化):
duibi_doudong二、冷啟動預載入
雖然我們做了 PWA 離線快取,但是對於冷啟動來說,客戶端裡面的 PWA 快取還是沒有的,這樣就會導致初次點選頁面,渲染速度相對慢一點。
這裡我們可以在 APP 啟動的時候,用一個預載入的指令碼最大限度的拉取使用者可能訪問的頁面。
核心程式碼如下:
// 預載入頁面時, PWA 預快取課程詳情頁面的直出
function prefetchCache(fetchUrl) {
fetch("https://you preFetch Cgi")
.then(data => {
return data.json();
})
.then(res => {
const { courseInfo = [] } = res.result || {};
courseInfo.forEach(item => {
if (item.cid) {
caches.open(cacheName).then(function(cache) {
fetch(`${courseURL}?course_id=${item.cid}`).then(function(
networkResponse
) {
if (networkResponse.status === 200) {
cache.put(
`${courseURL}?course_id=${item.cid}`,
networkResponse.clone()
);
}
// return networkResponse;
});
});
}
});
})
.catch(err => {
// To monitor err
});
}
複製程式碼
PWA 直出遺留問題
一、相容性問題
隨著 PWA 技術的發展,現今大部分手機以及 PC 環境已經支援對 PWA 進行了支援。經過我們的測試發現:安卓基本上都是支援的,IOS 需要11.3以上才支援。
Service Workers 相容性圖
二、IOS 渲染問題
很多的經驗告訴我們,外聯的 script 標籤要放在 body 的後面,因為它會阻塞頁面的 DOM 渲染。
經過測試發現,IOS 的 WebView
(UIWebView
)渲染機制並不會上述一樣,而是要等到後面的 JS 執行完之後才渲染頁面,如果是這樣,我們的直出渲染優化就沒有效果了(因為 HTML 並不在最開始渲染),這裡可以使用 script
標籤的 async
與 defer
屬性來達到非同步渲染的作用。
升級 WkWebView 之後,情況得到改善,渲染正常。
附錄
參考資料
此文已由作者授權騰訊雲+社群在各渠道釋出
獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號