單頁式應用性能優化-首屏數據漸進式預加載

withoutD 8年前發布 | 20K 次閱讀 性能優化 Node.js JavaScript開發

前言

針對首頁和部分頁面打開速度慢的問題,我們開始對單頁式應用性能進行優化。本文介紹其中一個方案:基于 HTTP Chunk 的首屏數據漸進式預加載方案,該方案總體減少了單頁應用1.2s的首屏呈現時間。同時對比其與同構渲染方案的異同。

背景介紹

單頁式應用是近幾年來前端技術棧發展與落地的最典型場景,Angular、Vue、React等,這些相關的技術棧目的都是從架構層面為單頁式應用提供研發解決方案,著重解決單頁式應用的研發效率。基礎框架的進化也催生著關聯工具鏈路的發展,如 Yeoman,Grunt -> Gulp -> 各種 cli,Webpack1/2, Babel 等。

隨著研發鏈路體系的穩定成熟,在功能上能夠及時滿足用戶后,我們開始將精力集中關注產品的可用性層面。經過和產品,運營,用研等多個團隊配合,我們走訪了多位使用我們產品的用戶,產出了一份流量端產品可用性得報告。除了部分交互和產品流程設計上的問題,另一個主要問題就是用戶反饋整體的系統流暢性不錯,但首頁和部分頁面打開極其慢,針對這塊問題,我們開始了對單頁式應用性能優化的探索和實踐。

本文接下來將一步一步闡述對應用首屏呈現中各個節點的拆解,并根據拆解的節點推導出我的優化思路,最終為大家介紹我提出并嘗試的第一個性能優化方案: 首屏數據漸進式預加載

首屏呈現節點分析

在進行任何的性能優化之前,我們都應該先找出系統的性能瓶頸點,從而找出最有價值的優化方向。

絕大多數的單頁式應用都符合 Application Shell 架構,根據這個架構我們可以看出一個應用首屏呈現節點可以分解為:請求入口頁 -> 渲染應用外殼 -> 渲染首屏片段。我在此基礎上進一步將三個節點細分如下:

即對渲染應用外殼和渲染片段這塊細分為: 應用資源加載,應用初始化,片段資源加載,片段初始化,片段數據加載,片段渲染 這些節點。

有了這些細分節點,再將埋點記錄的真實用戶數據代入:

得出我們的首屏時間為:

T(s) = T1 + ... + T7 = 2800ms

注:我們一般都將首屏資源一起與應用資源打包在一起,因此這里耗時認為是0。

整個 timeline 如下:

首屏數據漸進式預加載方案

根據上面的節點數據,首屏數據漸進式預加載的優化思路也得到了體現:

  • 優化首屏數據加載節點的速度。
  • 預先加載首屏數據,使得多個串行節點并行化。

接下來詳細介紹我們的優化步驟。第1點會在第一步優化中體現,但核心思路和主要優化收益更多體現在第2點: 多個串行節點并行化

Step1:資源文件下載與首屏數據請求節點并行

為了達到資源下載與數據請求并行的效果,我們充分利用了 HTTP Chunk 傳輸與瀏覽器的漸進式渲染特性

1、將入口頁分為靜態片段和數據片段:靜態片段包含了各個資源標簽(script,link),靜態的導航欄,加載指示器等;數據片段則是包含首屏數據的內聯腳本,大至如下:

 

<script>window.__APP_DATA__ = { /* 相關的首屏數據 */ };</script>

2、瀏覽器請求入口頁時,入口頁服務器(這里我們用了 NodeJS ) 并行 做以下操作:

  • HTTP Chunk 方式輸出靜態片段
  • 請求首屏數據并在所有數據請求完成后將數據片段和應用初始化代碼返回給瀏覽器。

注:http chunk 方式輸出在 NodeJS 中及其容易滿足,簡單的 res.write(chunk) 即可。

整體架構如下:

瀏覽器的漸進式渲染特性在收到靜態片段并解析后立刻去下載資源,由此巧妙的將應用資源加載節點和首屏數據請求節點并行化;當應用初始化完畢后,首屏組件直接讀取window.__APP_DATA__拿到數據渲染即可。

整個首屏呈現 timeline 變化如下:

最終并行化這塊耗時為:Max(下載資源文件,請求首屏數據輸出片段) = 1000ms。

根據變化后的節點我們算出首屏呈現時間為: 2350ms

首屏呈現耗時的通用計算公式變為:

下載靜態片段 + Max(下載資源文件,請求首屏數據) + 應用初始化 + 首屏初始化 + 首屏渲染 Step2:應用初始化,資源文件下載,首屏數據請求節點并行

在 Step1 的基礎上繼續分析,應用初始化節點耗時也很明顯,同時該節點要進行必須等待資源文件下載完畢,但理論上可以不依賴我們的首屏數據,還是可以讓其和首屏數據請求并行。

這里我們無法在 Step1 方案上直接將應用初始化和數據請求并行化,主要原因在于當首屏數據請求時間大于資源加載+應用初始化完成時間時,應用會在沒有數據的情況下進入收入首屏渲染節點,從而導致異常。

解決方案是將數據片段的輸出變成 promise 片段:

1、pending promise 片段,與靜態片段一起輸出,大概如下:

<script>
    window.__APP_DATA__ = {
     RESOLVERS: {}
     userInfo: new Promise((resolve, reject) => {
       // 超時認為失敗
       let timer = setTimeout(reject.bind(null, {message: 'timeout'}), 12000);
       window.__APP_DATA__.userInfo = (err, data) => {
         clearTimeout(timer);
         err ? reject(err) : resolve(data)
       }
     })
    };
    </script>

2、resolve promise 片段,該片段在數據請求成功返回后輸出,大概如下:

 

<script>window.__APP_DATA__.RESOLVERS.userInfo(null, data); </script>

3、reject promise 片段,該片段在數據請求失敗后輸出,大概如下:

 

<script>window.__APP_DATA__.RESOLVERS.userInfo(error); </script>

即此時應用初始化完畢后可以無視首屏數據的完成度,直接進入首屏渲染節點,組件在數據 promise 被 resolve 后渲染即可:window.__APP_DATA__.userInfo.then(data => component.render());

通過對數據片段的promise化改造,使得應用初始化節點也加入了并行隊列。

整個首屏呈現 timeline 變化如下:

根據變化后的節點我們得到首屏呈現時間為: 1800ms

首屏呈現耗時的通用計算公式變為:

下載靜態片段 + Max(下載資源文件 + 應用初始化,請求首屏數據) + 首屏初始化 + 首屏渲染 優化小結

經過上述2個步驟改進,我們應用首屏呈現時間從 2800ms -> 2350ms -> 1800ms, 總體效果約為36% ,可以看到是收益還是很可觀的。

在實際項目中耗時是在 1600ms 左右,比1800ms還要小,主要原因如下:

  • 用戶在請求入口頁中半個RTT時間,服務器就開始了數據請求。
  • 數據請求在服務端進行減少了瀏覽器與服務端的請求創建開銷,同時數據請求在內網進行,總體調用速度也會加快。

當首屏數據請求數超過瀏覽器并發請求數時,該方案收益會更明顯,因為 NodeJS 端沒有并發限制,甚至在NodeJS端與后端服務的交互中可以采用更高效的協議如HTTP2來提高調用速度。

與服務端同構渲染對比

看到這里,相信很多人會問,為啥不用服務端渲染直出HTML呢,或者和服務端渲染方案相比有何優勢?

事實上,一開始我和大多數人想到的優化方案就是服務端渲染,但真正的障礙在于服務端渲染依賴視圖層框架的支持,而我們的項目歷史悠久,視圖層框架并不支持這一點,為了優化而喪失產品的穩定性得不償失。

當然,在另辟蹊徑使用了數據漸進式預加載方案后,我總結該方案與與服務端同構渲染對比如下。

優勢

  • 對客戶端代碼來說數據漸進式預加載方案實現成本非常簡單,基本可以做到透明化,我們在實際的開發過程中采用基于 uIoC(ecomfe/uioc ) 提供的AOP攔截方案,通過配置化的方式讓客戶端的代碼改造僅局限在配置文件,應用代碼基本未改動。
  • 對NodeJS端來說,分層合理的應用只需要將數據層簡單適配下 NodeJS 端即可完成數據漸進式預加載,這對底層基礎框架在視圖層沒有支持同構的應用來說,整個改造成本可以說大大減小,且收益明顯。我們目前的應用基于自有的一套MVC框架,僅僅是將 Model 層簡單適配 NodeJS 端執行輸出數據。
  • 服務端渲染方案如果未能提供較基于 BigPipe 的渲染,總體的頁面呈現速度還是不如數據漸進式預加載的,且目前我也暫時還沒有在三大框架中發現有一套基于BigPipe的服務端渲染方案。

不足

整體呈現速度可能不如結合了BigPipe的服務端渲染方案,但這點沒有經過論證,畢竟數據漸進式預加載與服務端同構渲染的區別僅僅在于渲染環節放在客戶端還是服務端:渲染看的是CPU,服務端的CPU資源是有限的,要服務諸多請求,而客戶端渲染則基本無此壓力,渲染能力未必弱于服務端。

總結

我們在單頁應用的性能優化上基于很樸素的并行化理念實施了首屏數據漸進式預加載方案,在實際項目中也得到了較為明顯的效果,減少了1.2s的加載時間,整體的節點變化如下:

優化前:

優化后:

最終數據漸進式預加載方案的首屏呈現時間計算公式為:

下載靜態片段 + Max(應用資源加載 + 應用初始化,請求首屏數據) + 首屏初始化 + 首屏渲染

這里忽略了影響很小的片段傳輸時間,有打算嘗試的朋友可以將自己應用的相關節點數據代入計算即可。

數據漸進式預加載,服務端同構渲染,客戶端渲染三種方案各有優缺和場景,個人未來計劃是將三種方案結合實時流量數據動態切換:在服務器壓力不大時用同構渲染;服務器壓力較大時用數據預加載;服務器壓力很大時用客戶端渲染。

 

來自:http://www.iteye.com/news/32370

 

 本文由用戶 withoutD 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!