本文由雲+社區發表css
做者:思衍Jax前端
天下武功,惟 (wei) 快(fu) 不(bu) 破(po)。android
隨着近幾年的前端技術的高速發展,愈來愈多的團隊使用 React、Vue 等 SPA 框架做爲其主要的技術棧。以 React 應用爲例,從性能角度,其最重要的指標可能就是首屏渲染所花費的時間了。那麼今天,咱們要給你們分享的一個把優化作到極致的故事。ios
咱們的目標是讓 H5 的頁面也可以擁有 Native 般的體驗,若是你還在尋求什麼技術可以讓老闆虎軀一震(拯救你的KPI),那麼這篇文章或許可以幫助到你。git
課程詳情頁是騰訊旗下企鵝輔導 APP 中最重要頁面之一,也是流量最大的頁面之一,因此它的打開速度也是相當重要的。github
這是一個使用 React
編寫的 H5 頁面,運行於多端,包括: 企鵝輔導APP
、手機 QQ
、手機瀏覽器
。web
咱們知道當前主流的 SPA 的應用的默認渲染方式都是這樣的:json
在這種狀況下,從加載頁面到用戶看到頁面(首屏渲染所花費的時間)就是上圖中灰色邊框區域所包括的時間。瀏覽器
這是最慢的一種方式,就算 CGI 夠快,最少要花費 1S 到 2S 左右的時間了。緩存
接着咱們簡單優化一下:
怎麼作到呢?咱們的方案是統一封裝 Request 請求工具,在用 Webpack 打包的時候,會往頁面頂部注入一段 預加載 CGI 的 JS 代碼,維護一個CGI 與 DATA 對應 MAP,後面發請求的時候,先去 MAP 裏取值,若是有值的話直接拿出來,沒有的話則發起HTTP 請求。(具體請查閱咱們團隊開源的 Preload 工具)
這種模式還有一些其餘的優化的方法:
效果以下圖所示:
在異步的模式下,除了上述優化,咱們還在端內(企鵝輔導 APP、手機 QQ)內作了離線包緩存(騰訊手Q方面獨立研發出來的針對手機端優化的方案,簡而言之就是將靜態資源緩存在手機 APP 內),通過咱們的數據測試,首屏渲染大概可以達到秒開(1s左右) 的效果。
但對有着性能極致追求的咱們來講,確定是不會滿意的。
繼續優化,最容易、最大衆的套路確定就是直出(服務端渲染)了。
如今直出的方案已經有不少不少種,這裏也很少作介紹了,若是您想了解更多關於服務端渲染的方案,請參考這篇文章。
直出針對首屏時間的優化效果是很是明顯的,通過咱們的測試,數據大概可以提高**25%**左右。
直出以後的效果以下圖:
能夠看到對於首屏來講,沒有了**【加載中...】**的等待時間,視覺體驗提高了很多。
針對上述、常見的直出應用來講,咱們可以優化的點在哪裏呢?讓咱們來詳細分析一波,這也是今天咱們要給你們分享的重點。
首先看看直出應用各個環節的耗時表 (本地環境 2018款 iMac):
過程名稱 | 過程花費 |
---|---|
Node 內 CGI 拉取 | 300 ms |
RenderToString | 20 ms |
網絡耗時 | 10 ms |
前端HTML渲染 | 30 ms |
從上面的表中咱們看出,直出渲染的耗時的大頭仍是在 CGI 接口的拉取上。
咱們如今提出兩個問題:
這個頁面的接口數據中,有一些數據,是實時變更的, 好比:當前還剩多少個名額、此時此刻課程的價格、用戶是否購買過這個課程等。
這些數據的特性決定了這個數據接口不可以被緩存。(假設將其緩存,那麼就會存在可能用戶進來看到當前還剩下10個名額,其實課程已經賣光了的狀況)
爲了這個時間耗時的大頭,咱們作了CGI接口的動靜分離。
將與用戶態、當前時間沒有關聯的數據(好比
課程標題
、課程上課的時間
、試聽模塊的地址
等)放在一個接口(靜態接口),其餘變化的數據放在另外一個接口(動態接口)。
那麼可使用靜態的接口來作服務端渲染,好處是第一比較快(少了動態的信息,並且後臺也能夠作緩存),第二 Node 直出能夠作緩存了。
這樣咱們就能夠將那部分靜態的、不會常常變更的數據用來直出 HTML,而後將這個 HTML 文件緩存到 Redis 中。
客戶端請求此網頁,Node 端接受到請求以後,先去 Redis 裏拿緩存的 HTML,若是 Redis 緩存沒有命中,則拉取靜態的 CGI 接口渲染出 HTML存入 Redis。
客戶端拿到 HTML 以後,會馬上渲染,而後再用 JS 去請求動態的數據,渲染到相應的地方。
作完以後咱們能夠看到優化效果的提高是很是很是明顯的:
直接從 262ms 提高到了 16ms !(本地環境),簡直飛通常的感受,媽媽不再用擔憂領導看耗時了。
關於什麼是 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 直出;右 離線包):
從上圖能夠看出,使用了 PWA 直出緩存以後,首屏渲染基本是毫秒開,能夠說與 Native 並肩了。
通過咱們的數據測試,使用 PWA 直出緩存,首屏渲染的時間最好能夠到400ms左右級別:
由於對接口進行了動靜分離,使用靜態接口直出頁面,而後在客戶端拉取動態數據渲染完。這就可能會致使頁面的抖動(好比詳情頁中的試聽模塊,是在客戶端渲染的)。
由於高度改變了,視覺上就會出現抖動(具體能夠參考上面章節直出時候的 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 以前。
防抖動優化效果以下 (左優化完,右未優化):
雖然咱們作了 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 技術的發展,現今大部分手機以及 PC 環境已經支持對 PWA 進行了支持。通過咱們的測試發現:安卓基本上都是支持的,IOS 須要11.3以上才支持。
Service Workers 兼容性圖
不少的經驗告訴咱們,外聯的 script 標籤要放在 body 的後面,由於它會阻塞頁面的 DOM 渲染。
通過測試發現,IOS 的 WebView
(UIWebView
)渲染機制並不會上述同樣,而是要等到後面的 JS 執行完以後才渲染頁面,若是是這樣,咱們的直出渲染優化就沒有效果了(由於 HTML 並不在最開始渲染),這裏可使用 script
標籤的 async
與 defer
屬性來達到異步渲染的做用。
升級 WkWebView 以後,狀況獲得改善,渲染正常。
此文已由做者受權騰訊雲+社區在各渠道發佈
獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號