webpack優化
1.production 模式打包自帶優化
tree shaking
tree shaking是一個術語、通常用於打包時移除js中未引用的程式碼(dead-code),它依賴於ES6模組系統中的import 和 export 的靜態結構特性
開發時引入一個模組時,如果只引用其中一個功能,上線打包時只會把用到的功能打包進bundle中,其他沒有用到的功能都不會打包進來,可以實現最簡單的基本優化
- 建立一個 math.js, 丟擲兩個方法
export const add = (a, b) => a + b export const minus = (a, b) => a- b
- 在 main.js 中使用
// tree shaking 分析 // 若是此時使用 require 引入,不管 math 中的方法是否使用,都會被打包 const math = require('./utils/math') // 若是使用 import 引入, 只會打包使用了 math 的方法 import { add } from './utils/math' console.log('index 頁面',math.add(1,2)); console.log('index 頁面',add(1,2));
- 根據不同的引入方式進行打包,觀察打包後的檔案
scope hoisting
Scope hositing 作用:是將模組之間的關係進行結果推測,可以讓webpack檔案打包出來的程式碼檔案更小、執行的更快
scope hositing實現原理:分析出模組之間的依賴關係,儘可能的把打散的模組合併到一個函式中,但是前提是不能造成程式碼冗餘, 因此只有哪些被引用了一次的模組可能被合併
由於scope hositing 需要分析出模組之間的依賴關係,因此原始碼必須使用ES6模組化語句,不然就不能生效,原因和 tree shaking一樣
在 main.js 中定義幾個變數並輸出
const a = 1 const b = 2 const c = 3 // webpack 在這裡會進行 預執行,將結果推斷後打包放在這裡 console.log(a + b + c) console.log(a, b, c)
打包之後程式碼變成
console.log(6),console.log(1,2,3)
因為三個變數只是在這個地方定義並且使用,並沒有在其他位置使用,webpack會直接以具體的數值進行打包,節省了三個變數的定義
程式碼壓縮
所有程式碼使用UglifyJsPlugin進行壓縮、混淆
2.CSS優化
2.1 將CSS提取到獨立檔案中
Mini-css-extract-plugin 是用於將 CSS 提取為獨立的檔案的外掛,對每個包含css的js檔案都會建立一個css檔案,支援按需載入css和sourceMap
只能用於webpack4中,優勢
- 非同步載入
- 不重複編譯,效能更好
- 更容易使用
- 只針對css
使用
安裝
npm i -D mini-css-extract-plugin
引用
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
建立外掛物件,配置抽離的css檔名,支援placeholder語法
new MiniCssExtractPlugin({ filename:'[name].css' // [name] 就是 placeholder 語法 })
將原來配置的所有 style-loader 替換為 MiniCssExtractPlugin.loader
{ test:/\.css$/, use:[MiniCssExtractPlugin.loader, 'css-loader'] }, { test:/\.less$/, use:[MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] }, { test:/\.scss$/, use:[MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
2.2 自動新增CSS字首
使用 postcss,需要使用 postcss-loader 和 autoprefixer
安裝
npm i -D postcss-loader autoprefixer
修改配置檔案,將 postcss-loader 放置在 css-loader 右邊
{ test:/\.css$/, use:[MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader',] }, { test:/\.less$/, use:[MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] }, { test:/\.scss$/, use:[MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
專案根目錄下新增 postcss 的配置檔案: postcss.config.js
module.exports = { plugins: [ require('autoprefixer')({ browsers: [ // 加這個後可以出現額外的相容性字首 "> 0.01%" ] }) ] }
2.3 開啟CSS壓縮
需要使用 optimize-css-assets-webpack-plugin 外掛來完成css壓縮
但是由於配置css壓縮時會覆蓋掉webpack預設的優化設定,導致JS程式碼無法壓縮,所以還需要把JS程式碼壓縮外掛倒入進來 terser-webpack-plugin
安裝
npm i -D terser-webpack-plugin optimize-css-assets-webpack-plugin
引用
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
配置
optimization:{ minimizer: [ new TerserPlugin({}), new OptimizeCssAssetsPlugin({}) ] }
webpack4預設採用的JS壓縮外掛是 uglifyjs-webpack-plugin,在 mini-css-extract-plugin上一個版本中還推薦使用該外掛,但是新的版本卻建議使用 terser-webpack-plugin
3.JS優化
code splitting 是webpack打包時用到的重要的優化特性之一、此特效能夠把程式碼分離到不同的bundle中,然後可以按需載入或者並行載入這些檔案,程式碼分離可以用於獲取更小的bundle,以及控制資源載入優先順序,如果能夠合理的使用能夠極大影響載入時間
- 三種常見的程式碼分離方法
- 入口起點:使用entry配置,手動的分離程式碼
- 放置重複:使用 SplitChunksPlugin 去重和分離 chunk
- 動態匯入:通過模組的行內函數呼叫來分離程式碼
3.1手動配置多入口
- 手動配置多入口會存在一些問題
- 如果入口chunks之間包含重複的模組,哪些重複的模組都會被引入到各個打包後的js檔案中
- 方法不夠靈活,並且不能將核心應用程式邏輯進行動態拆分程式碼
在webpack配置檔案中配置多個入口
entry:{ main: './src/main.js', other: './src/other.js' }, output:{ path: path.join(__dirname, '..', './dist'), filename: '[name].js', publickPath: '/' }
在main.js 和 other.js 中都共同引入一個模組, 並使用其功能
Main.js
import $ from 'jquery' $(() => { $('<div></div>').html('main').appendTo('body') })
other.js
import $ from 'jquery' $(() => { $('<div></div>').html('other').appendTo('body') })
打包檔案,可以看到 main 和 other 打包的檔案中都載入的了 jquery
3.2抽取公共程式碼
webpack4 以上使用的外掛為 SplitChunksPlugin,webpack4 之前的使用的 CommonChunkPlugin已經被移除,最新版本的webpack中只需要在配置檔案中的optimization節點下新增一個splitChunks屬性即可進行相關配置
修改配置檔案
optimization splitChunks:{ chunks: "all" } }
打包檢視檔案
打包之後會將各自的入口檔案進行打包,額外會再生產一份js檔案,此檔案中就是各個chunk中所引用的公共部分
splitChunksPlugin 配置引數
SplitChunksPlugin 的配置只需要在 optimization 節點下的 splitChunks 進行修改即可,如果沒有任何修改,則會使用預設設定
預設的 SplitChunksPlugin 配置適用於絕大多數使用者
- webpack 會基於如下預設原則自動分割程式碼
- 公用程式碼塊或者來自 node_modules 資料夾的元件模組
- 打包的程式碼塊大小超過30kb,最小化壓縮之前的
- 按需載入程式碼塊時,同時傳送的請求最大數量不應該超過5
- 頁面初始化時,同時傳送的請求最大數量不應該超過3
- SplitChunksPlugin 預設配置
module.exports = { optimization: { splitChunks: { chunks: 'async', // 只對非同步載入的模組進行拆分,import('jquery').then()就是典型的非同步載入,可選項還有 all | initial minSize: 30000, // 模組最少大於 30kb 才會拆分 maxSize: 0, // 為0時模組大小無上限,只要大於 30kb 都會拆分。若是非0,超過了maxSize的值,會進一步拆分 minChunks: 1, // 模組最少引用一次才會拆分 maxAsyncRequests: 5, // 非同步載入時同時傳送的請求數量最大不能超過5,超過5的部分不拆分 maxInitialRequests: 3, // 頁面初始化時,同時傳送的請求數量最大不能超過3,超過3的不跟不拆分 automaticNameDelimiter: '~', // 預設的連線符 name: true, // 拆分的chunk名,設定為true表示根據模組名和CacheGroup的key來自動生成,使用上面的連線符連線 cacheGroups: { // 快取組配置,上面配置讀取完成後進行拆分,如果需要把多個模組拆分到一個檔案,就需要快取,所以命名為快取組 vendors: { // 自定義快取組名 test: /[\\/]node_modules[\\/]/, // 檢查 node_modules 目錄,只要模組在該目錄下就使用上面配置拆分到這個組 priority: -10, // 權重為-10,決定了那個組優先匹配,假如node_modules下面有個模組要拆分,同時滿足vendors和default組,此時就會分到 priority 值比較大的組,因為 -10 > -20 所以分到 vendors 組 filename:'vendoes.js' }, default: { // 預設快取組名 minChunks: 2, // 最少引用兩次才會被拆分 priority: -20, // 權重 -20 reuseExistingChunk: true // 如果主入口中引入了兩個模組,其中一個正好也引用了後一個,就會直接複用,無需引用兩次 } } } } };
- webpack 會基於如下預設原則自動分割程式碼
3.3動態匯入(懶載入)
webpack4預設是允許import語法動態匯入的,但是需要babel的外掛支援,最新版babel的外掛包為:@babel/plugin-syntax-dynamic-import,需要注意動態匯入最大的好處就是實現了懶載入,用到那個模組才會載入那個模組,可以提高SPA應用程式的首屏載入速度,三大框架的路由懶載入原理一樣
安裝
npm i -D @babel/plugin-syntax-dynamic-import
修改 .babelrc ,新增 @babel/plugin-syntax-dynamic-import 外掛
{ "presets": ["@babel/env"], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import" ] }
將jq模組動態匯入
function getDivDom(){ // import('jquery') 返回的是一個 promise,若是低版本需要注意 return import('jquery').then(({default: $}) => { return $('<div></div>').html('動態匯入') }) }
給某個按鈕新增點選事件,點選後呼叫getDivDom函式建立元素並新增到頁面
window.onload = () => { document.getElementById('btn').addEventListener('click',() => { getDivDom().then(item => { item.appendTo('body') }) }) }
4.noParse
在引入一些第三方模組時,如jq等,我們知道其內部肯定不會依賴其他模組,因為我們用到的只是一個單獨的js或者css檔案,所以此時如果webpack再去解析他們的內部依賴關係,其實是非常浪費時間的,就需要阻止webpack浪費精力去解析這些明知道沒有依賴的庫,可以在webpack的配置檔案的module節點下加上noParse,並配置正則來確定不需要解析依賴關係的模組
module:{
noParse: /jquery|bootstrap/ // jquery|bootstrap 之間不能加空格變成 jquery | bootstrap, 會無效
}
5.IgnorePlugin
在引入一些第三方模組時,例如momentJS、dayJS,其內部會做i18n處理,所以會包含很多語言包,而語言包打包時會比較佔用空間,如果專案只需要用到中文或者少數語言,可以忽略掉所有的語言包,然後按需引入語言包,從而使得構建效率更高,打包生成的檔案更小
以moment為例
import moment from 'moment' moment.locale('zh-CN') // 設定為中文 console.log(moment().subtract(6, 'days').calendar())
首先要找到moment依賴的語言包時什麼,通過檢視moment的原始碼來分析
function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && (typeof module !== 'undefined') && module && module.exports) { try { oldLocale = globalLocale._abbr; var aliasedRequire = require; aliasedRequire('./locale/' + name); getSetGlobalLocale(oldLocale); } catch (e) {} } return locales[name]; }
通過 aliasedRequire('./locale/' + name) 可以知道momentJS的多語言目錄是locale,所有的語言JS檔案都在這個目錄中
使用IgnorePlugin外掛忽略其依賴
將momentJS的多語言目錄locale忽略
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
需要使用某些依賴時自行手動引入
忽略其依賴之後,moment.locale('zh-CN')就會失效,因為其所依賴的語言包全都被忽略了,需要手動將其引入
import moment from 'moment' import 'moment/locale/zh-cn' // 需要手動引入方可生效 moment.locale('zh-CN') console.log(moment().subtract(6, 'days').calendar())
6.DLLPlugin
在引入一些第三方模組時,例如Vue、React等,這些框架的檔案一般都是不會修改的,而每次打包都需要去解析他們,也會影響打包速度,就算是做了拆分,也只是提高了上線後的使用者訪問速度,並不會提高構建速度,所以如果需要提高構建速度,應該使用動態連結庫的方式,類似windows的dll檔案
藉助DLLPlugin外掛實現將這些框架作為一個個的動態連結庫,只構建一次,以後的每次構建都只會生成自己的業務程式碼,可以很好的提高構建效率
豬喲思想在於,講一些不做修改的依賴檔案,提前打包,這樣我們開發程式碼釋出的時候就不需要再對這些程式碼進行打包,從而節省了打包時間,主要使用兩個外掛: DLLPlugin和DLLReferencePlugin
需要注意的是,若是使用的DLLPlugin,CleanWebpackPlugin外掛會存在衝突,需要移除CleanWebpackPlugin外掛
DLLPlugin
使用一個單獨webpack配置建立一個dll檔案,並且它還建立一個manifest.json,DLLReferencePlugin使用該json檔案來做對映依賴性,這個檔案會告訴webpack哪些檔案已經提取打包好了
- 配置引數
- context(可選):manifest檔案中請求的上下文,預設為該webpack檔案上下文
- name:公開的dll函式的名稱,和output.library保持一致即可
- path:manifest.json 生成的資料夾及名稱
- 配置引數
DLLReferencePlugin
該外掛主要用於主webpack配置,它引用的dll需要預先構建的依賴該系
- 配置引數
- context: manifest檔案中的請求上下文
- manifest: DLLPlugin外掛生成的manifest.json
- content(可選): 請求的對映模組id(預設為manifest.content)
- name(可選): dll暴露的名稱
- scope(可選): 字首用於訪問dll的檔案
- sourceType(可選): dll是如何暴露(libraryTarget)
- 配置引數
將VUE專案中的庫抽取成DLL
- 準備一份將VUE打包成DLL的webpack配置檔案。
- 在build目錄下新建一個檔案webpack.vue.js,專門用於打包vue的DLL的。
- 配置入口:將多個要做成dll的庫全放進來
- 配置出口:一定要設定library屬性,將打包好的結果暴露在全域性
- 配置plugin:設定打包後dll檔名和manifest檔案所在地
// 此配置檔案 是打包VUE全家桶的
const path = require('path')
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry:{
vue: [
'vue/dist/vue',
'vue-router'
]
},
output:{
path: path.resolve(__dirname, '../dist'),
filename: '[name]_dll.js',
library: '[name]_dll' // 最終會在全域性暴露出一個[name]_dll的物件
},
plugins:[
new webpack.DllPlugin({
name: '[name]_dll',
path: path.resolve(__dirname, '../dist/manifest.json'),
})
]
}
webpack.vue.js 只是用來打包生成 [name]_dd.js 檔案和 manifest.json檔案的,是不需要參與到業務程式碼打包的,因為只會在每一次修改了需要生成dll檔案的時間才會執行一次,否則不需要參與到打包
- 在
webpack.base.js
中進行外掛的配置
使用DllReferencePlugin指定manifest檔案的位置即可
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dist/manifest.json'),
})
- 由於[name]_dll檔案生成之後,並沒有動態的引入進去,所以需要一個外掛可以動態的將生成的dll檔案引入
安裝add-asset-html-webpack-plugin
npm i -D add-asset-html-webpack-plugin
配置外掛自動新增script標籤到HTML中,需要注意的是,必須在HtmlWebpackPlugin後面引入,因為HtmlWebpackPlugin是生產一個html檔案,AddAssetHtmlWebpackPlugin是在已有的html中注入一個script,否則會被覆蓋
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dist/vue_dll.js')
})
7.瀏覽器快取
在做了眾多程式碼分離的優化後,其目的是為了更好的利用瀏覽器快取,達到提高訪問速度的效果,所以構建專案時做程式碼分割是必須的,
例如將固定的第三方模板抽離,下次修改了業務程式碼,重新發布上線不重啟伺服器,使用者再次訪問伺服器就不需要再次載入第三方模板了
但是此時會遇到一個問題,如果再次打包上線不重啟伺服器,客戶端會把以前的業務程式碼和第三方模組同時快取,再次訪問時依舊會訪問快取中的業務程式碼,所以會導致業務程式碼也無法更新
需要在output節點的filename中使用placeholder語法,根據程式碼內容生產檔名的hash,之後每次打包業務程式碼時,如果有改變,會生成新的hash作為檔名,瀏覽器就不會使用快取了,而第三方模組不會重新打包生成新的名字,則會繼續使用快取
output: {
path: path.join(__dirname, '..','./dist'),
filename:'[name].[contenthash:8].bundle.js',
publicPath: '/'
},
8.打包分析
專案構建完成後,需要通過一些工具對打包後的bundle進行分析,通過分析可以得到一些有用的資訊
- 使用
--profile --josn
引數,以json格式來輸出打包後的結果到某個指定的檔案中
webpack --profile --json > stats.json
- 將stats.json檔案放到工具中進行分析
官方工具: analyse
webpack-chart:webpack stats 可互動餅圖。
webpack-visualizer:視覺化並分析你的 bundle,檢查哪些模組佔用空間,哪些可能是重複使用的。
webpack-bundle-analyzer:一個 plugin 和 CLI 工具,它將 bundle 內容展示為便捷的、互動式、可縮放的樹狀圖形式。是一個外掛,可以以外掛安裝到專案中
- 安裝
npm i -D webpack-bundle-analyzer
- 使用, 配置在一個單獨的檔案中 webpack.analyse.js (直接拷貝的web pack.prod.js,僅僅是多了此外掛的使用)
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin() ] }
webpack bundle optimize helper:此工具會分析你的 bundle,併為你提供可操作的改進措施建議,以減少 bundle 體積大小。
9.prefetching && preloading
在優化訪問效能時,除了利用瀏覽器快取之外,還需要涉及到一個效能指標: 覆蓋率(coverage rate)
可以在chrome瀏覽器的控制檯中按 ctrl + shift + p,查詢 coverage,開啟覆蓋率皮膚,開始錄製後重新整理頁面,即可看到每個js檔案的覆蓋率,以及總的覆蓋率
想提高覆蓋率,需要儘可能多的使用impor動態匯入,也就是懶載入的功能,將一切能使用懶載入的地方都是用懶載入,這樣可以大大的提高覆蓋率
但是有時候使用懶載入會影響使用者體驗,所以可以在使用懶載入的時候使用魔法註釋(Magic Comments): prefetching,是指在首頁資源載入完畢後,空閒的時候,將動態匯入的資源載入進來,這樣既可以提高首屏載入速度,也可以解決懶載入可能會影響使用者體驗的問題