Optimization webpack 內部優化配置
webpack配置文件中的 optimization 屬性,用於集中去配置webapck內部的一些優化功能。
Tree Shaking 搖樹
字面意思就是伴隨着搖樹的動作,樹上的枯樹枝和樹葉就會掉落下來。
web開發術語 Tree Shaking 也是相同的道理,它表示「搖掉」代碼中未引用的部分(未引用代碼dead-code)。
MDN:Tree shaking 通常用於描述移除Javascript上下文中的爲引用代碼(dead-code)的行爲。
它依賴於ES6中的import和export語句,用來檢測代碼模塊是否被導出、導入,且被Javascript文件使用。
在webpack中就是將多個JS文件打包爲單個文件時,自動刪除未引用的代碼。以使最終文件具有簡潔的結構和最小化大小。
webpack生產模式中,自動開啓了Tree Shaking這個功能。
示例:
// /src/component.js
export const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
export const Link = () => {
return document.createElement('a')
}
export const Heading = level => {
return document.createElement('h' + level)
}
// /src/index.js
// 只導入一個成員
import { Button } from './component'
document.body.append(Button())
使用生產模式打包後,Tree Shaking的效果就是,只將Button
打包進輸出文件,其他兩個成員由於未使用,而沒有打包到輸出文件。
// /dist/main.js
!(function (e) {
/*...*/
})([
function (e, t, n) {
'use strict'
n.r(t)
// 只有Button
document.body.append(document.createElement('button'))
},
])
手動開啓 Tree Shaking
Tree Shaking並不是 webpack 的某個配置選項。
它是一組功能搭配使用後的優化效果。
這組功能會在生產模式production下自動使用。
webpack官方文檔對 Tree Shaking 介紹有些混亂,這裏學習如何手動開啓它,學習它的工作過程和優化功能。
上例代碼,使用none
模式打包,查看打包文件,components.js
模塊依然保留了Link
和 Heading
。
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// 導出了3個成員
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Button", function() { return Button; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Link", function() { return Link; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Heading", function() { return Heading; });
// components.js的內容全部打包進來了
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
const Link = () => {
return document.createElement('a')
}
const Heading = level => {
return document.createElement('h' + level)
}
})
通過配置optimization中的usedExports和minimize優化功能實現Tree Shaking。
usedExports
usedExports: true
表示在輸出結果中模塊只導出外部使用了的成員。
打包查看輸出文件:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 只導出了Button
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
// Link和Heading被標記未使用
/* unused harmony export Link */
/* unused harmony export Heading */
// components.js的內容全部打包進來了
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
/** 沒有用到的代碼 start **/
const Link = () => {
return document.createElement('a')
}
const Heading = level => {
return document.createElement('h' + level)
}
/** 沒有用到的代碼 end **/
})
此時就可以通過壓縮優化,刪除掉「沒有用到的代碼」。
minimize
minimize: true
開啓代碼壓縮優化。
刪除註釋、刪除沒有用到的代碼、刪除空白、替換變量名爲簡短的名稱等。
它使用的是TerserPlugin或optimization .minimizer中指定的插件。
再次打包:
function (e, t, n) {
'use strict'
/*
壓縮前:
__webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
*/
n.d(t, 'a', function () { return r })
/*
壓縮前:
const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
*/
const r = () => document.createElement('button')
}
總結
webpack打包後,將每個模塊放到一個函數中,其中包含對成員的定義 和 對成員的導出。
usedExports 可以標記模塊導出的成員是否被外部使用,從而在打包結果中,不導出未使用的成員。
標記打包後表現爲:包裹模塊的函數中保留定義這些成員的代碼,但是移除導出它們的代碼,並添加註釋/* unused harmony export */
。
而函數中沒有了導出它們的代碼,也就表示這些成員未使用,那定義它們的代碼也沒有了意義,minimize就會將這些未使用的定義成員的垃圾代碼一併刪除。
- usedExports 負責標記「枯樹葉、枯樹枝」
- minimize 負責「搖掉」它們
concatenateModules 合併模塊
普通的打包結果,是將每個模塊單獨放在一個函數中。
如果模塊很多,打包結果中就會有很多這樣存放模塊的函數。
開啓concatenateModules: true
打包後,打包後的文件中,就不是一個模塊對應一個函數,而是將所有模塊都放在一個函數中。
concatenateModules
的作用就是儘可能的將所有模塊合併輸出到一個函數中。
既提升了運行效率,又減少了代碼的體積。
這個特性又被稱爲「Scope Hoisting」,也就是作用域提升。
它是webpack3增加的特性。
Tree Shaking & Babel
由於webpack早期發展非常快,變化比較多。
當我們找資料時,找到的結果,並不一定適用於當前所使用的版本。
比如 Tree Shaking,很多資料中都表示 如果 使用了 babel-loader,就會導致 Tree Shaking 失效。
在這裏統一說明一下。
首先要了解,Tree Shaking 實現的前提,是基於 「必須用 ES Modules 組織代碼」。
即交給webpack處理的代碼,必須使用 ESM 方式實現模塊化。
MDN:Tree shaking 依賴於ES6中的import和export語句,用來檢測代碼模塊是否被導出、導入,且被Javascript文件使用。
webpack優化的過程是先將代碼交給loader去處理,然後再將處理結果優化輸出。
而爲了轉換代碼中的 ECMAScript 新特性,一般會選擇babel-loader去處理JS。
而在babel-loader處理代碼時,就有可能將代碼中的 ESM 轉換爲 CommonJS。
PS:實際上這取決於是否使用了轉換ESM的babel插件,而常用的插件集合 @babel/preset-env 就包含轉換ESM的插件。
所以當 @babel/preset-env 工作時,代碼中的 ESM 就應該被轉換爲 CommonJS。
所以webpack在打包時,拿到的就是 CommonJS 組織的代碼,從而 Tree Shaking 也就不能生效。
但是現在的效果並不是這樣。
在項目中使用babel-loader,並僅開啓usedExports,查看打包結果。
module.exports = {
mode: 'none',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
optimization: {
usedExports: true,
},
}
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
// usedExports 生效了,也就是Tree Shaking 生效了
/* unused harmony export Link */
/* unused harmony export Heading */
var Button = function Button() {
return document.createElement('button');
console.log('dead-code');
};
var Link = function Link() {
return document.createElement('a');
};
var Heading = function Heading(level) {
return document.createElement('h' + level);
};
})
發現 usedExports 生效了,也就表示 Tree Shaking 並沒有失效。
這是因爲在最新版本的 babel-loader 中自動關閉了 轉換ESM 的插件。
可以查看 node_modules/babel-loader
模塊的源代碼(/lib/injectCaller.js
)。
其中已經通過supportsStaticESM
supportsDynamicImport
標識了支持 ESM 和 動態import 方法(Webpack >= 2 supports ESM and dynamic import.
)。
"use strict";
const babel = require("@babel/core");
module.exports = function injectCaller(opts, target) {
if (!supportsCallerOption()) return opts;
return Object.assign({}, opts, {
caller: Object.assign({
name: "babel-loader",
// Provide plugins with insight into webpack target.
// https://github.com/babel/babel-loader/issues/787
target,
// Webpack >= 2 supports ESM and dynamic import.
supportsStaticESM: true,
supportsDynamicImport: true,
// Webpack 5 supports TLA behind a flag. We enable it by default
// for Babel, and then webpack will throw an error if the experimental
// flag isn't enabled.
supportsTopLevelAwait: true
}, opts.caller)
});
};
// ...
再去查看插件集合源代碼(node_module/@babel/preset-env/lib/index.js
)。
翻到下面的代碼:
const modulesPluginNames = getModulesPluginNames({
modules,
transformations: _moduleTransformations.default,
shouldTransformESM: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsStaticESM)),
shouldTransformDynamicImport: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsDynamicImport)),
shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait)
});
看到preset-env根據babel-loader的標識,自動禁用了 ESM 和 動態import 的轉換。
所以webpack通過babel-loader轉換後,打包時還是ESM組織的代碼,Tree Shaking也就能正常工作。
可以通過修改插件集合的配置,開啓 ESM 轉換:
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
// 注意:僅使用時,是將插件集合名稱放到一個數組中
// presets: ['@babel/preset-env'],
// 當對插件集合編寫配置時,就需要再套一個數組
// 數組的第一個元素是插件集合的名稱
// 第二個元素是它的配置對象
presets: [
[
'@babel/preset-env',
{
// modules默認是auto,即根據環境去判斷是否開啓轉換ESM插件
// 這裏設置爲強制轉換爲commonjs
modules: 'commonjs',
},
],
],
},
},
},
],
},
再次打包查看:
(function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Heading = exports.Link = exports.Button = void 0;
var Button = function Button() {
return document.createElement('button');
console.log('dead-code');
};
exports.Button = Button;
var Link = function Link() {
return document.createElement('a');
};
exports.Link = Link;
var Heading = function Heading(level) {
return document.createElement('h' + level);
};
exports.Heading = Heading;
})
3個成員都被導出,從而壓縮優化時,也不會將它們刪除。
總結
通過以上實驗發現,最新版本的babel-loader,並不會導致Tree Shaking失效。
如果還不確定,也可以嘗試將preset-env配置中的modules
,設置爲false
。
這樣就會確保,preset-env不會開啓 轉換ESM 的插件。
同時確保了Tree Shaking工作的前提。