最近給項目進行webpack優化,嘗試過幾乎所有方法,一共26條,列舉在此。
優化webpack,首先明確優化目標:
- 構建速度: 開發環境項目的啟動速度,以及生產環境項目的打包速度。構建時間越短,速度越快越好。
- 增量構建速度:開發項目時,每修改一次代碼,webpack會對修改的部分進重新構建。增量構建時間越短,速度越快越好。
- 打包體積:開發環境webpack打包后,生成的文件體積。打包體積越小越好
- 加載體積: 除關注打包體積外,瀏覽器打開頁面,加載資源的體積也很重要。按需加載、緩存等技術可以減少加載體積。加載體積越小越好。
減少依賴包,縮小打包體積,構建速度會更快。使用壓縮,打包體積會減小,但構建速度則變慢。討論webpack的優化,速度和體積需同時考慮,二者有時正相關,有時負相關,需要均衡速度和體積。
下文中每個優化建議,都會列出優化目標,影響明顯的會加粗。除速度和體積,還有: 分析, 即幫助分析webpack打包性能,呈現打包時間等信息;錯誤追蹤,webpack打包后的文件與源文件不同,需要特殊的處理以方便錯誤定位和調試。
webpack 在生產環境更關注打包體積,而開發環境更關注打包時間,生產環境和開發環境使用不同的策略,下文每一個優化建議都列出使用環境。
webpack及相關打包工具在不斷跟新優化中,不同版本可能會有很大不同,請以官方文檔為準。本文以webpack4.42為準,與webpack3和webpack5區別較大的地方會標注版本變動。
1. 量化打包速度
環境:開發,生產
目標:分析,構建速度,增量構建速度
對webpack進行優化,首先要測量webpack打包耗時,發現問題所在。
- 使用
speed-measure-webpack-plugin
,快速測量各插件和loader的耗時。
speed-measure-webpack-plugin使用非常簡單,直接包裹webpack配置即可
// npm i -D speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
const webpackConfig = smp.wrap({
// ... webpack config
})
speed-measure-webpack-plugin對webpack 4的兼容性不好,一些插件可能會發生錯誤,需暫停發生錯誤的插件。
-
使用
profilingPlugin
, 創建構建性能報告。profilingPlugin
可以生成一份JSON文件,上傳到chrome devtool的performence面板, 可以看到詳細的編譯過程,尋找性能瓶頸:const webpack = require('webpack') module.exports = { //... plugins: [ new webpack.debug.ProfilingPlugin( { // 產生出的文件位置,相對于根目錄 outputPath: 'stats/profileEvents.json' }) ] }
上傳到chrome,出現漂亮的時間線,可供分析:
perfomrance.png
webpack多進程運行時,插件和loader的運行時間較難觀察,比如各插件的運行時間相加會大于總時間,loader的先后順序可能不對。使用單進程可以保證有效的測試數據。測速時請將 parallelism
配置項設為 false
(默認為 true
),確保webpack單線程運行:
module.exports = {
//...
parallelism: false
}
除調試webpack外,其他情況請開啟多進程。
2. 量化打包體積
環境:開發,生產
目標:分析,打包體積,加載體積
可以通過 webpack-bundle-analyzer
查看打包后的體積。探究某個依賴是否過大,是否存在重復的依賴,是否有功能類似的依賴,是否進行有效的代碼分離。
// npm i -D webpack-bundle-analyzer
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
//...
plugins: [
new BundleAnalyzerPlugin({
defaultSizes: 'gzip' // 設置默認尺寸為'gzip'
})
)
]
}
webpack-bundle-analyzer
顯示的尺寸有三種:
-
stat
: 未經Uglify、Terser 等插件最小化的體積。最小化js就是我們常說的壓縮js,最小化(minify)是指去除注釋、簡化變量名等操作壓縮體積,而壓縮是通過gzip等算法壓縮體積。 -
parsed
: 經Uglify、Terser 等插件最小化后的體積。 -
gzip
: 經過gzip壓縮后的尺寸。
線上一般按照 gzip
進行數據傳輸,所以選擇 gzip
作為衡量體積的標準。
3. 查看打包詳情
環境:開發,生產
目標:分析,構建速度,增量構建速度,打包體積,加載體積
除查看打包體積和時間,有時需要更多打包信息,比如模塊的依賴關系,可以用如下方式:
-
配置
profile
選項,生成詳細打包報告。配置profile
選項后,每次打包會生成一個json文件,里面有打包的詳細信息,也可通過stats-webpack-plugin
進行更多設置:// npm i -D stats-webpack-plugin const StatsPlugin = require('stats-webpack-plugin') module.exports = { //... profile: true // 簡單的配置,將在打包文件輸出報告 pulgins: [ // 更靈活的配置 new StatsPlugin( '../../stats/stats.json', { exclude: [/node_modules/], }) ] }
將產生的json文件上傳至https://webpack.github.io/analyse/ 官方分析工具,查看可讀的報告:
-
打包后,在terminal終端中顯示打包詳情。可以通過
stats
選項配置terminal中的輸出結果,如:module.exports = { //... stats: 'verbose' // 將在terminal中呈現全部輸出 };
每次打包后,在終端會顯示:
`stats` 的可選址值有:
* `none`: 沒有輸出
* `errors-only` :只顯示錯誤信息,**使用此選項,可提高webpack編譯速度**
* `minimal`: 顯示錯誤信息,或者新的輸出
* `normal` : 標準輸出
* `verbose` : 全部輸出,建議用 `profile` 選項輸出成文檔。
可以個性化配置輸出信息。使用Node.js API 時此配置無效,使用 `webpack-dev-server` 時,請將此選項需放入 `devServer` 中。。
Jarvis、webpack-dashboard、webpack visualizer等非官方工具也可提供打包信息,幫你分析webpack編譯。
4. 使用最新版本
環境:開發,生產
目標: 構建速度,增量構建速度,打包體積,加載體積
工欲善其事,必先利其器。使用最新的版本,保證最快的速度。請檢查node、 webpack、插件、 loader等的版本號,使用最新版本。推薦一款工具 npm-check
, 只要在項目根目錄運行:
npm-check -u
即查看哪些包有更新:
5. 刪除不必要的插件、loader等工具
環境:開發,生產
目標:構建速度,增量構建速度
任何插件和loader的啟用都占用時間,使用最少的插件和loader可以減少webpack的打包時間。
- 一些工具,只對生產階段有效,比如壓縮、CSS分離、hash,請在開發環境刪除。
- 一些工具,比如
progress-bar-webpack-plugin
進度提示條,請衡量對你是否真的有用,每增加一個插件都會讓打包更慢。 - 對于
speed-measure-webpack-plugin
,webpack-bundle-analyzer
這類量化插件,只在調試webpack時使用,在其他時候請刪除。 - 在webpack打包時,會在terminal中輸出moudle、chunk等打包信息,生成這些信息會消耗一定的時間。建議設置為
stats: 'errors-only'
或者stats: 'minimal'
。
6. 設置正確的模式mode
環境:開發,生產
目標: 構建速度,增量構建速度,構建體積,加載體積
版本變動:webpack3
webpack提供開箱即用的配置環境,通過 mode
屬性告知webpack你的使用環境,webpack將自動開啟一系列的優化。
- 生產環境,請設置
mode:'production'
- 開發環境,請設置
mode: 'development'
module.exports = {
//...
mode: 'production' // 生產環境,也是默認值
mode: 'development' // 開發環境
}
設置 mode
后,webpack自動將 process.env.NODE_ENV
設為相應的值(生產為 production
, 開發為 develpment
),react等包根據此值進行優化,所以不要將process.env.NODE_ENV
改為其他值。
webpack3沒有 mode
選項,需要用 DefinePlugin
手動設置process.env.NODE_ENV
,明確生產或者開發環境 。
webpack自動啟用的優化,如無需配置,本文不再介紹。
7. 啟用多進程,并行編譯
環境:開發,生產
目標: 構建速度,增量構建速度
webpack默認是單進程編譯,不利于發揮多核CPU的威力。可以采取些措施,使用多進程,這將大幅提高打包速度:
對于
TerserWebpackPlugin
等插件,本身支持多進程,默認開啟,也可通過parallel
選項進行個性化配置。-
對于其他loader,可以使用
happyPack
或者thread-loader
進行并行編譯。這兩個功能類似,對打包速度的提升也類似。對于babel-loader
尤其需要并行編譯,提高打包速度。happyPack
使用的人較多,使用限制較少, 但其配置較復雜,例如:// npm i -D happypack const HappyPack = require('happypack') module.exports = { // ... module: { loaders: [{ test: /\\.js$/, loader: 'babel-loader', happy: { id: 'js' } }] }, plugins: [ new HappyPack({ id: 'js' }) ] }
thread-loader
有些使用限制,但配置非常簡單:// npm i -D threa-loader module.exports = { // ... module: { rules: [ { test: /\\.jsx?$/, use: ['thread-loader', 'babel-loader'] } ] } }
thread-loader
將其后的loader 放在單獨的池中運行,要求loader不能產生新的文件、不能使用插件、無法獲取webpack選項。
開啟多進程會消耗大量時間,各進程之間的通信也非常耗時。請僅對長耗時的loader啟用。進程數過多會導致打包變慢,進程的默認數通常是CPU數-1,即 os.cpus().length - 1
。
8. 使用持久緩存
環境:開發,生產
目標:構建速度,增量構建速度
版本變動:webpack5
webpack構建一次耗費很長時間,如果將構建的結果進行緩存,第一次構建的時間可能略有增加,但之后的構建時間將大幅度縮短,這種技術叫持久緩存。
-
建議使用
hardSourcePlugin
進行全局緩存,開箱即用,無需配置,使用效果非常好,能減少50%以上的構建時間。hardSourcePlugin
在使用中可能存在一些問題,可能導致CSS分離失效,如有問題請查閱官方文檔。使用hardSourcePlugin
后,不需再使用cache-loader
或者DLL plugin
等緩存工具。hardSourcePlugin
配置示例:// npm i -D hard-source-webpack-plugin const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') module.exports = { // ... plugins: [ new HardSourceWebpackPlugin () ] }
對于
TerserWebpackPlugin
,請確保緩存開啟,即cache
不為false
。hardSourcePlugin
對TaresePlugin似乎沒有效果,需單獨處理。對于webpack5,請直接配置
cache: { type: "filesystem” }
,不再需要hardSourcePlugin
等緩存工具,TarserPlugin
自帶的緩存特性也會失效,無需開啟。
另外一種緩存技術叫做”長期緩存" (long-term caching), 是指瀏覽器緩存一些資源,可以更快的打開頁面,需與“持久緩存”相區別,后文有更多介紹。
9. 選擇合適的devTool
環境:開發,生產
目標:構建速度、增量構建速度
webpack打包后的代碼是經過壓縮處理,缺少可讀性。devTool可以控制代碼的輸出品質,添加 source-map
,增加代碼可讀性,方便錯誤定位。注意代碼輸出品質的提高,會使打包速度變慢,需均衡選擇。
module.exports = {
//...
devTool: 'source-map' // 產生源碼品質的source-map,有暴露源碼風險,不建議使用
}
生產環境常用的值:
-
none
:不使用devtool,速度最快,無法定位錯誤,是生成環境默認值。 -
hidden-source-map
:產生source-map
, 需要手動加載到瀏覽器devtool,或使用其他工具加載,才可查看源碼和追蹤錯誤,推薦使用。 -
nosources-source-map
:無法看到源碼,但能在瀏覽器devtoo看到錯誤棧,錯誤發生的準確位置、行號等信息。這是線上環境最簡單的方案,但暴露源碼結構。
開發環境常用的值:
-
eval
: 源碼被放入eval
函數中,缺少可讀性,只知道錯誤的大概位置,沒有錯誤行號、列號提示,構建速度最快,這是開發環境默認值。 -
eval-cheap-source-map
: 看到近似源碼的代碼,有錯誤行號提示,沒列號提示。較快的構建速度和增量構建速度。 -
eval-cheap-module-source-map
: 能看到源碼,有行號錯誤提示,沒列號提示。較慢的構建速度和較快的增量構建速度,推薦此選項。 -
eval-source-map
: 能看到源碼,并有行號和列號的錯誤提示。很慢的構建速度和一般的增量構建速度。
devTool
還有其他配置值,需均衡考慮構建速度和錯誤定位,并防止源碼在生產環境中暴露。你可以用 SourceMapDevToolPlugin
代替 devTool
進行個性化配置。
如未能生成 source-map
,檢查一些插件或者loader是否配置 sourceMap: true
,這包括postcss-loader、resolve-url-loader、TeserWebpackPlugin、OptimizeCSSAssetsPlugin 等。
10. 合理配置loader的解析范圍
環境:開發,生產
目標:構建速度,增量構建速度
一些loader,比如 babel-loader
轉換JS的過程是非常耗時的,如果能減少loader處理的文件數量,會大大減少編譯時間。
可以通過 test
正則匹配文件, include
指定要包含的文件, exclude
指定要排除的文件。優先級為: exclude
> include
> test
,例如:
module.exports = {
//...
module: {
rules: [
{
test: /\\.styl$/,
include: [
path.resolve(__dirname, 'app/styles'),
path.resolve(__dirname, 'vendor/styles')
],
// 在app/styles 中排除掉 app/styles/common
exclude: [path.resolve(__dirname, 'app/styles/commen')],
use: ['style-loader', 'css-loader', 'stylus-loader']
}
]
}
}
順便提下loader的執行順序,列表中較后的選項先執行,比如在上例中,先執行 stylus-loader
解析.style,再執行 css-loader
解析CSS,最后執行 style-loader
將CSS內聯處理。
11. 忽略jquery等模塊的解析
環境:開發,生產
目標:構建速度、增量構建速度
jquery, lodash等是獨立的、不需要外部依賴即可運行的模塊。可以使用 module.noParse
避免對這些庫進行解析,以提高構建性能。配置方法如下:
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
}
};
module.noParse
配置后,將忽略相關模塊中 import
、 reuqire
、 define
的調用,請確保配置的模塊沒有其他依賴。
module.noParse
只是不對jquery等模塊進行解析(如通過babel-loader轉換js文件),打包后的文件中仍包含jquery的代碼。 module.noParse
的優先級高于上文中對loader的include
,test
配置。
12. 使用 externals
引入公共CDN
環境:開發,生產
目標:構建速度、增量構建速度
可以利用公共CDN服務加載包,而不用webpack打包。如下配置,將 jquery
變量掛載在 windows
變量上:
module.exports = {
//...
externals: {
'jquery': 'jQuery'
}
};
當然,要在HTML代碼中引入jquery:
<script src="<http://libs.baidu.com/jquery/2.0.0/jquery.min.js>"></script>
webpack打包后的文件中將不包含jquery的代碼。
如果你的多個應用使用相同的依賴,可以將公共依賴分離,再用此種方式單獨部署使用。
13. 合理配置 reslove
字段,縮小webpack尋找模塊的范圍
環境: 開發、生產
目標:構建速度、增量構建速度
resolve
配置webpack如何查找模塊,例如 import 'vue'
,resolve
告訴webpack在哪里找到vue。更精準的配置將減小webpack搜索模塊的時間。
module.exports = {
//...
resolve: {
modules: ['node_modules'], // 僅在node_modules目錄尋找模塊
extensions: ['.js', '.json'], // import 'utils' 類似于 import 'utils.js'
mainFields: ['loader', 'main'], // 通過package.json 的loader、main 選項找到入口文件名
mainFiles: ['index'], // 設index為入口文件名
symlinks: false, // 忽略npm link
alias: {
// import 'Pages' 將等同于 import 'src/pages'
Pages: 'src/pages',
// webpack直接確定react模塊位置,不需要層層查找
react: patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
}
};
-
resolve.modules
:告訴webpack搜索模塊的范圍。 -
resolve.mainFields
和resolve.mainFiles
:告訴webpack模塊的入口文件。 -
resolve.extensions
:如果引入文件時不指定文件類型后綴,將通過此字段查找相應的文件類型。 -
resolve.symlinks
:如果不使用NPM link
,可將此設為false
。 -
resolve.alias
:設置別名,這將提高代碼的可讀性,并加快模塊的尋找速度。
如果選項是列表的,請盡量減少列表項,并將高頻值寫在前面, 如 extensions: ['.js', '.json']
中 js
在前。
未掌握 reslove
各選項含義前,請使用默認值, reslove
對webpack編譯的提速不明顯,但錯誤的配置可能導致一些bug,比如明明存在某個模塊但webpack無法找到該模塊。
14. 使用ES module,保證tree-shaking刪除無用代碼
環境:開發,生產
目標:構建體積
版本變動:webpack3
webpack4支持tree-shaking,可以刪除未使用的代碼,請使用ES module的導入方式,確保tree-shaking的有效,例如:
// common.js
const run = () => {console.log('run')}
const fly = () => {console.log('fly')}
export {run, fly}
// index.js
import {run, fly} from './common'
run()
如此,fly將會從代碼中刪除。按下述導入方式,tree-shaking不會生效:
// common.js 同上
// index.js
import common from './common'
common.run()
// common 依賴將被整個打包,無法實現tree-shaking
由于JS動態編譯的特性,tree-shaking的效果有限。對于很多依賴,webpack無法很好的執行tree-shaking。另外webpack3不支持tree-shaking。
在開發其他項目的依賴包時,可考慮在 package.js
中添加 sideEffects:false
,其他項目在導入你的包時,會嘗試進行tree-shaking。
15. 使用TerserWebpackPlugin
最小化JS
環境:生產
目標:構建速度,打包體積
TerserWebpackPlugin
可以最小化JS,即壓縮js,并且支持最小化ES6,webpack4生產模式下自動啟用,你也可以進行一些配置:
- 請確保
parallel
多進程開啟,提高打包速度。 - 像loader一樣,通過
test
,include
,exclude
縮小壓縮文件范圍。也可以用chunkFilter
排除一些chunk。 - 非webpack5版本,請確保
cache
緩存開啟,webpack5中此選項失效。 - 如果需要source-map,請配置
sourceMap
為true
,這將生成source-map。注意cheap-source-map
等包含cheap
的source-map 不會產生source-map。請使用hidden-source-map
或者nosources-source-map
。
配置示例如下:
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
exclude: /\\/excludes/,
chunkFilter: (chunk) => {
return chunk.name !== 'vendor' // 排除名為 `vendor` 的chunk
},
parallel: true, // 多進程,默認開啟
cache: true, // 持久緩存,默認開啟
sourceMap: true, // 支持產生source-map,默認不開啟
terserOptions: {
//... 設置terser的壓縮選項,比如是否清除注釋,詳見官方文檔
}
}),
],
},
};
不要使用 UglifyjsWebpackPlugin
,此插件不支持ES6,并已經停止維護。
16. 使用 optimize-css-assets-webpack-plugin
最小化CSS
環境:生產
目標:打包體積
使用 optimize-css-assets-webpack-plugin
最小化CSS,減少CSS代碼的體積。webpack4 中不包含此插件,需先安裝。
// npm i -D optimize-css-assets-webpack-plugin
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({})
],
},
};
17. 優化圖片
環境:生產
目標: 打包體積
圖片是網站重要的一部分,不僅體積大,而且數量多,占用大量帶寬,下面是優化圖片的一些方法:
- 使用
url-loader
將小尺寸圖片進行內聯。可以配置limit
選項,將小于此值的圖片轉換成base64 格式并存在JS代碼中,以節少請求數量。file-loader
有和url-loader
類似的功能,可以解析靜態文件,但不能設置limit
屬性進行內聯。 - 使用
svg-loader
將svg圖片進行內聯。此插件和url-loader
類似,但SVG是文本,壓縮體積上更有效率。 - 雪碧圖可將多張小圖片組裝合成一張大圖片,減少請求數量。由于雪碧圖配置較復雜,不推薦使用。可以利用
webpack-spritesmith
插件實現雪碧圖。 - 使用
image-webpack-loader
壓縮圖片。它支持JPG,PNG,GIF和SVG等幾乎所有格式。 它不會將圖片內聯,需與url-loader
等配合使用,即先壓縮圖片,再進行內聯。可以通過enforce: 'pre'
確保在其他loader前執行。
配置示例如下:
// npm i -D image-webpack-loader
// npm i -D url-loader
// npm i -D svg-url-loader
module.exports = {
module: {
rules: [
// 壓縮圖片
{
test: /\\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
enforce: 'pre', // 在其他loader調用前,先行調用,避免重復代碼
},
// 內聯圖片
{
test: /\\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
limit: 10 * 1024, // 小于10kb的會被內聯
},
}
// 內聯svg
{
test: /\\.svg$/,
loader: 'svg-url-loader',
options: {
limit: 10 * 1024, // 小于10kb的會被內聯
noquotes: true, // 刪除引號
iesafe: true, // 支持IE,但會增加體積
},
},
],
},
};
18. 優化第三方依賴包
環境: 開發,生產
目標: 構建速度、增量構建速度、打包體積
幾乎一半以上的JS體積來源于第三方依賴包,如果能優化依賴包體積,會大幅減少構建體積。我們通常只用到包的幾個方法,但卻引入包的全部內容。比如,我們只在中文環境使用Moment.js,卻引入了Moment.js的各國語言包,使得項目臃腫。
Googel 在Github repo上收集了一些優化建議,其中包括babel-loard、loadsh、react、bootstrap等的優化建議。請在github上搜索“webpack-libs-optimizations”查看。
19. 對html-webpack-plugin的優化
環境:生產
目標:打包體積
hteml-webpack-plguin
可以很方便的創建入口html文件,但它對自定義模板默認不開啟壓縮的,請配置 minify
選項,進行最小化代碼
// npm i -D html-webpack-plugin
// npm i -D uglifyjs-webpack-plugin 壓縮js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
// 使用'ejs'后綴防止模板文件被html—loader 解析
template: path.join(__dirname, 'index.ejs'),
filename: 'index.html',
minify: { // 開啟最小化操作
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyCSS: true,
minifyJS: true, // 使用uglifyjs進行壓縮,所以需要安裝相關依賴
}
})
]
}
20. 優化Node核心庫的polyfill
環境:生產
目標:打包體積,加載體積
版本變動:webpack5
webpack打包時會自動加入Node環境的polyfill,所以在瀏覽器中也可使用Node核心庫,比如 process.env.node_env
,這會略微增加打包的體積。如果不需要瀏覽器使用Node的核心庫,請配置:
module.exports = {
// ...
node: false
}
你也可以對每一個核心庫進行精確配置,以下是webpack4的默認配置值(webpack5有差別):
module.exports = {
// ...
node: {
console: false,
global: true,
process: true,
__filename: "mock",
__dirname: "mock",
Buffer: true,
setImmediate: true
}
}
其中的 console
,global
,process
等為node核心庫中的相應功能,更多選項需查找 node
核心庫。配置的值的含義如下:
-
true
:提供polyfill。 -
mock
:提供 mock 實現預期接口,但功能很少或沒有。 -
empty
:提供一個空對象。 -
false
:不提供polyfill,如果使用相應的node庫,會觸發錯誤。
21. 分離代碼
環境:生產
目標:打包體積,加載體積
版本變動:webpack3,webpack5
webpack將各種資源打包成一個JS文件,合并請求,優化用戶體驗。現在的web應用體積越來越大,單一的js文件體積過大,需拆分成多個js文件,減少加載體積。這也是懶加載、長久緩存等技術使用的基礎。分離出的每一個js文件,稱之為chunk。
webpack3使用 CommonsCunksPlugn
配置復雜,webpack4提供開箱即用的 optimization.splitChunks
。生產環境自動啟用 optimization.splitChunks
,默認配置足以應對大多數情況,不需要專門配置。默認配置遵照的主要規則:
- 主入口會打包成單獨的chunk。
- 動態導入的每一個模塊,會打包成一個chunk。例如按路由分離3個頁面,將會打包出3個chunk。
- 每個chunk中用到的node_modules依賴,會被打單獨打包成以
vendors~
開頭的chunk - 如果兩個chunk中包含相同的內容,相同的內容會被單獨打包成一個共享chunk。
比如有個項目:
- index 入口頁,用到react,index組件
- about 介紹頁,用到react,about組件,common組件
- detail 詳情頁,用到angular,detail組件,common組件
將打包成:
- index.js: react, index組件
- about~detail.js: common組件
- about.js: about組件
- detail.js: detail組件
- vendors~detail.js: angular
如修改默認規則,請詳細閱讀官方文檔,并注意評估打包結果,防止負優化,例如:
module.exports = {
//...
optimization: {
splitChunks: {
//... 一些配置
}
}
}
webpack5對optimization.splitChunks
進行一些優化,配置略有不同。
22. 分離CSS
環境:生產
目標:打包體積,加載體積
版本變動:webpack3
在分離JS的基礎上,有時需分離CSS,以實現CSS的按需加載。在webpack3 時使用 ExtractTextWebpackPlugin
, 在webpack4 時請使用 mini-css-extract-plugin
:
// npm i -D mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[hash].css',
chunkFilename: '[contenthash].css' // 注意使用contenthash
})
],
module: {
rules: [
{
test: /\\.css$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true,
hmr: process.env.NODE_ENV === 'development' // 熱響應
},
},
'css-loader',
],
},
],
},
};
CC將分離成一個個chunk。在開發階段可以考慮不分離CSS,以提高打包速度。
23. 使用按需加載,懶加載,預加載
環境:生產
目標:加載體積
版本變動:webpack5
現在網頁應用功能越來越多,尺寸越來越大,我們可以只加載用戶用到的部分,而不加載其他部分以提高性能。比如只加載首屏需要的chunk,而其他部分的chunk暫時不加載。
我們可以使用上文所說的 split-chunk
分離代碼,并通過動態導入實現按需加載/懶加載。Vue,React等都提供動態導入組件的方案,也可以用 function import() 實現懶加載,比如:
import(/* webpackChunkName: "lodash" */ 'lodash')
webpack5使用更加智能的chunk命名方式,不需要諸如 /* webpackChunkName: "lodash" */
的代碼。
對于分離的代碼,我們也可在空閑時間,將其預先加載,以提高用戶體驗,比如:
import(/* webpackPrefetch: true */ 'lodash')
24. 使用長久緩存
環境:生產
目標:加載體積
版本變動:webpack3,webpack5
瀏覽器打開應用時會下載資源,這會占用大量的時間。如果將一部分資源在瀏覽器中緩存,下次打開頁面時不需要重下載,可以降低首屏渲染時間。
我們在修改項目時,通常只修改一部份,讓瀏覽器僅更新修改部分即可,其余資源使用緩存。webpack打包后,會給每一個靜態資源添加hash后綴,如果修改該資源,hash會發生改變,瀏覽器會重新加載該資源。如果該資源沒被修改,hash會保持不變,瀏覽器會使用緩存,這種技術叫做“長久緩存”(long cache)。需與前文提到的"持續緩存”相區別,持續緩存是用于提高打包速度的。
webpack5對長久緩存進行大量優化,配置更為方便。下面的步驟限于webpack4版本,webpack3也需要類似復雜的配置,但略有不同。
長久緩存依靠 splitChunk
代碼分離,請參考上文。
-
第一步,告知瀏覽器緩存文件
需要在服務器上設置緩存文件時長,比如通過
Cache-Control
// Server header Cache-Control: max-age=31536000 // 設置緩存一年時間
有多種設置緩存的方法,請查看相關文章
-
第二步,使用
recordsPath
配置項,方便調試在設置長久緩存時,我們需要知道webpack打包后的文件是如何劃分的,模塊id是否改變,
recordsPath
配置項可以產生一份json文件,記錄每次打包后的結果。const path = require('path') module.exports = { // ... recordsPath: path.join( process.cwd(), // npm run build 在terminal中執行時的目錄路徑 'records.json' // 輸出的文件名 ) }
-
第三步,為文件添加hash
需要為打包后的文件設置hash后綴,相當于給該文件添加一個版本號,告知瀏覽器該文件是否更新,比如設置
[name].[hash].js
。hash值有三種:-
[hash]
: webpack每次編譯都會產生一個唯一的hash值,每次編譯hash都會改變 -
[chunkhash]
: webpack每次編譯,對每一個chunk(每一個js文件)生成不同的hash值,如果修改chunk,會重新計算chunkhash。如果沒修改chunk,chunkhash不會變。對JS資源我們通常添加[chunkhash]
后綴。 -
[contenthash]
:webpack根據輸出的文件內容,計算并生成contenthash,每次編譯后都會根據內容生成contenthash,如果改文件內容不變,生成的contenthash是相同的。通常對圖片、字體、樣式等資源添加[contenthash]
后綴。
module.exports = { // ... output: { chunkFilename: '[chunkhash].js' // 為chunk添加chunkhash }, module: { rules: [ { test: /\\.ttf/, loader: 'file-loaser', options: { name: '[contenthash].ext'} //為字體添加contenthash } ] } }
由于不同的系統、計算機使用的hash計算方式不同,在不同的計算機對相同的項目打包,產生的hash值可能不同,導致長久緩存失效。
-
-
第四步,分離runtime
runtime記錄所有的資源文件名和路徑,以幫助瀏覽器找到最新版本的chunk,比如:
{ "0":"js/0.840dc3db.js", "common":"js/common.50055e90.js", "detail":"js/detail.dd333b62.js" //... }
通常每個chunk中都包含runtime信息,只要有一個chunk發生變化,runtime就會變化,進而使所有chunk發生變化。可以通過
optimization.runtimeChunk
將runtime分離為一個單獨的chunk,我們將將這個chunk稱為runtime.js
,有時也稱為manifest.js
(比如vue-cli中)。module.exports = { // ... optimization: { runtimeChunk: { name: 'runtime' // 分離runtime chunk,并命名為 'runtime' }, } }
-
第五步,內聯runtime文件
分離的runtime文件,將作為一個獨立的js加載,瀏覽器的請求順序變為:
index.html → runtime.js → 首屏chunk。
可以看出多了次請求,使用
inline-manifest-webpack-plugin
將runtime內聯進html文件,瀏覽器的請求順序修改為:index.html (包含runtime的代碼) → 首屏chunk
也可以使用
webpack-manifest-plugin
實現同樣功能,inline-manifest-webpack-plugin
的配置示例:// npm i -D inline-manifest-webpack-plugin const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') module.exports = { // ... plugins: [ new InlineManifestWebpackPlugin(), ], };
-
第六步,穩定moudle id
webpack會給每一個moudle分配一個id,id是按照順序計算的,如 0,1,2。如果你刪除了某個moudle,或者新增某個moudle,可能導致module id的順序發生變化,比如:
moudleId 變動情況 0 原來的0 1 新增的module 2 原來的1, id變化 3 原來的2,id變化
moudle id的變化會導致chunk文件發生變化,我們可以用
hashed-module-ids-plugin
或者用optimization.moduleIds
來穩定moudle id:const webpack = require('webpack') module.exports = { // ... plugins: [ new webpack.HashedModuleIdsPlugin(), ] }
經過復雜的配置,終于可以實現長久緩存。
25. 使用性能預算,控制體積
環境:生產
目標:打包體積
項目初期的體積較小,但隨項目發展,可能沒注意到,安裝的某個依賴,使項目打包體積急劇增加。使用性能預算,及時發現打包體積的增加。
配置 performance
即可啟用性能預算,生成環境自動開啟,默認配置如下:
module.exports = {
// ...
performance: {
hints: 'warning', // 尺寸過大后警告,
// 設為'errors'時體積過大將報錯
// 設為false時關閉性能預算
maxEntrypointSize: 250000, // 最大入口體積
maxAssetSize:250000 // 單個資源最大體積
}
}
打包后,如果有體積過大,將顯示:
使用 bundlesize
工具,可以進行更靈活的配置,比如單獨設置某個文件的體積預算,同時支持自動化CI。
26. 其他優化建議
-
并行運行多個webpack實例 。打包多個版本,比如打包國內版本應用、國際版本應用,可以使用
parallel-webpack
并行運行多個webpack實例。 -
指定browserslist。指定合適的browserslist,babel將據此選擇核合適的降級工具,有助于代碼體積的減少和打包速度的提升。比如開發環境需要更快的打包速度,可以設為
'last 2 Chrome versions'
;生產環境需考慮兼容性,可設為'> 1%, ie >= 11, not dead'
。 - 考慮為不同的瀏覽器打包不同版本,可以為IE、現代瀏覽器分別打包,再根據瀏覽器的標識加載不同的版本。也可以根據瀏覽器的尺寸將圖片打包成不同的尺寸,對PC端瀏覽器返回大圖,而對移動端瀏覽器返回較小尺寸的圖片。
-
如果擔心打包后的文件包含多個相同模塊,可以用
duplicate-package-checker-webpack-plugin
或者bundle-duplicates-plugin
進行檢查。