讓老闆虎軀一震的前端技術,KPI殺手

本文由雲+社區發表css

做者:思衍Jax前端

img

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

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

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

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

img
企鵝輔導詳情頁

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

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

架構演變

純異步渲染

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

img

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

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

接着咱們簡單優化一下:

  • 把靜態資源緩存起來,這樣下次用戶打開的時候就不用從網絡請求了。
  • 步拉取 CGI 這個動做是否能夠提早呢?咱們能夠在請求 HTML 以後,先經過一段 JS 腳本去請求 CGI 數據,後面第 步的時候,就能夠直接拿到數據了,這就是 CGI 預加載

怎麼作到呢?咱們的方案是統一封裝 Request 請求工具,在用 Webpack 打包的時候,會往頁面頂部注入一段 預加載 CGI 的 JS 代碼,維護一個CGI 與 DATA 對應 MAP,後面發請求的時候,先去 MAP 裏取值,若是有值的話直接拿出來,沒有的話則發起HTTP 請求。(具體請查閱咱們團隊開源的 Preload 工具)

img

這種模式還有一些其餘的優化的方法:

  • 在 HTML 內實現 Loading 態或者骨架屏;
  • 去掉外聯 css;
  • 使用動態 polyfill;
  • 使用 SplitChunksPlugin 拆分公共代碼;
  • 正確地使用 Webpack 4.0 的 Tree Shaking;
  • 使用動態 import,切分頁面代碼,減少首屏 JS 體積;
  • 編譯到 ES2015+,提升代碼運行效率,減少體積;
  • 使用 lazyload 和 placeholder 提高加載體驗。

效果以下圖所示:

img
異步渲染

直出同構

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

img
-w300

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

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

img

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

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

直出以後的效果以下圖:

img
直出同構

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

PWA 直出

img
PWA

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

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

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

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

咱們如今提出兩個問題

  • CGI 接口的數據是否能夠緩存 ?
  • HTML 又是否能夠緩存 ?
1、接口的動靜分離

img
動態信息

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

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

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

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

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

2、直出 Redis 緩存

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

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

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

img

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

img

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

3、PWA 直出緩存

關於什麼是 PWA ,以及如何使用,請移步這篇文章。

作了 Node 端直出的 HTML 緩存以後,咱們接着優化,接着思考,是否能夠在客戶端也緩存 HTML,這樣連網絡延時這部分消耗也省掉呢。

答案就是使用 PWA 在客戶端作離線緩存,將咱們直出的 HTML 緩存在客戶端,每次用戶請求的時候,直接從 PWA 離線緩存裏取出對應的直出頁面(HTML)響應給用戶,響應以後緊接着請求 Node 服務更新本地的 PWA 緩存。(以下圖所示)

img

核心代碼:

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 直出;右 離線包):

img
duibi

從上圖能夠看出,使用了 PWA 直出緩存以後,首屏渲染基本是毫秒開,能夠說與 Native 並肩了。

通過咱們的數據測試,使用 PWA 直出緩存,首屏渲染的時間最好能夠到400ms左右級別:

img

PWA 直出細節優化

1、防頁面跳動

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

img

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

img
-w500

防抖動優化效果以下 (左優化完,右未優化):

img
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 兼容性圖

img

2、IOS 渲染問題

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

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

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

附錄

參考資料

此文已由做者受權騰訊雲+社區在各渠道發佈

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

獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號

相關文章
相關標籤/搜索