企鵝輔導課程詳情頁毫秒開的祕密 - PWA 直出

天下武功,惟 (wei) 快(fu) 不(bu) 破(po)。css

-w300

隨着近幾年的前端技術的高速發展,愈來愈多的團隊使用 React、Vue 等 MVVM 框架做爲其主要的技術棧。以 React 應用爲例,從性能角度,其最重要的指標可能就是首屏渲染所花費的時間了。那麼今天,咱們要給你們分享的一個把優化作到極致的故事。前端

咱們的目標是讓 H5 的頁面也可以擁有 Native 般的體驗,若是你還在尋求什麼技術可以讓老闆虎軀一震(拯救你的KPI),那麼這篇文章或許可以幫助到你。android

企鵝輔導課程詳情頁是什麼

課程詳情頁是騰訊旗下 企鵝輔導 APP 中最重要頁面之一,也是流量最大的頁面之一,因此它的打開速度也是相當重要的。ios

這是一個使用 React 編寫的 H5 頁面,運行於多端,包括: 企鵝輔導APP手機 QQ手機瀏覽器git

架構演變

純異步渲染

咱們知道當前主流的 SPA 的應用的默認渲染方式都是這樣的:github

在這種狀況下,從加載頁面到用戶看到頁面(首屏渲染所花費的時間)就是上圖中灰色邊框區域所包括的時間。web

這是最慢的一種方式,就算 CGI 夠快,最少要花費 1S2S 左右的時間了。json

接着咱們簡單優化一下:瀏覽器

  • 把靜態資源緩存起來,這樣下次用戶打開的時候就不用從網絡請求了。
  • 步拉取 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 提高加載體驗。

這種模式的優化不是咱們此次講述的重點,想了解的童鞋能夠查看這篇文章

效果以下圖所示:

-w300

直出同構

在異步的模式下,除了上述優化,咱們還在端內(企鵝輔導 APP、手機 QQ)內作了離線包緩存(騰訊手Q方面獨立研發出來的針對手機端優化的方案,簡而言之就是將靜態資源緩存在手機 APP 內),通過咱們的數據測試,首屏渲染大概可以達到秒開(1s左右) 的效果。

-w300

但對有着性能極致追求的咱們來講,確定是不會滿意的。

繼續優化,最容易、最大衆的套路確定就是直出(服務端渲染)了。

如今直出的方案已經有不少不少種,這裏也很少作介紹了,若是您想了解更多關於服務端渲染的方案,請參考這篇文章。

直出針對首屏時間的優化效果是很是明顯的,通過咱們的測試,數據大概可以提高**25%**左右。

直出以後的效果以下圖:

-w300

能夠看到對於首屏來講,沒有了**【加載中...】**的等待時間,視覺體驗提高了很多。

PWA 直出

-w500

針對上述、常見的直出應用來講,咱們可以優化的點在哪裏呢?讓咱們來詳細分析一波,這也是今天咱們要給你們分享的重點。

首先看看直出應用各個環節的耗時表 (本地環境 2018款 iMac):

過程名稱 過程花費
Node 內 CGI 拉取 300 ms
RenderToString 20 ms
網絡耗時 10 ms
前端HTML渲染 30 ms

從上面的表中咱們看出,直出渲染的耗時的大頭仍是在 CGI 接口的拉取上。

咱們如今提出兩個問題

  • CGI 接口的數據是否能夠緩存 ?
  • HTML 又是否能夠緩存 ?

1、接口的動靜分離

-w300

這個頁面的接口數據中,有一些數據,是實時變更的, 好比:當前還剩多少個名額、此時此刻課程的價格、用戶是否購買過這個課程等。

這些數據的特性決定了這個數據接口不可以被緩存。(假設將其緩存,那麼就會存在可能用戶進來看到當前還剩下10個名額,其實課程已經賣光了的狀況)

爲了這個時間耗時的大頭,咱們作了CGI接口的動靜分離

將與用戶態、當前時間沒有關聯的數據(好比課程標題課程上課的時間試聽模塊的地址等)放在一個接口(靜態接口),其餘變化的數據放在另外一個接口(動態接口)。

那麼可使用靜態的接口來作服務端渲染,好處是第一比較快(少了動態的信息,並且後臺也能夠作緩存),第二 Node 直出能夠作緩存了。

2、直出 Redis 緩存

這樣咱們就能夠將那部分靜態的、不會常常變更的數據用來直出 HTML,而後將這個 HTML 文件緩存到 Redis 中

客戶端請求此網頁,Node 端接受到請求以後,先去 Redis 裏拿緩存的 HTML,若是 Redis 緩存沒有命中,則拉取靜態的 CGI 接口渲染出 HTML存入 Redis。

客戶端拿到 HTML 以後,會馬上渲染,而後再用 JS 去請求動態的數據,渲染到相應的地方。

作完以後咱們能夠看到優化效果的提高是很是很是明顯的:

直接從 262ms 提高到了 16ms !(本地環境),簡直飛通常的感受,媽媽不再用擔憂領導看耗時了。

3、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 直出細節優化

1、防頁面跳動

由於對接口進行了動靜分離,使用靜態接口直出頁面,而後在客戶端拉取動態數據渲染完。這就可能會致使頁面的抖動(好比詳情頁中的試聽模塊,是在客戶端渲染的)。

由於高度改變了,視覺上就會出現抖動(具體能夠參考上面章節直出時候的 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

2、冷啓動預加載

雖然咱們作了 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 直出遺留問題

1、兼容性問題

隨着 PWA 技術的發展,現今大部分手機以及 PC 環境已經支持對 PWA 進行了支持。通過咱們的測試發現:安卓基本上都是支持的,IOS 須要11.3以上才支持。

Service Workers 兼容性圖

具體的兼容性支持點我查看

2、IOS 渲染問題

不少的經驗告訴咱們,外聯的 script 標籤要放在 body 的後面,由於它會阻塞頁面的 DOM 渲染。

通過測試發現,IOS 的 WebView (UIWebView)渲染機制並不會上述同樣,而是要等到後面的 JS 執行完以後才渲染頁面,若是是這樣,咱們的直出渲染優化就沒有效果了(由於 HTML 並不在最開始渲染),這裏可使用 script 標籤的 asyncdefer 屬性來達到異步渲染的做用。

升級 WkWebView 以後,狀況獲得改善,渲染正常。

附錄

參考資料

更多基於 PWA 的性能優化實踐,請查看 IMWeb 團隊劉華的分享

關於咱們

IMWeb 團隊隸屬騰訊公司,是國內最專業的前端團隊之一。

咱們專一前端領域多年,負責過 QQ 資料、QQ 註冊、QQ 羣等億級業務。目前聚焦於在線教育領域,精心打磨 騰訊課堂企鵝輔導 兩大產品。

現團隊有大量 HC,歡迎對技術有着強烈興趣的你來加入咱們,和咱們一塊兒在前端的世界裏愉快地玩耍, Work hard,Play hard。

簡歷投遞: jaxjiang@tencent.com

相關文章
相關標籤/搜索