webpack之效能優化(webpack4)
在講解效能優化的方案之前,我們需要了解一下webpack的整個工作流程,
方案一:減少模組解析
也就是省略了構建chunk依賴模組的這幾個步驟
如果沒有loader對該模組進行處理,該模組的原始碼就是最終打包結果的程式碼。不對某個模組進行解析,可以縮短構建時間
哪些模組不需要解析?
模組中無其他依賴
webpack配置
配置module.noParse
,它是一個正則,被正則匹配到的模組不會解析
module.exports = { mode: "development", module: { noParse: /test/ } }
方案二:優化loader
1.對於某些庫,不使用loader
例如:babel-loader可以轉換ES6或更高版本的語法,可是有些庫本身就是用ES5語法書寫的,不需要轉換,使用babel-loader反而會浪費構建時間
通過module.rule.exclude
或module.rule.include
,排除或僅包含需要應用loader的場景,可以直接排除掉node_modules的所有包,也可以僅排除單獨的包
module.exports = { module: { rules: [ { test: /\.js$/, exclude:/node_modules/,// exclude: /lodash/ //或 // include: /src/, use: "babel-loader" } ] } }
2.利用cache-loader對模組進行快取
如果某個檔案內容不變,經過相同的loader解析後,解析後的結果也不變,所以可以將loader的解析結果儲存下來,讓後續的解析直接使用儲存的結果
module.exports = { module: { rules: [ { test:/\.js$/, use: ['cache-loader', 'babel-loader'] }, ], }, };
大家可能會感到疑惑,明明loader是從後往前執行的,那麼cache-loader是怎麼拿到babel-loader的結果的呢?
其實,每個loader上,還有一個pitch的靜態方法
function loader(source){ return `new source` } loader.pitch = function(filePath){ // 可返回可不返回 // 如果返回,返回原始碼 } module.exports = loader;
loader真正執行的順序是這樣的:
loader1.pitch => loader2.pitch => loader3.pitch => loader3 => loader2 => loader1
因此,以['cache-loader', 'babel-loader']為例,
第一次打包:
- 先呼叫cache-loader.pitch,發現無快取,往後執行,
- 呼叫babel-loader.pitch,也發現無快取,往後執行,
- 讀取當前需要處理的模組的程式碼
- 呼叫babel-loader,返回修改成es5的程式碼
- 呼叫cache-loader,返回babel-loader處理的結果程式碼並快取
第二次打包:
- 先呼叫cache-loader.pitch,發現有快取,則返回原始碼
- 直接返回上次處理好的原始碼,不會繼續往後走了
當然對於babel-loader,使用它本身的配置也是可以快取的
module.exports = { module: { rules: [ { test: /\.js$/, use: 'babel-loader?cacheDirectory' }, ], }, };
3.開啟thread-loader
它會把後續的loader放到執行緒池的執行緒中執行,以提高構建效率
由於後續的loader會放到新的執行緒中,所以,後續的loader不能:
- 使用 webpack api 生成檔案 (loader上下文中的emitFile、emitError等api)
- 無法使用自定義的 plugin api (某些外掛提供了自身的plugin和loader,plugin會向webpack注入新的api,loader中會使用)
- 無法訪問 webpack的配置
注意,開啟和管理執行緒需要消耗時間,在小型專案中使用thread-loader
反而會增加構建時間
方案三:熱替換
熱替換並不能降低構建時間(可能還會稍微增加),但可以減少程式碼改動到效果呈現的時間
// webpack配置 module.exports = { devServer:{ hot:true // 開啟HMR } }
// index.js if(module.hot){ // 是否開啟了熱更新 module.hot.accept() // 接受熱更新 }
方案四:動態連結庫
什麼情況下使用?
當打包出來的多個bundle.js檔案都有重複的第三方程式碼,會增加檔案的體積,不利於傳輸
打包的過程:
1.使用output.library配置
公共模組的全域性變數名
// webpack.dll.config.js module.exports = { mode: "production", entry: { jquery: ["jquery"], lodash: ["lodash"] }, output: { filename: "dll/[name].js", library: "[name]"// 每個buldle暴露的全域性變數名 } };
打包結果
// dist/dll/lodash var lodash=function(n){xxx} // dist/dll/jquery var jquery=function(n){xxx}
2.用DllPlugin
建立資源清單(包含資訊:全域性變數名、node_modules對應包的路徑)
// webpack.dll.config.js module.exports = { plugins: [ new webpack.DllPlugin({ path: path.resolve(__dirname, "dll", "[name].manifest.json"), //資源清單的儲存位置 name: "[name]"//資源清單中,暴露的變數名 }) ] };
打包生成的資源清單
// dll/lodash.manifest.json { "name": "lodash", "content": { "./node_modules/lodash/lodash.js": { xxx } } } // dll/jquery.manifest.json { "name": "jquery", "content": { "./node_modules/jquery/dist/jquery.js": { xxx } } }
3.用DllReferencePlugin
使用資源清單
在頁面中手動引入公共模組
<script src="./dll/jquery.js"></script> <script src="./dll/lodash.js"></script>
重新設定clean-webpack-plugin
如果使用了外掛clean-webpack-plugin
,為了避免它把公共模組清除,需要做出以下配置,webpack.config.js配置(注意不是和output.library、DllPlugin在同一個配置檔案中哦):
new CleanWebpackPlugin({ // 要清除的檔案或目錄 // 排除掉dll目錄本身和它裡面的檔案 cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*'] })
使用DllReferencePlugin
new webpack.DllReferencePlugin({ manifest: require("./dll/jquery.manifest.json") }), new webpack.DllReferencePlugin({ manifest: require("./dll/lodash.manifest.json") })
打包過程:首先要根據webpack.dll.config.js配置檔案打包一次,之後再根據webpack.config.js打包
最終打包結果的格式:
(function(modules){ //... })({ // index.js檔案的打包結果並沒有變化 "./src/index.js": function(module, exports, __webpack_require__){ var $ = __webpack_require__("./node_modules/jquery/index.js") var _ = __webpack_require__("./node_modules/lodash/index.js") _.isArray($(".red")); }, // 由於資源清單中存在,jquery的程式碼並不會出現在這裡 "./node_modules/jquery/index.js": function(module, exports, __webpack_require__){ module.exports = jquery; }, // 由於資源清單中存在,lodash的程式碼並不會出現在這裡 "./node_modules/lodash/index.js": function(module, exports, __webpack_require__){ module.exports = lodash; } })
優點:
- 極大提升自身模組的打包速度
- 極大的縮小了自身檔案體積
- 有利於瀏覽器快取第三方庫的公共程式碼
缺點:
- 使用非常繁瑣
- 如果第三方庫中包含重複程式碼,則效果不太理想
方案五:抽離公共程式碼
有多個模組都引用了公共模組,當一個模組載入時,訪問了公共模組,並快取下來,另一個模組載入就可以直接使用快取的結果。
module.exports = { optimization: { splitChunks: { // 分包策略 chunks: "all", cacheGroups: { // 公共模組 common: { mixSize: 0, minChunks: 2, // 至少被幾個檔案引用 }, vendors: { test: /[\\/]node_modules[\\/]/, // 當匹配到相應模組時,將這些模組進行單獨打包 priority: 1 // 快取組優先順序,優先順序越高,該策略越先進行處理,預設值為0 }, } } } }
還可以抽離公共樣式,使用MiniCssExtractPlugin
module.exports = { optimization: { splitChunks: { chunks: "all", cacheGroups: { styles: { test: /\.css$/, // 匹配樣式模組 minSize: 0, // 覆蓋預設的最小尺寸,這裡僅僅是作為測試 minChunks: 2 // 覆蓋預設的最小chunk引用數 } } } }, module: { rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }] }, plugins: [ new CleanWebpackPlugin(), new MiniCssExtractPlugin({ filename: "[name].[hash:5].css", // chunkFilename是配置來自於分割chunk的檔名 chunkFilename: "common.[hash:5].css" }) ] }
通過cdn方式引入js、css檔案,將不怎麼需要更新的第三方庫脫離webpack打包,不被打入bundle中,從而減少打包時間,但又不影響運用第三方庫的方式,例如import方式等。
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin'); module.exports = { plugins: [ new HtmlWebpackExternalsPlugin({ externals: [{ module: 'vue', entry: 'https://lib.baomitu.com/vue/2.6.12/vue.min.js', global: 'Vue' }] }) ], }
最後看到在dist/index.html中動態添加了如下程式碼:
<script type="text/javascript" src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>
方案六:程式碼壓縮
壓縮js和css程式碼:
const TerserPlugin = require('terser-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = { optimization: { // 是否要啟用壓縮,預設情況下,生產環境會自動開啟 minimize: true, minimizer: [ // 壓縮時使用的外掛 // 壓縮js檔案 new TerserPlugin({
parallel: true // 開啟多執行緒壓縮
}), // 壓縮css檔案 new OptimizeCSSAssetsPlugin() ], }, };
使用compression-webpack-plugin
外掛對打包結果進行預壓縮,可以移除伺服器的壓縮時間
new CmpressionWebpackPlugin({ test: /\.js/, minRatio: 0.5 })