前端高級進階:如何更好地優化打包資源

這是山月關於高級前端進階暨前端工程系列文章的第 M 篇文章 (M 隨便打的,畢竟也不知道能寫多少篇),關於前 M-1 篇文章,可以從我的 github repo shfshanyue/blog[1] 中找到,如果點進去的話可以捎帶~點個贊~,如果沒有點進去的話,那就給這篇文章點個贊。。今天的文章開始了

本篇文章地址在 前端工程化系列[2],歡迎訂閱。


在前端中但凡談到打包,肯定要提及到 webpack,畢竟它現在已經是最為流行的打包工具。但 webpack 更多地是表現在 上,於是我決定寫這篇文章,更多地講解一些關於 的。

對於一個前端而言,生產環境的靜態資源優化,它既是面試中的高頻問題,同時也最容易成為平時工作中的 OKR/KPI。如果你經常致力於優化前端打包提及,必然會對一些數字極為敏感,比如:

  1. lodashreact gzip 後的體積是多少 (定性,可以給出範圍)
  2. 打包 moment 時會有什麼問題
  3. 你們線上前端項目首屏靜態資源 gzip 後的體積是多少

如果你負責了你們前端項目的打包優化,如果以上問題連一個都不了解那麼是說不通的。以我作為面試官的兩年經驗中,如果候選人對這些問題有所了解的話,往往對打包以及webpack的了解就會相對深入一些

原則

一般談到打包會有兩方面的意思,第一在於提高打包的速度,第二在於對打包後的靜態資源的優化。而對於靜態資源的優化又不僅僅是打包提及的縮減。

對於打包資源優化的總體原則,在於儘可能的減少或者延遲模組的引用。主要遵循以下三點

  1. 減小打包的整體體積
  2. Code Splitting: 按需載入,優化頁面首次載入體積。如根據路由按需載入,根據是否可見按需載入
  3. Bundle Splitting:分包,根據模組更改頻率分層次打包,充分利用快取

接下來本篇文章將會結合實例分別闡述這三點

01 減小打包的整體體積

第一種方法是減小打包的整體體積。減小打包的總體積有多種方式,這往往也是打包資源優化的著力點,一方面操作性高易於實踐,~另一方面有具體數據支撐易於寫PPT來晉陞~。我從網站性能優化的實踐角度,來分為以下幾個方面

程式碼壓縮

程式碼壓縮可以非常可觀地減小資源打包體積,但是它的可操作性空間過小。可操作性低的意思是這一項不太容易出現在晉級評審的PPT上,如同 CDN 在網站性能優化的重要程度一樣,重要但不歸你做(或者傻瓜式配置)。

它良好的模組化,以致於 webpack 就自作主張在生產環境中默認把這件事給做了。

那它是如何壓縮程式碼的?最典型的兩種方法就是空白符替換以及縮短變數名,如程式碼所示,僅僅通過這兩種方式就大大壓縮了 javascript 資源:

// 壓縮前  function sum (first, second) {    return first + second;  }    // 壓縮後  function s(x,y){return a+b}  

關於程式碼壓縮,可以參考山月的前端高級進階系列[3]javascript 程式碼的體積是如何被壓縮的[4]

移除不必要的模組

這句話好像是廢話,但它卻是真正有用並且極為容易實現的一點。

在以下程式碼中,對 lodash 這個模組進行了引入,但在之後的程式碼中並無使用 lodash,那在 webpack 中這個模組還會繼續打包嗎?

很遺憾,仍會對它進行打包。但好消息是這一點優化起來相當簡單。

// 僅僅引入而未在程式碼中使用,該模組仍然會被打包  import _ from 'lodash'  

對於這類問題總應該防患於未然,扼殺於搖籃中。eslint 的用武之地來了,它除了統一團隊的程式碼風格以外,也用來提高團隊的程式碼品質以及性能。

選擇可替代的體積較小的模組

針對這一條,有一個典型的例子是以體積過大而臭名昭著的 moment.js 模組,它僅僅用於 DateTime 的格式化及各種計算。但你 import 之後它的體積竟然達到了 200kb+,gzip 後仍然有 69kb。以至於在 github 上有一個倉庫專門用來介紹如何優化它,

  • How to optimize moment.js with webpack[5]

再來一張圖感受一下它巨大的體積吧:

此時可以選擇一個可替代它功能,但體積更小的模組。與 moment.js API 兼容的 day.js,它 gzip 後體積僅僅只有 2kb。

按需引入模組

當你面對一個巨無霸的,捆綁式的大型模組時,可能你並不會使用到它的所有的功能,你只需要按照你的需求引入模組就可以了。那經常會有哪些巨無霸模組呢?

lodash (勉強算),antdecharts,我相信這三個模組對於以 React 為主的前端工程師都或多或少使用過。對你所需要使用的模組單獨引入:

import DatePicker from 'antd/es/date-picker'; // for js  import 'antd/es/date-picker/style/css'; // for css    import get from 'lodash.get'  

02 Code Splitting: 按需載入,優化頁面首次載入體積

懶載入,如果面試中提到懶載入的話,大概率面試官此時是想問你關於圖片懶載入的問題。

前端開發中的圖片懶載入如何實現[6]

通過 Code Splitting 可以只載入當前所需要的核心資源:

  1. 如果你處在首頁,並且首頁中有佔用資源過重的圖表,需要對圖表懶載入,否則它會大幅拖垮應用的首次渲染,加大白屏時間
  2. 如果你處在首頁,你無需載入當前不可見螢幕下方的複雜組件
  3. 如果你處在頁面 A,你沒有必要載入頁面 B 的資源

他們實現起來均需要額外編寫程式碼,所以可操作性中等,但是好在它能夠帶來極大的益處,投資回報率較高,操作起來也極為簡單,接下來就屬於體力活了:

  • 使用 import() 動態載入模組
  • 使用 React.lazy() 動態載入組件
  • 使用 lodable-component 動態載入路由,組件或者模組

大部分情況下,你只要做一個莫得感情的 API 工程師調用以上三個 API 就可以解決問題,大幅度降低頁面的首次載入體積。但是在前往高級前端工程師的路上,你有可能需要了解其中的原理,(有可能並不需要,數據比原理重要) 來做更加精細化的控制,比如針對快取。

Code Splitting 的原理是什麼?[7]

03 Bundle Splitting

除了資源體積上的優化,另一個大的優化就是快取。單頁應用有一個最好的方面,就是所有資源都是帶有指紋資訊的,這意味著所有的資源都是能夠設置永久快取的。

但僅僅如此了嗎?

如果你所有的 js 資源都打包成一個文件,它確實有永久快取的優勢。但是當有一行文件進行修改時,這一個大包的指紋資訊發生改變,永久快取失效。

所以我們現在需要做到的是:當修改文件後,造成最小範圍的快取失效,這樣便能夠更充分的利用快取,減小寬頻,減小伺服器費用。一個好消息是 webpack 等打包工具雖然在 optimization 上內置了很多性能優化,但它不會幫你做這件事,它並不知道你有哪些模組,以及這些模組的重要緊急程度,你終於可以大展拳腳了。

此時我們可以對資源進行分層次快取的打包方案,這是一個建議方案

  1. webpack-runtime: 應用中的 webpack 的版本比較穩定,分離出來,保證長久的永久快取
  2. react-runtime: react 的版本更新頻次也較低
  3. vundor: 常用的第三方模組打包在一起,如 lodashclassnames 基本上每個頁面都會引用到,但是它們的更新頻率會更高一些

隨著 http2 的發展,特別是多路復用,初始頁面的靜態資源不受資源數量的影響。因此為了更好的快取效果以及按需載入,也有很多方案建議把所有的第三方模組進行單模組打包。

在 webpack 中,使用 splitChunks.cacheGroups

{    splitChunks: {      cacheGroups: {        react: {          test: /[\/]node_modules[\/](react|react-dom "\/]node_modules[\/")[\/]/,          name: 'react',          chunks: 'all'        },        vendor: {          }      }    },    runtimeChunk: {      name: entrypoint => `runtime-${entrypoint.name}`,    },  }  

小結

毫無疑問在前端中更好地優化打包資源屬於網站性能優化強操作性部分的重中之重,整理下本篇文章關於資源優化的所有內

  1. 減小打包的整體體積
    • 程式碼壓縮
    • 移除不必要的模組
    • 按需引入模組
    • 選擇可以替代的體積較小的模組
  2. Code Splitting: 按需載入,優化頁面首次載入體積。如根據路由按需載入,根據是否可見按需載入
    1. 使用 import() 動態載入模組
    2. 使用 React.lazy() 動態載入組件
    3. 使用 lodable-component 動態載入路由,組件或者模組
  3. Bundle Splitting:分包,根據模組更改頻率分層次打包,充分利用快取

參考資料

[1]

shfshanyue/blog: https://github.com/shfshanyue/blog

[2]

前端工程化系列: https://github.com/shfshanyue/blog/tree/master/frontend-engineering

[3]

前端高級進階系列: https://github.com/shfshanyue/blog/tree/master/frontend-engineering

[4]

javascript 程式碼的體積是如何被壓縮的: https://github.com/shfshanyue/blog/blob/master/frontend-engineering/uglify.md

[5]

How to optimize moment.js with webpack: https://github.com/jmblog/how-to-optimize-momentjs-with-webpack

[6]

前端開發中的圖片懶載入如何實現: https://q.shanyue.tech/fe/html/1.html

[7]

Code Splitting 的原理是什麼?: https://q.shanyue.tech/fe/webpack/206.html