以前寫的《webpack入門必備(一):基礎配置》主要介紹了webpack基礎解析所需的loader/plugin。而隨着平常webpack的使用,咱們會更多關注如何構建更快、構建產物更小、構建產物符合規範...但願這篇文章可讓你找到答案。
這裏介紹的主要的幾種優化配置以下所示:javascript
縮小構建範圍css
多進程html
緩存前端
若是你有沒用過的配置能夠接着看下面的具體使用方法,若是你已經很熟悉了則能夠跳過此節~java
配置來確保轉譯儘量少的文件(exclude 的優先級高於 include)
const rootDir = process.cwd(); { test: /\.(j|t)sx?$/, include: [path.resolve(rootDir, 'src')], exclude: [ /(.|_)min\.js$/ ], }
PS. 相比exclude能夠多用includenode
若是一些庫不依賴其它庫的庫,不須要解析他們,能夠引入來加快編譯速度。
noParse: /node_modules\/(moment|chart\.js)/
忽略第三方包指定目錄。 (他是webpack 內置的插件)
例如: moment (2.24.0版本) 會將全部本地化內容和核心功能一塊兒打包,咱們就可使用 IgnorePlugin 在打包時忽略本地化內容(語言包),見下圖。react
plugins: [ // 表示忽略moment下的locale文件夾內容 new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) ]
把 thread-loader 放置在其它 loader 以前,那麼它以後的 loader 就會在一個單獨的 worker 池中運行。
// 項目中babel-loader通常耗時比較長,因此能夠配置thread-loader rules: [ { test: /\.jsx?$/, use: ['thread-loader', 'cache-loader', 'babel-loader'] } ]
運行在Node.js上的webpack是單線程,將文件解析的任務拆分由多個子進程併發進行,而後子進程處理完任務後再將結果發送給主進程,提高項目構件速度。
(可是由於進程的分配和管理也須要時間,因此使用後不必定快,須要項目接入實驗一下)
const Happypack = require("happypack"); module.exports = { module: { rules: [ { test: /\.js[x]?$/, use: "Happypack/loader?id=js", include: [path.resolve(__dirname, "src")], }, { test: /\.css$/, use: "Happypack/loader?id=css", include: [ path.resolve(__dirname, "src"), path.resolve(__dirname, "node_modules", "bootstrap", "dist"), ], }, { test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/, use: "Happypack/loader?id=file", include: [ path.resolve(__dirname, "src"), path.resolve(__dirname, "public"), path.resolve(__dirname, "node_modules", "bootstrap", "dist"), ], }, ], }, plugins: [ new Happypack({ id: "js", //和rule中的id=js對應 //將以前 rule 中的 loader 在此配置 use: ["babel-loader"], //必須是數組 }), new Happypack({ id: "css", //和rule中的id=css對應 use: ["style-loader", "css-loader", "postcss-loader"], }), new Happypack({ id: "file", //和rule中的id=file對應 use: [ { loader: "url-loader", options: { limit: 10240, //10K }, }, ], }), ], };
在性能開銷較大的loader處使用,將構建結果緩存中磁盤中。
(默認存在node_modueles/.cache/cache-loader目錄下。 )
cacheDirectory例子:webpack
rules: [ { test: /\.(j|t)sx?$/, use: [ { loader: 'babel-loader', options: { cacheDirectory: true, }, } } ]
cache-loader例子:es6
rules: [ { test: /\.(css)$/, use: [ { loader: 'style-loader' }, { loader: 'cache-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' } ] } ]
將複用性較高的第三方模塊打包到DLL中,再次構建時直接複用,這樣只需從新打包業務代碼。
(注意是DLL緩存是大大縮短了首次構建時間
,像以前的cache-loader優化都是縮短rebuild時間
)
使用相關插件:web
具體步驟:
(1) 新增一個webpack配置去編譯DLL文件([name].dll.js
、[name].manifest.json
)
// 新增一個webpack-dll.config.js配置文件 const path = require('path'); const DllPlugin = require('webpack/lib/DllPlugin'); const distPath = path.resolve(__dirname, 'dll'); module.exports = { entry: { // 把 React 相關模塊的放到一個單獨的動態連接庫 react: ['react', 'react-dom'], // 把項目須要全部的 polyfill 放到一個單獨的動態連接庫 polyfill: [ 'core-js/fn/object/assign', 'core-js/fn/object/entries', ... ], }, output: { // 輸出的動態連接庫的文件名稱,[name] 表明當前動態連接庫的名稱(react 和 polyfill) filename: '[name].dll.js', path: distPath, // 存放動態連接庫的全局變量名稱,例如對應 react 來講就是 _dll_react // 之因此在前面加上 _dll_ 是爲了防止全局變量衝突 library: '_dll_[name]', }, plugins: [ // 接入 DllPlugin new DllPlugin({ // 動態連接庫的全局變量名稱,須要和 output.library 中保持一致 // 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值(_dll_react) name: '_dll_[name]', context: process.cwd(), // 描述動態連接庫的 manifest.json 文件輸出時的文件名稱 path: path.join(__dirname, 'dll', '[name].manifest.json'), }), ], };
// package.json裏新增dll的構建命令 "scripts": { "dll": "webpack --config webpack-dll.config.js", }
(2) dev構建時,告訴 Webpack 使用了哪些動態連接庫
// webpack.config.js文件 const DllReferencePlugin = require('webpack/lib/DllReferencePlugin'); plugins: [ // 使用的動態連接庫(react和polyfill的) new DllReferencePlugin({ context: process.cwd(), manifest: path.join(rootDir, 'dll', 'react.manifest.json'), }), new DllReferencePlugin({ context: process.cwd(), manifest: path.join(rootDir, 'dll', 'polyfill.manifest.json'), }), ... ]
(3) html template裏引入文件
由於我這裏只是本地構建加速,因此就以dev的方式引入
<script src="./dll/polyfill.dll.js?_dev"></script> <script src="./dll/react.dll.js?_dev"></script>
到這DLL就配好了。有些人可能比較好奇react.dll.js
和react.manifast.js
究竟是什麼文件,作了什麼事?你看看他兩個文件就知道啦~
react.dll.js
其實主要就是所引用模塊的代碼集合react.manifast.js
則寫明包含哪些模塊、模塊路徑// react.dll.js文件部份內容以下所示。 var _dll_react = (function(modules) { // ... 此處省略 webpackBootstrap 函數代碼 }([ function(module, exports, __webpack_require__) { // 模塊 ID 爲 0 的模塊對應的代碼 }, function(module, exports, __webpack_require__) { // 模塊 ID 爲 1 的模塊對應的代碼 }, // ... 此處省略剩下的模塊對應的代碼 ])); // react.manifast.js文件部份內容以下所示。 { // 描述該動態連接庫文件暴露在全局的變量名稱 "name": "_dll_react", "content": { "./node_modules/process/browser.js": { "id": 0, "meta": {} }, // ... 此處省略部分模塊 "./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": { "id": 42, "meta": {} }, ... }
經常使用工具:speed-measure-webpack-plugin
使用方法:用其來包裹 Webpack 的配置
這裏介紹的主要的幾種優化配置以下所示:
@babel/plugin-transform-runtime
具體使用:
將業務代碼和第三方依賴庫進行分包,減少index.js的大小;
抽離多頁應用的公共模塊,單獨打包。公共代碼只須要下載一次就緩存起來了,避免了重複下載。
optimization: { minimize: false, moduleIds: 'named', splitChunks: { chunks: 'all', minSize: 30000, maxSize: 0, minChunks: 1, maxAsyncRequests: 6, maxInitialRequests: 6, automaticNameDelimiter: '~', name: true, cacheGroups: { polyfill: { test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/, name: 'polyfill', priority: 70, minChunks: 1, reuseExistingChunk: true }, lib: { test: /[\\/]node_modules[\\/]/, name: 'lib', chunks: 'initial', priority: 3, minChunks: 1, }, ... } } }
提取全部頁面所需的helper函數到一個包裏,避免重複注入
"plugins": [ "@babel/plugin-transform-runtime" ... ]
若是使用ES6的import 語法,那麼在生產環境下,會自動移除沒有使用到的代碼。
(1) 具體配置
const TerserPlugin = require('terser-webpack-plugin'); const config = { // 生產模式下tree-shaking才生效 mode: 'production', optimization: { // Webpack 將識別出它認爲沒有被使用的代碼,並在最初的打包步驟中給它作標記。 usedExports: true, minimizer: [ // 刪除死代碼的壓縮器 new TerserPlugin({...}) ] } };
(2) 哪類代碼會被shake掉?如下有一些事例
// no tree-shaking import Stuff from './stuff'; doSomething(Stuff); // tree-shaking import Stuff from './stuff'; doSomething(); // tree-shaking import './stuff'; doSomething(); // no tree-shaking import 'my-lib'; doSomething(); // 所有導入 no tree-shaking import _ from 'lodash'; // 具名導入 tree-shaking import { debounce } from 'lodash'; // 直接導入具體的模塊 tree-shaking import debounce from 'lodash/lib/debounce';
(3) 什麼叫有反作用的代碼?
`只要被引入,就會對應用程序產生重要的影響。
(一個很好的例子就是全局樣式表,或者設置全局配置的js文件。)`
(4) 有反作用的代碼咱們不但願被shake,咱們能夠配置以下
// 全部文件都有反作用,全都不可 tree-shaking { "sideEffects": true } // 沒有文件有反作用,全均可以 tree-shaking { "sideEffects": false } // 只有這些文件有反作用,全部其餘文件均可以 tree-shaking,但會保留這些文件 { "sideEffects": [ "./src/file1.js", "./src/file2.js" ] }
(5) 注意,babel配置須要配modules: false
,忽略import/export代碼編譯
const config = { presets: [ [ '@babel/preset-env', { // commonjs代碼不能被tree-shaking // 因此babel保留咱們現有的 es2015 import/export 語句,不進行編譯 modules: false } ] ] };
經常使用工具:webpack-bundle-analyzer
使用方法:用其來包裹 Webpack 的配置
生產環境構建時,會檢查構建產物裏是否存在es6語法。有則拋出錯誤並提示你去進行babel編譯,這樣避免了構建產物不合要求的狀況。
具體使用例子:
// package.json 命令里加上es-check檢查 "dist:basic": "rimraf public && cross-env NODE_ENV=production webpack --config webpack-dist.config.js && es-check es5 ./public/**/*.js"
編譯緩存就是在首次編譯後把結果緩存起來,在後續編譯時複用緩存,從而達到加速編譯的效果。
webpack5默認開啓編譯緩存,緩存默認是在內存裏,你能夠自定義。
module.exports = { cache: { // 將緩存類型設置爲文件系統 type: "filesystem", // 緩存的位置(默認是node_modules/.cache/webpack) cacheDirectory: path.resolve(__dirname, '.temp_cache'), // 指定構建過程當中的代碼依賴。webpack將使用這些項目以及全部依賴項的哈希值來使文件系統緩存無效。 buildDependencies: { // 當配置文件內容或配置文件依賴的模塊文件發生變化時,當前的構建緩存即失效。 config: [__filename], // webpack.config、loader和全部從你的配置中require的模塊都會被自動添加。若是有其餘的東西被構建依賴,你能夠在這裏添加它們 }, // 指定緩存的版本。當須要更新配置緩存時,經過設置此版本使緩存失效。 version: '1.0' } }
一些參數註解
cache: true
就是 cache: { type: 'memory' }
的別名type
: 'filesystem'|'memory'。若是設置'memory'則緩存在內存且不能配置其餘信息,設置成'filesystem'就能夠配置更多信息。默認開發模式使用的是'memory',生產模式是false。
version
: 當配置文件和代碼都沒有發生變化,可是構建的外部依賴(如環境變量)發生變化時,預期的構建產物代碼也可能不一樣。這時就可使用 version 配置來防止在外部依賴不一樣的狀況下混用了相同的緩存。例如,能夠傳入 cache: {version: process.env.NODE_ENV},達到當不一樣環境切換時彼此不共用緩存的效果。長效緩存指的是能充分利用瀏覽器緩存,儘可能減小因爲模塊變動致使的構建文件hash值的改變,從而致使文件緩存失效。
(因爲moduleId和chunkId肯定了,構建的文件的hash值也會肯定。)
chunk、module都是什麼?
好比只是jsx刪除引用的一個css文件 好多bundle文件的hash就都變了。)
以前須要經過以下配置達到長效緩存:
plugins: [ - new webpack.NamedModulesPlugin(), + new webpack.HashedModuleIdsPlugin(),
或者配置
optimization.moduleIds = 'hashed’ optimization.chunkIds = 'named'
配置說明:
(NamedChunksPlugin 只能對普通的 Webpack 模塊起做用,異步模塊(異步模塊能夠在 import 的時候加上 chunkName 的註釋,好比這樣:import(/ webpackChunkName: 「lodash」 / ‘lodash’).then() 這樣就有 Name 了),external 模塊是不會起做用的。)
Webpack5採用新的算法,生產模式下默認啓用以下配置不只實現長效緩存,還減小了文件打包大小:
optimization.chunkIds: "deterministic" optimization.moduleIds: "deterministic" mangleExports: 「deterministic"
PS.具體採用的算法還須要進一步深刻研究~
Webpack 4版本附帶了大多數Node.js核心模塊的polyfill,一旦前端使用了任何核心模塊,這些模塊就會自動應用,致使polyfill文件很大,可是其實有些polyfill是沒必要要的。
而如今webpack5將不會自動爲Node.js模塊添加Polyfills,須要開發者手動添加合適的Polyfills。
升級遷移至webpack5須要注意:
1.嵌套tree-shaking
可以跟蹤對export的嵌套屬性的訪問,分析模塊的export和import的依賴關係,去掉未被使用的模塊
// inner.js export const a = 1; export const b = 2; // module.js export * as inner from './inner'; // or import * as inner from './inner'; export { inner }; // user.js import * as module from './module'; console.log(module.inner.a); // 在此示例中,能夠在生產模式下移除導出 b。
2.內部模塊tree-shaking(深度做用域分析)
新屬性optimization.innerGraph分析模塊導出和導入之間的依賴關係,在生產模式下默認啓用。
import { something } from './something'; function usingSomething() { return something; } export function test() { return usingSomething(); } // 在使用 test 導出時才使用 something。
能夠分析如下符號:
3.package.json 中的「sideEffects」標誌容許將模塊手動標記爲無反作用,從而在不使用它們時將其移除。
webpack 5 還能夠根據對源代碼的靜態分析,自動將模塊標記爲無反作用。
更多Webpack5的內容推薦閱讀: