速度有多重要
在周星馳電影裡有一句話說道:「天下武功,無堅不破,唯快不破」,現在的 Web 應用比以往更具有互動性,搞定性能可以幫助你極大改善使用者的體驗。
而根據 CDN 廠商 Akamai
所發表的報告更指出,當你的網站延遲超過 3 秒,則 53% 的使用者會跳出網站。
檢測指標
在進行優化前,要先知道問題才能對症下藥,Google 有提供兩個常用的檢測工具,一般網頁可以用 PageSpeed Insights
檢測網頁與行動版本的使用者體驗,另外則是利用 Lighthouse
來做 PWA
(Progressive Web Apps, 漸進式網頁應用程式) 的相關項目檢測與修改建議。
先檢測看看目前正在運作的網站
使用 PageSpeed Insights
使用 Lighthouse
PWA 只有 45 分,不具有漸進式應用程式的特性
從檢測報告可看出造成效能低落的兩個主要原因:
- JS 文件過大,首次載入會下載超過 1MB 左右的 JavaScript
- 有些圖片檔案太大
專案架構
針對第一點 JS 檔案過大,先來看看我們的架構和遇到了什麼樣的問題
專案整體採用了以下技術構建
- SPA (Single Page Application)
- React SSR (Server-side Rendering)
- Webpack
我們使用 React 開發 SPA 和 SSR 實現 Universal 應用,然後在以 React 生態系的相關套件完成各種功能,打包工具則是採用 Webpack 進行模組打包。
會有什麼樣的問題
- 網站所用的 JavaScript、Image、CSS 等靜態資源全部打包在一起
- 功能越來越多會導致資源文件越來越大
- 首次讀取網站時加載全部內容而導致載入時間過慢
針對程式方面的優化策略
接著就來分析看看有哪些方案可以實作來改善效能
Code Splitting
對於大型 Web 應用而言,把所有的程式碼放到一個文件的做法效率很差,有些功能一般使用者是不會用到的,可以等跳轉到該頁面時在載入即可。
Webpack 有個功能可以把你的程式碼分離成獨立的 Chunk,而此 Chunk 可以依照需求動態載入。這個功能就是 Code Splitting,實作方式依照 Webpack 版本也不同,1 版本是用 require.ensure
;2 版本也可以用 System.import
來實作。
Route-Level Code Splitting
可以搭配 react-router
的 getComponent,以路由作為分離點產生 chunks 檔案,require 是指向 routing component (container) 的路徑。
routes.js
部分程式碼如下
|
|
編譯結果
Component-Level Code Splitting
對於一些元件或模組,不要一開始就載入它們,例如 aws-sdk
、video.js
等比較大包但又不需要馬上用到的模組,Webpack 也可以實現元件層級的分離,require.ensure 也可以應用在 Module。
以使用 aws-sdk
的 S3 為例子
|
|
再來是 React 的 Component 也可以改寫成 Async Cmponent,不過因為寫 require.ensure 程式碼不好閱讀,且 React 元件在 SSR 也有些雷要處理,因此直接下載 react-async-component
使用會比較無痛。
建立一個 Async Component
|
|
引用 Async Component
|
|
現在再回頭來看看 require.ensure 這個函式
|
|
第一個參數表示相依模組,通過這個參數,在回調函數被執行前,我們可以將所有需要用到的模組進行宣告。
所以,如果有個模組會依賴其他模組,你可以這樣子寫,這樣所有相依模組都會被打包在一起
|
|
Split single bundle to multiple
雖然做了 Code Splitting 將其他程式碼拆出去了,但如果網站功能太多,剩下的那包 JS 還是會很大,這時可以用 Webpack 的 CommonChunksPlugin 拆解 main bundle
webpack 設定如下
|
|
參數說明:
name
: 要把共用代碼提取到在哪個 chunkchunks
: 從哪些 chunk 提取共用代碼minChunks
: 總共有幾個 chunk
在 entry 設定要打包的 chunk name 與要打包的模組,設定好後還要在 plugins 裡面用 CommonChunkPlugin 將重複的模組提取到指定的 chunk,否則打包完你會發現 vendor 和 redux 的 chunk 裡都有 react。
Async Load JavaScript
要怎麼載入那些打包檔案呢? HTML5 的 script tag 多了 async
和 defer
兩個屬性,可讓 script 以非同步的方式下載而不會停止網頁的繪製。
以下說明差異處
- 綠色代表 HTML 繪製
- 灰色代表暫停 HTML 繪製
- 藍色代表下載 JavaScript
- 紅色代表執行 JavaScript
script with no attribute
下載 JavaScript 時會中斷網頁的繪製,下載完成後會馬上 Script,等執行完後才會繼續繪製網頁
script with async attribute
下載 JavaScript 不會中斷網頁的繪製,下載完成後會先中斷網頁繪製並執行 Script,執行完成後再繼續繪製網頁
script with defer attribute
下載 JavaScript 不會中斷網頁的繪製,等網頁繪製完成後才會執行 Script
該用哪個? 一般情況能用 async 就用 async,再來才是 defer,使用的時機點如下
- 如果 script 沒有相依其他的 scripts 時用 async
- 如果 script 有相依其他的 script 時用 defer
Optimizing React && Redux
這邊說明如何對你的 React 和 Redux 來進行優化
Enable production build
記得要在 webpack 的 DefinePlugin
中定義 NODE_ENV
參數為 production
,可以大幅減少打包檔案大小和提升執行效能
|
|
Make use of the shouldComponentUpdate method
針對效能較差的頁面,可以使用 React 推出的 Performance Tools 檢測,再針對效能較差的元件寫 shouldComponentUpdate
函式減少不必要的更新。也有 Chrome Extension ,把 Perf 指到 window 全域變數就可以使用
|
|
另外在 React 15.4 中引入了新的效能檢測工具,可以方便與 Chrome DevTools 整合使用,簡化我們找尋效能問題的困難,使用操作可以參考 React 官方的介紹
Batch Actions into a Single Dispatch
你應該把所有 dispatch action
寫在一起,並使用 redux-saga
的 all effect
來處理多個 API
的呼叫和 Action
的調用。
不要這樣子寫
|
|
這樣寫會比較好
|
|
|
|
Service-Worker
這邊就不說明 PWA 是什麼和怎麼建立 service-worker 了,有興趣可參考其他文章
- 透過各種技術及設計的優化來達到應用程式的體驗,並保留網頁的優勢,藉此做到最好的使用者體驗 (User Experience)
- PWA 介紹
可以通過在 webpack 中使用 sw-precache-webpack-plugin
和 html-webpack-plugin
這兩個插件引入 service-worker 實現 PWA
CDN && Image Compression
- 一定要壓縮圖片,如果有 CDN 就傳上去提升存取速度
- 小於 10kb 的圖檔可轉成 base64 的編碼以減少 HTTP Request 數量
優化成果
對打包檔案進行優化後,初次載入的 JS 大小縮減了一半降為 500KB 左右,且分割出多個 chunk 並以非同步的方式載入網頁,加速載入速度。
再重新對優化後的網站進行檢測
PageSpeed Insights
Lighthouse
可以看到分數獲得了顯著的提升,而因為還有許多以前上傳在 AWS S3 的圖片尚未進行壓縮,因此 Insights 行動版和 Lighthouse Performance 分數會較低,目前主要的效能瓶頸卡在圖片,預計之後會再進行改善。