1. 程式人生 > >初級前端小程式專案載入速度優化

初級前端小程式專案載入速度優化

這份文字是根據近期團隊做來問丁香醫生 SPA 和 丁香醫生小程式 載入速度優化的經歷整理而成。

效果

古人有一句話叫做:治感冒看療效。既然是關於速度優化的,我們就先來看一下優化的效果。

Chrome Network

選取了訪問量較大的首頁和我的頁面進行隨機取樣,通過下圖可以看到首頁的載入時間從 5.1s 下降到 1.67s,我的頁面從 2.92s 下降到 1.82s。

mta

2018.01.02 早上的頁面響應速度資料,目前國內各省份平均載入速度在 0.99~2s(雖然沒有達到 1s 內載入,但是以目前業務量級,這樣的速度是可以被接受的):

前者這是 Google 的一個評分工具,最開始做優化時用它測了一些頁面的分數。後來發現了後面這些 Chrome 外掛。讓我困惑的是同樣的頁面這幾個工具給出的結果分數都不一樣。手淘 的首屏載入速度挺快的,但是跑出來的分數也不高。最終我只是選擇性的參考一下工具給出來的建議,忽視了其給出的評分。

丁香醫生小程式

對於小程式,做了優化後得到部門同學的反饋是這樣的:

具體的資料指標如何呢?雖然目前沒有特別好用的效能檢測方式(包括官方提供的效能檢測工具在內),最終我們組的舒哲同學還是利用官方提供的工具做了一下簡單的資料對比,資料如下:

在不影響產品需求正常迭代的前提下,兩個專案的優化斷斷續續持續了兩週。整體上來說,本次優化的價效比還是較高的。

為什麼做載入速度優化?

直接原因很簡單:慢。雖然說頁面載入速度並沒有達到慢的讓人無法忍受,但至少沒辦法讓人說載入很快。

既然明知道載入速度不快,那之前在幹什麼?為什麼不早早的去做優化呢?

這是一個好問題,我曾經在深夜中問過自己多次。我給自己的答案是:首先,要承認自身技術水平和經驗的限制,如果是一個在前端戰場上身經百戰的人一直在負責專案的迭代,或許情況會比優化前好一些。 其次,之前整個產品線的專案一直處於探索和快速迭代中,前端研發資源基本上總是處在被需求排滿的狀態下,產品需求快速上線的優先順序是最高的。正是因為產品的整體節奏稍微放緩了一些,才讓研發資源有精力來做一些優化。

為什麼說是前端響應速度優化,而不是前後端?

因為我是親眼看著這兩個專案逐漸長大的,單從前端工程的角度來審視,在自己的認知範圍內,早就認為專案中有一些地方是需要優化的。堅定了先從前端動手的想法,是因為讀了《高效能網站建設指南》這本書,書中提到了一個性能黃金法則(Performance Golden Rule):只有 10% ~ 20% 的終端使用者響應時間是花在下載 HTML 文件上。話說到這個份上,還猶豫什麼呢,先從前端專案開始擼起袖子加油幹吧。

之前去 Qcon 等技術大會上,聽過幾次關於載入速度的分享。比如:使用 HTTP2,整站級別的前後端優化等。方案確實是好的方案,但具體是否要應用到自己團隊實際專案中,還得根據執行成本、團隊技術儲備等維度從長計議。

為什麼說是初級?

因為深感自己在前端效能優化這個領域還有很長的路要走。

如何做的?

前戲這麼長,終於可以開始了。

來問丁香醫生 SPA

先看圖(綠色部分為已在專案中應用的方法):

實現遊客機制

最初來問丁香醫生是基於微信服務號做的,當時的設計是使用者通過服務號選單進入應用時,會自動幫他進行跳轉登入,登入成功後服務端再重定向回到應用。登入這個環節,雖然與專案程式碼層面的載入優化關係不大,但是從使用者體驗的角度這樣的流程是不好的。因為相比於直接開啟頁面,使用者需要等更長的時間,並且會看到兩次頁面載入的進度條。從產品的角度,一些頁面是不需要使用者登入即可訪問的。綜上,將登入流程後置,讓使用者可以直接進入應用這件事情,於情於理都是必須要做的。

改造流程大致為:梳理產品現有流程 -> 使用者進入應用時取消強制登入 -> 在產品流程核心環節進行使用者登入狀態判斷並引導登入。具體實現細節不再贅述。

減小資源包體積

實現了遊客機制後,接下來就是對應用的資源包動手了。因為通過 Chrome 開發者工具的 Network 可以看出,下載 CSS、JS 資源還是佔用了不少時間的。下圖是減小資源包體積之前的情況:

優化前包體積大小-Gzipped

精簡第三方依賴

想要減少資源體積大小,首先需要知道哪些資源時應該/可以被刪除的。由於專案是基於 Webpack 構建的,因此可以使用  進行分析Webpack 生成的包體組成。然後根據實際情況進行移除就好。

精簡了第三方依賴後,啟動應用時需要下載的資源體積還是挺大的。此時就需要使用 Webpack 的程式碼分離和懶載入進行進一步的優化。

程式碼分離

程式碼分離的思想就是化整為零,將程式碼分離到不同的 bundle 中,然後可以按需載入或並行載入這些小的 bundle 檔案。

程式碼分離主要是利用 Webpack 的動態匯入

Webpack 目前有三種常用的程式碼分離方法:

  • 入口起點:使用 entry 配置手動地分離程式碼。(優勢:簡單、直觀。劣勢:配置繁瑣、同一份程式碼可能會被引入到各個 bundle 中、不靈活,並不能將核心應用程式邏輯進行動態拆分程式碼)
  • 防止重複:使用 CommonsChunkPlugin 去重和分離 chunk。
  • 動態匯入:通過模組的行內函數呼叫來分離程式碼。

經過對比之後,最終選擇了動態匯入的方式。

動態匯入(dynamic imports)

webpack 提供了兩個類似的技術:

  • import() 語法(推薦,符合 ECMAScript 提案)
  • require.ensure(webpack 欽定)

示例

// 分離 lodash
async function getComponent() {
    const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
}

懶載入

懶載入是在程式碼分離的基礎上更近了一步。

雖然我們可以將程式碼進行程式碼分離,但程式碼分離後的 bundle 只是載入的優先順序會不同,最終還是會載入,但實際情況是某些程式碼在使用者進行某項操作之前是不需要載入的。比如:個人資訊編輯頁面有一個使用者修改頭像功能,對於使用者來說,即使他進入了個人資訊編輯頁面,在他未點選上傳按鈕之前,用於上傳頭像的程式碼是沒必要載入的。

Vue-Router 結合 Vue 的非同步元件和 Webpack 的程式碼分割功能,實現了路由元件的懶載入。

在經過精簡依賴、程式碼分離和懶載入之後,專案的資源包體積大小如下圖:

Gzipped:

使用者進入首頁需要載入的 js 資源從 vendor.js 、 main.js 和 chunks 共 672.84kb 變為只需要載入一個 186kb 的 main.js 。

複用 Store 資料以減少網路請求數量

來問丁香醫生是基於Vue.js 全家桶實現的,狀態管理用的是Vuex。

之前的實現中,有些功能實現沒有很在意 Store 資料的複用。比如:從 A 頁面進入 B 頁面後再返回 A 頁面時,會再去獲取端獲取一次 A 頁面需要的資料。這種處理不僅僅是多發了不必要的請求,如果在請求過程中做了一些頁面級別載入中的處理,那麼每次切換頁面時都會讓使用者看到 loading 效果,這也會讓人覺得載入慢。既然用了狀態管理,那麼就應該把他利用好才是。

本次優化過程中的資料複用,主要是在部分請求 action 之前增加邏輯判斷,如果 Store中有當前操作需要的資料,則不再呼叫 action 。

前後端徹底分離

關於這一點會再寫一篇文章進行闡述。

丁香醫生小程式

老規矩,先看圖:

圖片資源

最開始做小程式時,是把所有圖片資源 base64 後進行使用的,這導致了所有圖片資源最終都被打包到小程式的安裝包中。所以做小程式的載入速度優化的第一步,就是把一些體積較大的圖片資源改為使用線上資源。具體做法是將素材先上傳到 cdn,然後在小程式中直接使用線上圖片地址。

登入鑑權優化

原本小程式的登入是我們自己實現的一套登入方案,核心是前後端一起維護一個類似於 SessionId 的 ID。服務端對於這個 ID 是設定了有效期的,而之前前端的實現是每次使用者啟動小程式,都直接去請求公司的 SSO 獲取一個新的 ID,沒有在意本地的 ID 是否過期。

優化的點在於在應用啟動時,增加對 ID 有效期的判斷,從而避免每次使用者啟動都需要發請求獲取新的 ID。

預渲染

之前在小程式所有需要從服務端獲取資料的頁面,都實現了一個載入中的效果,即請求未返回結果時,整個頁面使用者只會看到一個載入中的菊花。如果某頁面只有服務端提供的元資料級別介面,沒有業務介面,並且介面返回的資料是有依賴關係的,那麼使用者等待的時間會大大加長。

仔細思考會發現,其實是沒有必要等所有介面資料回來後再給使用者呈現完整頁面的。

最終的優化方案分為兩種:一種是取消載入中效果,先給使用者呈現完整的利用本地資料渲染好的頁面,等介面返回資料後在進行頁面檢視的更新;另外一種方案是取消載入中效果,但是不做本地資料渲染,而是直接給使用者看到部分靜態頁面。

分包載入

關於分包載入,就老老實實的按照官方文件做就好了。進行分包後的效果還是很不錯的。具體效果可以參考文章開頭的資料統計。

目前上述方案中,效果比較明顯的是預渲染和分包載入。一個是視覺上讓使用者覺得快了,一個是真真切切的把首次載入的資源包變小了。