Webpack 是一款強大的前端構建工具, 社群對其介紹的相關文章已經很多了, 本文不再贅述. 基於 Webpack 2, 本文是對我在搭建團隊前端腳手架的過程中, 蒐羅的 Webpack 2 常見的優化措施的一個總結.
如果你還不瞭解 Webpack 2, 可以先看下 Webpack 2 快速入門
1. 分離第三方依賴
在開發環境下, 通常會採取 HMR 模式來提高開發效率. 但一般情況下, 我們只會更改自身的業務檔案, 不會去更改第三方的依賴, 但 webpack 在 rebuild 的時候, 依舊會 build 所有的依賴. 因而, 為減少 rebuild 的時間, 我們可以分離第三方依賴, 在專案啟動之前, 將其單獨打包和引入.
這要藉助 DllPlugin 外掛.
我們定義一份生成 dll 的配置檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
## webpacl.dll.config.js module.exports = { entry: { vendor: [ 'vue', 'vuex', 'vue-router', 'vuex-router-sync', 'babel-polyfill', '...' ] }, output: { path: path.join(__dirname, './public/', 'dist'), filename: '[name].dll.js', library: '[name]_library' }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, './public/', 'dist', '[name]-manifest.json'), name: '[name]_library' }) ] } |
生成 dll 檔案之後, 可以根據環境變數在頁面的靜態檔案中引入:
這樣, 在每次 rebuild 的時候, webpack 都不會去重新 build vendor, 能極大減少 rebuild 的時間, 提升開發效率.
僅在開發環境下使用
2. 多程式構建
Webpack的構建過程是單程式的, 利用 HappyPack 可讓 loader 對檔案進行多程式處理, 其原理圖如下:
在業務檔案依賴越多和複雜的情況下, HappyPack 對 Webpack 構建效率的提升會越明顯. 下圖是我在專案使用 HappyPack 前後的一張構建時間對比圖:
HappyPack 會充分利用系統的資源來提升 Webpack 的構建效率, 所以系統本身的硬體配置會對 HappyPack 的使用有一定的影響.
HappyPack 不限於處理 js 檔案, 也可以同時處理 css/vue 等其它型別檔案. HappyPack 支援多個例項, 可以建立多個例項來分別處理不同的型別檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
let HappyPack = require('happypack'); let os = require('os'); let happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); module.exports = { ... plugins: [ new HappyPack({ id: 'vue', threadPool: happyThreadPool, cache: true, verbose: true, loaders: ['vue-loader'], }), new HappyPack({ id: 'js', threadPool: happyThreadPool, cache: true, verbose: true, loaders: ['babel-loader'], }) # others ] ... } |
此外, HappyPack 同時還利用快取來使得 rebuild 更快.
開發環境和生產環境下均可使用. 關於其原理分析, 請看 HappyPack 原理解析
3. 提取公共的依賴模組
無論是單頁還是多頁應用, 在生產環境下, 通常都會利用 CommonsChunkPlugin 外掛來提供公共的依賴模組:
1 2 3 4 5 6 7 8 |
new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: ({resource}) => { resource && resource.indexOf('node_modules') && resource.match(/.js$/) } }), |
上述的配置會提取 node_modules
下的所有模組, 打包出來的結果可能是這樣的:
打包結果分析圖由 webpack-bundle-analyzer 提供
這樣提取了公共模組之後, 的確會減少業務包的大小, 但是, 這種方式會導致兩個問題:
- 業務越複雜, 三方依賴會越多, vendor 包會越大
- 沒有隔離業務路由元件, 所有的路由都有可能會去載入 vendor, 但並不是所有的路由元件都依賴
node_modules
下的所有模組
所以, 上述提取公共依賴的方式不可取. 我們應該去分析業務依賴和路由, 儘可能將所有路由元件的公共依賴提取出來:
1 2 3 4 5 6 7 8 9 10 11 12 |
entry: { app: path.resolve(__dirname, '../src/page/index.js'), vendor: [ 'vue', 'vuex', 'vue-router', 'vuex-router-sync', 'babel-polyfill', 'axios', '....' ] }, new webpack.optimize.CommonsChunkPlugin({ name: "vendor", filename: "vendor.js" }), |
前後兩種方式打包出來的 vendor 大小對比:
既要去提取公共依賴, 也要避免 vendor 包過於太大.
4. 檔案分離
檔案分離主要是將圖片和 CSS 從 js 中分離. 圖片和 CSS 都是 Webpack 需要構建的資源, 通過某種配置, 圖片可以以 base64 的方式混淆在 js 檔案中, 這會增加最終的 bundle 檔案的大小. 在 生產環境下, 應該將圖片和 CSS 從 js 中分離:
- 在生產環境下, 通過自定義外掛, 將圖片的本地引用替換為 CDN 的連結
- 在生產環境下, 通過 ExtractTextPlugin 來 提取 CSS.
5. 資源混淆和壓縮
Webpack提供的 UglifyJS 外掛由於採用單執行緒壓縮, 速度比較慢,
可以使用 Parallel 外掛進行優化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); let os = require('os'); new ParallelUglifyPlugin({ workerCount: os.cpus().length, cacheDir: '.cache/', uglifyJS: { compress: { warnings: false, drop_debugger: true, drop_console: true }, comments: false, sourceMap: true, mangle: true } }) |
6. Gzip 壓縮
在生產環境下, 如果想進一步減小 bundle 檔案的大小, 可以使用 Gzip 壓縮.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let CompressionPlugin = require("compression-webpack-plugin"); module.exports = { plugins: [ new CompressionPlugin({ asset: "[path].gz[query]", algorithm: "gzip", test: /.(js|html)$/, threshold: 10240, minRatio: 0.8 }) ] } |
Gzip 壓縮能有效減少 bundle 的檔案大小:
部署上線時, 服務端也需要開啟 gzip 壓縮
7. 按需載入
在單頁應用中, 一個應用可能會對應很多路由, 每個路由都會對應一個元件; 如果將這些元件全部全部放進一個 bundle, 會導致最終的 bundle 檔案比較大(看上圖的 app bundle 檔案). 因而, 我們需要利用 Webpack 的 Code Splitting 功能, 將程式碼進行分割, 實現路由的按需載入.
在 Vue 中, 利用 vue-router
的懶載入功能, 是比較容易實現按需載入的:
當訪問首頁時, 會去載入 Index
元件, 此時並不會載入 Info
元件; 只有當路由切換為 /info
時, Info
元件才會被載入.
以上是個人的一些總結, 如有不足請指正, 如有遺漏, 歡迎補充.
vue-startup 是基於上述的一些優化措施寫的一個 Vue 的腳手架, 歡迎 star.