本文由雲+社區發表
作者:思衍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 之后,情況得到改善,渲染正常。
附錄
參考資料
此文已由作者授權騰訊雲+社區在各渠道發布
獲取更多新鮮技術干貨,可以關注我們騰訊雲技術社區-雲加社區官方號及知乎機構號