1.提高 Webpack 打包速度
(1)優化loader的檔案搜尋範圍
Babel 是編寫下一代 JavaScript 的編譯器
對于 Loader 來說,影響打包效率首當其沖必屬 Babel 了。因為 Babel 會将代碼轉為字元串生成 AST,然後對 AST 繼續進行轉變最後再生成新的代碼,項目越大,轉換代碼越多,效率就越低
首先我們可以優化 Loader 的檔案搜尋範圍,在使用loader時,我們可以指定哪些檔案不通過loader處理,或者指定哪些檔案通過loader處理。
module.exports = {
module: {
rules: [
{ // js 檔案才使用 babel
test: /\.js$/,
use: ['babel-loader'], // 隻處理src檔案夾下面的檔案
include: path.resolve('src'), // 不處理node_modules下面的檔案
exclude: /node_modules/
}
]
}
}
(2)将babel編譯過的檔案緩存起來
對于babel-loader,我們還可以将 Babel 編譯過的檔案緩存起來,下次隻需要編譯更改過的代碼檔案即可,這樣可以大幅度加快打包時間。
通過使用 cacheDirectory 選項,将 babel-loader 提速至少兩倍。這會将轉譯的結果緩存到檔案系統中。
{
test: /\.js$/,
use: 'babel-loader?cacheDirectory',
include: [resolve('src'), resolve('test') ,resolve('node_modules/webpack-dev-server/client')] //緩存用戶端(浏覽器)這個檔案夾
}
cache-loader:緩存其他loader
除了 babel-loader,如果我們想讓其他的 loader 的處理結果也緩存,該怎麼做呢?
答案是可以使用 cache-loader。在一些性能開銷較大的 loader 之前添加 cache-loader,以便将結果緩存到磁盤裡
安裝
npm install --save-dev cache-loader
module.exports = {
module: {
rules: [
{
// js 檔案才使用 babel
test: /\.js$/,
use: ['cache-loader', ...loaders], //...loader是要指定的要緩存的loader的一個統稱
include: path.resolve('src'), //前面babel-loader 時候,已經統一指定隻處理src下的檔案,這裡就不用單獨再寫了
},
],
},
};
那這麼說的話,我給每個loder前面都加上cache-loader,然而凡事物極必反,儲存和讀取這些緩存檔案會有一些時間開銷,是以請隻對性能開銷較大的 loader 使用 cache-loader。關于這個cache-loader更詳細的使用方法請參照這裡cache-loader
那上面說了,我可以給指定的loader做緩存,那麼我怎麼知道是哪些loader比較慢,需要給其做緩存呢?因為不能無腦的全部緩存,這樣儲存和讀取有時間開銷,我們要知己知彼才能對症下藥。是以我們需要一個分析速度的插件:
(3)分析各loader和plugin速度插件:speed-measure-webpack-plugin
// 安裝npm install --save-dev speed-measure-webpack-plugin// 使用方式const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
module: {},
plugins: [ new MyPlugin(), new MyOtherPlugin()
]
});
運作時候如下圖所示

但需要注意的是:HardSourceWebpackPlugin 和 speed-measure-webpack-plugin 不能一起使用。
二、使用多線程打包
1.HappyPack :就是能夠讓Webpack把打包任務分解給多個子線程去并發的執行,子線程處理完後再把結果發送給主線程。
在使用 Webpack 對項目進行建構時,webpack建構過程中的有兩個部分是直接影響建構效率的,一個是檔案的編譯,另一個則是檔案的分類打包。相較之下檔案的編譯更為耗時,而且在Node環境下檔案隻能一個一個去處理,是以這塊的優化需要解決。
由于 JavaScript 是單線程模型,要想發揮多核 CPU 的能力,隻能通過多程序去實作,而無法通過多線程實作。
而 Happypack 的作用就是将檔案解析任務分解成多個子程序(多個子程序也就意味着多個子線程,一個程序裡面有一個線程)并發執行。子程序處理完任務後再将結果發送給主程序。是以可以大大提升 Webpack 的項目構件速度
由于HappyPack 對file-loader、url-loader 支援的不友好,是以不建議對該loader使用。
module: {
rules: [
{
test: /\.js$/, // 把對 .js 檔案的處理轉交給 id 為 babel 的 HappyPack 執行個體
use: ['happypack/loader?id=babel'],
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.css$/, // 把對 .css 檔案的處理轉交給 id 為 css 的 HappyPack 執行個體
use: ['happypack/loader?id=css']
}
]
},
plugins: [ new HappyPack({
id: 'js', //ID是辨別符的意思,ID用來代理目前的happypack是用來處理一類特定的檔案的
threads: 4, //你要開啟多少個子程序去處理這一類型的檔案
loaders: [ 'babel-loader' ]
}), new HappyPack({
id: 'css',
threads: 2,
loaders: [ 'style-loader', 'css-loader' ]
})
]
2.thread-loader:在worker 池(worker pool)中運作加載器loader。把thread-loader 放置在其他 loader 之前, 放置在這個 thread-loader 之後的 loader 就會在一個單獨的 worker 池(worker pool)中運作。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
{
loader: "thread-loader", // 有同樣配置的 loader 會共享一個 worker 池(worker pool) options: { // 産生的 worker 的數量,預設是 cpu 的核心數
workers: 2, // 一個 worker 程序中并行執行工作的數量
// 預設為 20
workerParallelJobs: 50, // 額外的 node.js 參數
workerNodeArgs: ['--max-old-space-size', '1024'], // 閑置時定時删除 worker 程序
// 預設為 500ms
// 可以設定為無窮大, 這樣在監視模式(--watch)下可以保持 worker 持續存在
poolTimeout: 2000, // 池(pool)配置設定給 worker 的工作數量
// 預設為 200
// 降低這個數值會降低總體的效率,但是會提升工作分布更均一
poolParallelJobs: 50, // 池(pool)的名稱
// 可以修改名稱來建立其餘選項都一樣的池(pool)
name: "my-pool"
}
},
{
loader:'babel-loader'
}
]
}
]
}
}
同樣,thread-loader也不是越多越好,也請隻在耗時的loader 上使用。
3.webpack-parallel-uglify-plugin :開啟多個子程序
在Webpack3 中,我們一般使用 UglifyJS 來壓縮代碼,但是這個是單線程運作的,也就是說多個js檔案需要被壓縮,它需要一個個檔案進行壓縮。是以說在正式環境打包壓縮代碼速度非常慢(因為壓縮JS代碼需要先把代碼解析成AST文法樹,再去應用各種規則分析和處理AST,導緻這個過程耗時非常大)。為了加快效率,我們可以使用 webpack-parallel-uglify-plugin 插件,該插件會開啟多個子程序,把對多個檔案壓縮的工作分别給多個子程序去完成,但是每個子程序還是通過UglifyJS去壓縮代碼。無非就是變成了并行處理該壓縮了,并行處理多個子任務,提高打包效率。來并行運作 UglifyJS,進而提高效率。
在 Webpack4 中,我們就不需要以上這些操作了,隻需要将 mode 設定為 production 就可以預設開啟以上功能。代碼壓縮也是我們必做的性能優化方案,當然我們不止可以壓縮JS 代碼,還可以壓縮HTML、CSS 代碼,并且在壓縮 JS 代碼的過程中,我們還可以通過配置實作比如删除 console.log 這類代碼的功能。
let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
module: {},
plugins: [ new ParallelUglifyPlugin({
workerCount:3,//開啟幾個子程序去并發的執行壓縮。預設是目前運作電腦的cPU核數減去1 uglifyJs:{
output:{
beautify:false,//不需要格式化
comments:false,//不保留注釋 },
compress:{
warnings:false,//在Uglify]s除沒有用到的代碼時不輸出警告
drop_console:true,//删除所有的console語句,可以相容ie浏覽器
collapse_vars:true,//内嵌定義了但是隻用到一次的變量
reduce_vars:true,//取出出現多次但是沒有定義成變量去引用的靜态值 }
},
})
]
}
4.DllPlugin&DllReferencePlugin :将特定的類庫提前打包成動态連結庫
DllPlugin可以将特定的類庫提前打包成動态連結庫,在一個動态連結庫中可以包含給其他子產品調用的函數和資料,把基礎子產品獨立出來打包到單獨的動态連接配接庫裡,當需要導入的子產品在動态連接配接庫裡的時候,子產品不用再次被打包,而是去動态連接配接庫裡擷取。這種方式可以極大的減少打包類庫的次數,隻有當類庫更新版本才有需要重新打包,并且也實作了将公共代碼抽離成單獨檔案的優化方案。
這裡我們可以先将react、react-dom單獨打包成動态連結庫,首先建立一個新的webpack配置檔案:webpack.dll.js
個人了解的dll基本過程:
1、第一次npm run的時候,把請求的内容存儲起來(存儲在映射表中)
2、再次請求時,先從映射表中找,看請求的内容是否有緩存,有則加載緩存(類似浏覽器的緩存政策,命中緩存),沒有就正常打包。
3、直接從緩存中讀取。
配置:
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = { // 想統一打包的類庫
entry:['react','react-dom'],
output:{
filename: '[name].dll.js', //輸出的動态連結庫的檔案名稱,[name] 代表目前動态連結庫的名稱
path:path.resolve(__dirname,'dll'), // 輸出的檔案都放到 dll 目錄下
library: '_dll_[name]',//存放動态連結庫的全局變量名稱,例如對應 react 來說就是 _dll_react },
plugins:[ new DllPlugin({ // 動态連結庫的全局變量名稱,需要和 output.library 中保持一緻
// 該字段的值也就是輸出的 manifest.json 檔案 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]', // 描述動态連結庫的 manifest.json 檔案輸出時的檔案名稱
path: path.join(__dirname, 'dll', '[name].manifest.json')
})
]
}
然後我們需要執行這個配置檔案生成依賴檔案:
webpack --config webpack.dll.js --mode development
接下來我們需要使用 DllReferencePlugin 将依賴檔案引入項目中
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = { // ...省略其他配置 plugins: [ new DllReferencePlugin({ // manifest 就是之前打包出來的 json 檔案
manifest:path.join(__dirname, 'dll', 'react.manifest.json')
})
]
}
5. noParse:可以用于配置那些子產品檔案的内容不需要進行解析(即無依賴) 的第三方大型類庫(例如jquery,lodash)等,使用該屬性讓 Webpack不掃描該檔案,以提高整體的建構速度。
module.exports = {
module: {
noParse: /jquery|lodash/, // 正規表達式
// 或者使用函數 noParse(content) { return /jquery|lodash/.test(content)
}
}
}
6.IgnorePlugin :IgnorePlugin用于忽略某些特定的子產品,讓webpack 不把這些指定的子產品打包進去。
module.exports = { // ...省略其他配置 plugins: [ new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
]
}
webpack.IgnorePlugin()參數中第一個參數是比對引入子產品路徑的正規表達式,第二個參數是比對子產品的對應上下文,即所在目錄名。
三、值的注意的小點點
- resolve.extensions:用來表明檔案字尾清單,預設查找順序是 ['.js', '.json'],如果你的導入檔案沒有添加字尾就會按照這個順序查找檔案。我們應該盡可能減少字尾清單長度,然後将出現頻率高的字尾排在前面
- resolve.alias:可以通過别名的方式來映射一個路徑,能讓 Webpack 更快找到路徑
module.exports ={ // ...省略其他配置 resolve: {
extensions: [".js",".jsx",".json",".css"],
alias:{ "jquery":jquery
}
}
};
四、減少Wwebpack打包後的檔案體積
1.image-webpack-loader 對圖檔進行壓縮和優化
image-webpack-loader這個loder可以幫助我們對打包後的圖檔進行壓縮和優化,例如降低圖檔分辨率,壓縮圖檔體積等。
module.exports ={ // ...省略其他配置 module: {
rules: [
{
test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
use: [ 'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: false,
},
pngquant: {
quality: '65-90',
speed: 4
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75
}
}
}
]
}
]
}
};
2.删除無用的CSS樣式
有時候一些時間久遠的項目,可能會存在一些CSS樣式被疊代廢棄,需要将其剔除掉,此時就可以使用purgecss-webpack-plugin插件,該插件可以去除未使用的CSS,一般與 glob、glob-all 配合使用。
注意:此插件必須和CSS代碼抽離插件mini-css-extract-plugin配合使用。
例如我們有樣式檔案style.css
body{
background: red
}
.class1{
background: red
}
這裡的.class1顯然是無用的,我們可以搜尋src目錄下的檔案,删除無用的樣式。
const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');
module.exports ={ // ... plugins: [ // 需要配合mini-css-extract-plugin插件
new PurgecssPlugin({
paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`,
{nodir: true}), // 不比對目錄,隻比對檔案 })
}),
]
}
3.以CDN方式加載資源
我們知道,一般常用的類庫都會釋出在CDN上,是以,我們可以在項目中以CDN的方式加載資源,這樣我們就不用對資源進行打包,可以大大減少打包後的檔案體積。
以CDN方式加載資源需要使用到add-asset-html-cdn-webpack-plugin插件。我們以CDN方式加載jquery為例:
const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')
module.exports ={ // ... plugins: [ new AddAssetHtmlCdnPlugin(true,{ 'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
})
], //在配置檔案中标注jquery是外部的,這樣打包時就不會将jquery進行打包了 externals:{ 'jquery':'$'
}
}
4.搖樹:Tree Shaking
5.開啟Scope Hoisting
Scope Hoisting 可以讓 Webpack 打包出來的代碼檔案更小、運作的更快, 它又譯作 "作用域提升",是在 Webpack3 中新推出的功能。
由于最初的webpack轉換後的子產品會包裹上一層函數,import會轉換成require,因為函數會産生大量的作用域,運作時建立的函數作用域越多,記憶體開銷越大。而Scope Hoisting 會分析出子產品之間的依賴關系,盡可能的把打包出來的子產品合并到一個函數中去,然後适當地重命名一些變量以防止命名沖突。這個功能在webpack4中,當我們将mode設定為production時會自動開啟。
比如我們希望打包兩個檔案
let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;// 引入dimport d from './d';
console.log(d)
最終打包後的結果會變成 console.log(6),這樣的打包方式生成的代碼明顯比之前的少多了,并且減少多個函數後記憶體占用也将減少。如果你希望在開發模式development中開啟這個功能,隻需要使用插件 webpack.optimize.ModuleConcatenationPlugin() 就可以了。
module.exports = { // ... plugins: [ // 開啟 Scope Hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
]
}
6.按需加載&動态加載
大家在開發單頁面應用項目的時候,項目中都會存在十幾甚至更多的路由頁面。如果我們将這些頁面全部打包進一個檔案的話,雖然将多個請求合并了,但是同樣也加載了很多并不需要的代碼,耗費了更長的時間。那麼為了首頁能更快地呈現給使用者,我們肯定是希望首頁能加載的檔案體積越小越好,這時候我們就可以使用按需加載,将每個路由頁面單獨打包為一個檔案。在給單頁應用做按需加載優化時,一般采用以下原則:
- 對網站功能進行劃分,每一類一個chunk
- 對于首次打開頁面需要的功能直接加載,盡快展示給使用者,某些依賴大量代碼的功能點可以按需加載
- 被分割出去的代碼需要一個按需加載的時機
動态加載目前并沒有原生支援,需要babel的插件:plugin-syntax-dynamic-import。安裝此插件并且在.babelrc中配置:
{ // 添加
"plugins": ["transform-vue-jsx", "transform-runtime"],
}
例如如下示例:
index.js
let btn = document.createElement('button');
btn.innerHTML = '點選加載視訊';
btn.addEventListener('click',()=>{
import(/* webpackChunkName: "video" */'./video').then(res=>{
console.log(res.default);
});
});
document.body.appendChild(btn);
module.exports = { // ... output:{
chunkFilename:'[name].min.js'
}
}