Webpack打包優化
Webpack打包優化
Webapck 4 之後預設為我們做了很多配置項,內部開啟了很多優化功能。對於開發人員,這種開箱即用的體驗顯然是很好的,但是同時也會導致我們忽略了很多需要學習的東西,一旦出現什麼問題的時候,我們就無從下手了,下面我們就來看一下主要的優化配置項。
DefinePlugin
DefinePlugin 是用來為我們的程式碼來注入全域性成員的,在 production 模式下,這個外掛就會預設開啟。它會在我們的環境中注入了一個 process.env.NODE_ENV 這樣一個環境變數,我們可以通過這個環境變數去判斷執行環境,從而去執行一些相應的邏輯。
const webpack = require('webpack') module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js' }, plugins: [ new webpack.DefinePlugin({ // 值要求的是一個程式碼片段 API_BASE_URL: JSON.stringify('https://api.example.com') }) ] }
這樣我們就可以直接在環境中使用 API_BASE_URL 這個變量了
// main.js
console.log(API_BASE_URL)
Tree-shaking
Tree-shaking 顧名思義就是搖樹,伴隨著搖這個動作,我們會將樹上的枯樹枝和枯樹葉搖下來。而在我們的專案中 Tree-shaking 會將我們程式碼中沒有引用的部分去掉,Tree-shaking 並不是某一個配置選項,它是一組功能搭配使用的效果。我們可以使用 optimization 去開啟一些功能,optimization 就是優化的意思,下面我們來看看怎樣去配置它
module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js' }, optimization: { // 模組只匯出被使用的成員 usedExports: true, // 儘可能合併每一個模組到一個函式中 concatenateModules: true, // 壓縮輸出結果 // minimize: true } }
我們可以將 usedExports 想象成它就是去標記"枯樹葉"的,而 minimize 就是去搖下這些枯樹葉的。而 concatenateModules 將所有的程式碼都儘可能的合併到一個函式中去,這樣既提升了執行效率,又減少了程式碼的體積。這個特性又被稱為 Scope Hoisting,這時 Webpack3 中提出的一個特性。
- Tree-shaking 與 babel
由於 Webpack 的發展比較快,所以我們在找資料的時候,找到的資料並不一定適用於我們當前的版本,Tree-shaking 更是如此,很多資料中都顯示如果我們使用的 babel-loader 的話,就會導致 Tree-shaking 失效。因為 Tree-shaking 使用的前提就是必須使用 ES Modules 規範去組織我們的程式碼,而 @babel/preset-env 這個外掛內部就會將 ES Modules 的程式碼轉換為 commonjs 程式碼的方式,所以 Tree-shaking 就不能生效。但是實際你同時開啟兩者的話,Tree-shaking 還是會生效的,因為 @babel/preset-env 這個外掛最新的版本內部將 ES Modules 轉換為 commonjs 關掉了。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 載入模組時已經轉換了 ESM,則會導致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用預設配置,也就是 auto,這樣 babel-loader 會自動關閉 ESM 轉換
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
},
optimization: {
// 模組只匯出被使用的成員
usedExports: true,
// 儘可能合併每一個模組到一個函式中
// concatenateModules: true,
// 壓縮輸出結果
// minimize: true
}
}
sideEffects
Webpack4 中還新增了一個叫 sideEffects 的新特性,它允許我們去標識我們的程式碼是否有副作用,從而為 Tree shaking 提供更大的壓縮空間。副作用就是模組去執行時除了匯出成員之外所做的事情,sideEffects 一般只有我們在去開發一個 npm 模組的時候才會去使用,那是因為官網將 sideEffects 和 Tree shaking 混到了一起,所以很多人誤認為它們兩個是因果關係,其實它們兩個的關係不大。
當我們去封裝元件的時候,我們一般會將所有的元件都匯入在一個檔案中,然後通過這個檔案集體匯出,但是其他檔案引入這個檔案的時候,就會將這個匯出檔案的所有元件都引入
// components/index.js
export { default as Button } from './button'
export { default as Heading } from './heading'
// main.js
import { Button } from './components'
document.body.appendChild(Button())
這樣 Webpack 在打包的時候,也會將 Heading 元件打包到檔案中,這時 sideEffects 就能解決這個問題
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
sideEffects: true,
}
}
同時我們在 packag.json 中匯入將沒有副作用的檔案關閉,這樣就不會將無用的檔案打包到專案中了
{
"name": "side-effects",
"version": "0.1.0",
"main": "index.js",
"author": "maoxiaoxing",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9"
},
"sideEffects": false
}
使用 sideEffects 的需要注意的是,我們的程式碼中真的沒有副作用,如果有副作用的程式碼,我們就不能去這樣配置了。
// exten.js
// 為 Number 的原型新增一個擴充套件方法
Number.prototype.pad = function (size) {
// 將數字轉為字串 => '8'
let result = this + ''
// 在數字前補指定個數的 0 => '008'
while (result.length < size) {
result = '0' + result
}
return result
}
例如我們在 extend.js 檔案中為 Number 的原型新增一個方法,我們並沒有向外匯出成員,只是基於原型擴充套件了一個方法,我們在其他檔案匯入這個 extend.js
// main.js
// 副作用模組
import './extend'
console.log((8).pad(3))
如果我們還標識專案中所有模組沒有副作用的話,這個新增在原型的方法就不會被打包進去,在執行中肯定會報錯,還有就是我們在程式碼中匯入的 css 模組,也都是副作用模組,我們就可以在 package.json 中去標識我們的副作用模組
{
"name": "side-effects",
"version": "0.1.0",
"main": "index.js",
"author": "maoxiaoxing",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9"
},
"sideEffects": [
"./src/extend.js",
"*.css"
]
}
這樣標識的有副作用的模組也會被打包進來。
Webpack 程式碼分割(Code Splitting)
模組化的優勢固然很明顯,但是也存在一些弊端,就是在我們的專案中所有的程式碼都會被打包到一起,如果我們的專案過大的話,那麼我們的打包結果就會特別大。但是實際的情況是,我們在首次載入的時候,並不是所有的模組都是必須載入的,但是這些模組又被打包到一起,所以一方面在瀏覽器執行的時候會慢,一方面也會浪費一些流量和頻寬。所以合理的方式就是將我們的程式碼按照一定的規則打包到多個 js 檔案中去,做分包處理、按需載入,這樣我們就會大大提高我們的應用的響應速率。那麼有人可能會想到 Webpack 不就是將我們程式碼中散落的程式碼合併到一個函式中去執行,從而去提高效率,這裡為什麼又要做分包處理,不是自相矛盾嗎?其實任何事情都是物極必反,Webpack 做程式碼合併是因為我們在開發中往往模組化顆粒度太細,所以 Webpack 必須將很多程式碼合併到一起,但是如果總體程式碼量過大的話,就會導致我們的單個打包檔案過大,反而影響效率。所以模組化顆粒度太小不行,太大也不行,而 Code Splitting 就是為了解決我們模組化顆粒度太大的問題。
- 多入口打包
多入口打包就是將一個頁面作為一個打包入口,而對於不同頁面中公共的部分再去提取到公共的檔案中去,而多入口打包的配置也很容易
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: { // 多入口打包,多個入口檔案
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // 由於多入口打包,採用佔位符
},
optimization: {
splitChunks: {
// 自動提取所有公共模組到單獨 bundle
chunks: 'all'
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index'] // 配置chunk,防止同時載入
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
- Webpack 按需載入
按需載入是我們在開發中常見的需求,我們在處理打包的時候,我們可以需要哪個模組是,再載入哪個模組。Webpack 中支援動態匯入的方式去支援按需載入我們的模組,所有動態載入的模組都會被自動分包,相比於分包載入的方式,動態載入的方式更加靈活。
例如我們有兩個模組 album 和 posts,我們就可以使用 import 去實現動態匯入,import 返回一個 promise 物件,
// ./posts/posts.js
export default () => {
const posts = document.createElement('div')
posts.className = 'posts'
...
return posts
}
// ./album/album.js
export default () => {
const album = document.createElement('div')
album.className = 'album'
...
return album
}
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// mainElement.appendChild(posts())\
// 魔法註釋:給模組重新命名
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
css 的模組化打包
- MiniCssExtractPlugin 是一個能夠將 css 檔案從打包檔案中單獨提取出來的外掛,通過這個外掛我們就可以實現 css 模組的按需載入。
- optimize-css-assets-webpack-plugin 是一個能夠壓縮 css 檔案的外掛,因為使用了 MiniCssExtractPlugin 之後,就不需要使用 style 標籤的形式去載入 css 了,所以我們就不需要 style-loader 了
- terser-webpack-plugin 因為 optimize-css-assets-webpack-plugin 是需要使用在 optimization 的 minimizer 中的,而開啟了 optimization,Webpack 就會認為我們的壓縮程式碼需要自己配置,所以 js 檔案就不會壓縮了,所以我們需要安裝 terser-webpack-plugin 再去壓縮 js 程式碼
// 安裝 mini-css-extract-plugin
yarn add mini-css-extract-plugin --dev
// 安裝 optimize-css-assets-webpack-plugin
yarn add optimize-css-assets-webpack-plugin --dev
// 安裝 terser-webpack-plugin
yarn add terser-webpack-plugin --dev
接下來我們就可以配置它們了
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(), // 壓縮 js 程式碼
new OptimizeCssAssetsWebpackPlugin() // 壓縮模組化的 css 程式碼
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 將樣式通過 style 標籤注入
MiniCssExtractPlugin.loader, // 使用 MiniCssExtractPlugin 的 loader 就不需要 style-loader 了
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Dynamic import',
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin()
]
}
輸出 hash 檔名
一般我們去部署前端的資原始檔的時候,我們都會啟用伺服器的靜態資源快取,這樣對於使用者的瀏覽器而言就可以快取住我們的靜態資源,後續就不再需要請求伺服器去請求這些靜態資源了,這樣我們的應用的相應速度就會有一個大幅度的提升。不過開啟客戶端的靜態資源快取也會有問題,如果我們在設定快取時間過短的話,那麼快取就沒什麼意義了,而設定過長的話,一旦應用發生了更新,就沒有辦法即時更新到客戶端。為了解決這個問題,我們就需要在生產模式下,為檔名使用 hash,這樣一旦我們的檔案資源發生改變,我們的檔名稱也會隨之發生改變,而對於客戶端而言,全新的檔名也就意味著全新的請求,這樣我們就可以將快取時間設定的非常長,也不用去擔心檔案不更新的問題。
-
hash
專案級別的 hash,一旦任何檔案發生修改,都會生成新的 hash -
chunkhash
只要是同一路的打包,hash 都是相同的,例如一個模組內的 js 和 css 的 hash 字首都是相同的 -
contenthash
檔案級別的 hash,根據檔案內容輸出的 hash 值,只要是不同的檔案就有不同的 hash 值,這也是最推薦的 hash 方式
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
main: './src/index.js'
},
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
optimization: {
...
},
module: {
...
},
plugins: [
...
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css'
})
]
}