某一天,我忽然發現構建項目會常常失敗,直接報錯:FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
,這個錯誤很明顯就是內存不足致使的構建失敗。因爲項目是在CI / CD 上構建的,而在此期間運維又調整了一下資源上限,所以什麼緣由致使的還得進一步排查,是因爲真的內存不足仍是存在內存泄漏?css
在64位計算機上,V8引擎的默認內存限制爲約爲1.5GB,就算有再多的RAM也無濟於事,可是也不是沒有辦法,NodeJs容許咱們設置節點進程的內存,也就是經過參數max_old_space_size
,咱們能夠暫時先設置內存限制,至少能先讓程序構建成功。html
// 增長上限至 4096 MB,該內存只要計算機支持就行。
node --max_old_space_size=4096 build.js
// 或者
// 增長上限至 4194304 KB
node --max_new_space_size=4194304 build.js
複製代碼
調整後發現再也沒有出現問題了,可是莫名其妙的爲啥會出現內存不足的問題呢?不過也能夠理解,項目愈來愈大,不免會出現內存不足,CPU 暴增的問題。如今咱們利用Chrome DevTools排查咱們的構建程序。node
排查的過程比較考驗耐心,由於電腦配置低跑起來都很慢。既然說到利用Chrome DevTools,那咱們就要製造證據。推薦使用 node-nightly 或者 node-heapdump 配合 memwatch-next。在這裏咱們使用 node-nightly
。安裝以及使用方法連接上有,就很少說了。react
我採集了堆內存分配樣本和堆內存動態分配時間線。結果發現並無異常的內存持續增加的狀況。雖說有少部分引用沒有回收,但不至於內存泄漏。有兩處忽然增加的緣由是一、實例化 Compiler
,繼承 Tapable
插件框架,實現註冊和調用一系列插件;二、實例化插件,如 UglifyJsPlugin
,而後讀取源文件,編譯並輸出,在這裏咱們還輸出了sourcemap
(特殊緣由,須要輸出)。webpack
堆內存分配樣本git
堆內存動態分配時間線程序員
內存問題解決了以後發如今本地打包速度也異常的慢(注:構建環境會影響打包速度,可是線上的構建環境資源是共享的,所以拿本地電腦來測試,構建時間因人而異)。目前的打包圖以下:es6
而同事(高端程序員)的電腦在未優化前則是這樣:github
話很少說,由於配置問題,纔會致使我有優化的慾望,低端配置以下:web
打包相關以下:
create-react-app v1
;React / Typescript / Antd / Less
;如今就開始選擇工具,來對咱們的項目進行分析。候選工具備progress-bar-webpack-plugin/webpackbar/speed-measure-webpack-plugin
。咱們想要的效果,是最好能分析出哪個階段的耗時。所以咱們來比較一下這些工具是否匹配咱們的需求。PS:webpack —progress,並不知足咱們的需求,由於是信息太過於簡單讓咱們無處排查問題。
從下圖能夠看出 progress-bar-webpack-plugin 跟 webpack --progress
同樣不知足咱們的需求,它只是展現打包的進度信息。
webpackbar 在不作任何的配置的前提下,也比 progress-bar-webpack-plugin
好,至少能知道卡在哪一步,加載 node_modules
依賴的過程。
咱們經過設置 profile
來獲取更多的信息,固然展現信息只有loaders
,而咱們每每也須要 plugins
的耗時,固然你也能夠經過自定義輸出信息,在這裏咱們就不展開討論,有興趣的小夥伴能夠自行嘗試。
// 經過配置 profile 展現詳細的信息
plugins: [
new WebpackBar({
profile: true,
reporters: ['profile'], // 注意這裏的配置很關鍵,不然沒信息
})
]
複製代碼
speed-measure-webpack-plugin 能夠經過很簡單的配置,就能夠獲取 plugins
以及loaders
的耗時。
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const smpWrapperConfig = smp.wrap({
// 將 webpack 的配置做爲參數傳給 SpeedMeasurePlugin
...webpackConfig,
});
module.exports = smpWrapperConfig;
複製代碼
咱們用 speed-measure-webpack-plugin
來檢測下咱們每一個階段的耗時,可是值得注意的是,咱們只須要關注哪個階段的耗時最長,而不須要關注它跑了多長時間,由於 speed-measure-webpack-plugin
的加入也會拖慢咱們構建的時間。(這是我反覆測試的結果,假若有問題,麻煩請指出😂)
咱們使用 speed-measure-webpack-plugin
來測試一下,發現UglifyJsPlugin
佔時最長,調研了一下發現 github issue 上有很多這樣的問題,甚至出現了咱們上文出現的 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
的問題:
在上文提到,項目構建速度慢,UglifyJsPlugin
佔一半。固然網上還有不少打包速度優化的手段,在這裏不作展開,一是由於效果不明顯,二是由於項目自己在早期也已經處理過,所以在這裏咱們針對性的優化一下。
我看網上有人推薦這個插件,可是其實在 CRA 中採用的 uglifyjs-webpack-plugin
也能夠經過參數 parallel: true
來達到多線程的做用,我測試過其實二者在速度上沒多大差異。更重要的是這個插件已經好久沒更新了,因此這個就直接跳過了,不推薦使用。
關於 happypack
,我相信網上已經能找到不少關於它的傳聞,從單一進程構建模式到多進程模式,從而加速代碼構建,關於更多話很少說,有興趣的自行研究。happypack
支持的 loaders
能夠看這裏 Loader Compatibility,原理看這裏happypack 原理解析。部分配置以下:
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
// module
{
test: /\.(ts|tsx)$/,
include: resolveApp('src'),
exclude: /node_modules/,
use:'happypack/loader?id=tsx',
}
// less 的就不寫了。
// plugins
new HappyPack({
id: 'tsx',
threadPool: happyThreadPool,
loaders: [
{
loader: require.resolve('ts-loader'),
options: {
happyPackMode: true,
transpileOnly: true,
getCustomTransformers: () => ({
before: [
tsImportPluginFactory([
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
]),
],
}),
},
}
]
}),
複製代碼
不知道爲何,在個人電腦使用 happypack
以前的比使用 happypack
以後的首次速度還要快,可是緩存構建也是不分上下沒差多少。這是由於 happypack
對電腦的內核有必定的要求,假如電腦的內核低的狀況下又開啓多線程,反而會讓佔滿電腦的 CPU,總體速度變慢,所以這個方案也不是最好的選擇(反正個人電腦爛)。
terser-webpack-plugin
是 webpack4 用來取代 uglifyjs-webpack-plugin
的壓縮插件,假如單純結合 webpack3 和 terser-webpack-plugin
,不知道能不能解決壓縮速度的問題。
在 webpack3 中,官方提供的插件是 terser-webpack-plugin-legacy
(看起來像是妥協版本)。從下圖能夠看出 ,oh my god(麻煩自行腦補李佳琦),這也太神奇了吧,簡直就是質的飛躍(不敢相信的我特地試了幾回)。
配置以下:
new TerserPlugin({
parallel: true,
cache: true,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
},
}),
複製代碼
通過一段時間(具體不詳)的觀察,在 webpack 3 提高構建速度的方法有以下的方法:
terser-webpack-plugin
替換 uglifyjs-webpack-plugin
;noParse
;alias
,這個能提高開發效率哦;webpack-bundle-analyzer
剔除無關的依賴;resolve.modules
,如 resolve.modules = ['node_modules']
,能夠減小搜索範圍;loaders
可使用 test/include/exclude
來減小沒必要要的遍歷;happypack
。假如你的項目使用了相似 React-Loadable進行按需加載,那麼請注意,React-Loadable
能夠幫助咱們根據路由來按需加載。它的原理是使用了import()
而非 import
是由於 import
是靜態編譯,而import()
同 require
,是能夠進行動態加載的。 可是千萬要注意的是,引用過程當中千萬不要使用變量,這會致使編譯經過可是編譯時間長得使人髮指又或者直接內存溢出。 - ES6 DYNAMIC IMPORT AND WEBPACK MEMORY LEAKS - Adrian Oprea - Medium
那麼最後咱們來嘗試一下這個號稱編譯速度提高了 60% ~ 98% 的「黑科技」。因爲咱們是使用了create-react-app
,所以咱們在升級過程當中會或多或少遇到不少問題,我在這裏記錄一下我升級過程當中遇到的問題。
因爲項目中已經 eject
了 create-react-app
,所以不能使用官方推薦且快速的升級 react-scripts
(本身挖的坑本身填)。
yarn add -D webpack webpack-cli webpack-dev-server
,升級webpack4 必備的三件套,缺一不可。別慌,準備工做其實就這麼多。慌的是如何處理升級後的兼容問題😂😂😂。
萬事開頭難,而後接着難,難上加難(滿臉寫着開心.jpg)。注意:每次解決問題就直接執行程序,即yarn start/build,下面就不贅述。
_this.compiler.applyPluginsAsync is not a function
👉🏻 升級 fork-ts-checker-webpack-plugin
。
Plugin could not be registered at 'html-webpack-plugin-before-html-processing'. Hook was not found. BREAKING CHANGE: There need to exist a hook at 'this.hooks'. To create a compatibility layer for this hook, hook into 'this._pluginCompat’.
👉🏻 升級 html-webpack-plugin@next
以及 react-dev-utils
; 👉🏻 同時對配置文件(dev/prod
)作如下優化:
// plugins
[
new HtmlWebpackPlugin({
... // dev 和 prod 保持原來的配置
}),
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)
]
複製代碼
webpack is not a function
👉🏻 對 start.js
作如下優化:
// 調整爲對象結構
const compiler = createCompiler({ webpack, config, appName, urls, useYarn });
複製代碼
When specified, "proxy" in package.json must be a string. Instead, the type of "proxy" was "object". Either remove "proxy" from package.json, or make it a string.
👉🏻 安裝/升級 http-proxy-middleware
; 👉🏻 將 package.json
中的 proxy
刪除,並添加src/setupProxy.js
,並將其添加到paths.js
; 👉🏻 修改 webpackDevServer.config.js
注意:
爲何要刪除
package.json
中的proxy
呢?由於proxy
在package.json
中雖然以字符串存在,可是在默認狀況下仍是會優先讀取package.json
中的proxy
字段,其次纔是setupProxy.js
。
// paths.js
module.exports = {
...,
proxySetup: resolveApp('src/setupProxy.js'),
}
// webpackDevServer.config.js
before(app, server) {
if (fs.existsSync(paths.proxySetup)) {
require(paths.proxySetup)(app);
}
}
// src/setupProxy.js
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(proxy('/api', {
target: 'https://xxx.xx.com',
changeOrigin: true,
}));
};
複製代碼
this.htmlWebpackPlugin.getHooks is not a function 假如報這個錯誤,那麼能夠嘗試如下操做:
👉🏻 刪除 node_modules
並從新安裝; 👉🏻 從新安裝 html-webpack-plugin@next
; 👉🏻 確保 new InterpolateHtmlPlugin(env.raw)
-> new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)
DeprecationWarning: Pass resolveContext instead and use createInnerContext DeprecationWarning: Resolver: The callback argument was splitted into resolveContext and callback DeprecationWarning: Resolver#doResolve: The type arguments (string) is now a hook argument (Hook). Pass a reference to the hook instead.
這個不是錯誤,你能夠選擇忽略,也能夠作出如下處理: 👉🏻 升級 tsconfig-paths-webpack-plugin
Tapable.plugin is deprecated. Use new API on
.hooks
instead
👉🏻 升級 extract-text-webpack-plugin
,可是在webpack4 已經不推薦使用該插件了,可使用 mini-css-extract-plugin
取代,值得注意的是使用 mini-css-extract-plugin
的同時能夠不使用style-loader
———Advanced configuration example
剩下的問題就是遇到什麼插件不兼容直接升級就能夠了,例如:
TypeError: Cannot read property 'ts' of undefined URIError: Failed to decode param ‘/%PUBLIC_URL%/favicon.ico’
👉🏻 升級ts-loader
以及 file-loader
如何使用 mini-css-extract-plugin
將全部的 css 文件都打包成一個css文件呢?其實有不少方法,咱們就使用官方推薦的方法Extracting all CSS in a single file,可是在這過程可能會報 Conflicting order between
的warnings,咱們能夠關閉警告 Remove Order Warnings。關於 CommonsChunkPlugin
能夠看這裏 RIP CommonsChunkPlugin.md · GitHub。
// 關於更多 splitChunks 能夠查看
// https://webpack.docschina.org/plugins/split-chunks-plugin/
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: { // entry 入口名稱
name: 'styles', // 提取 chunk 的名稱
test: /\.css$/,
chunks: 'all', // initial | all | async,默認 async
enforce: true,
},
},
},
},
...
}
複製代碼
配置完畢以後,將下面的依賴包替換成 webpack4 推薦的依賴包。
extract-text-webpack-plugin
-> mini-css-extract-plugin
;uglifyjs-webpack-plugin
-> terser-webpack-plugin
。到此 webpack4 基本上已經解決完畢了,剩下的問題,都是根據我的需求來處理了。升級到 webpack4 的過程不算太順利,可是這算是 webpack 的一個大版本,嘗試一下說不定就成功,畢竟 webpack4 進行了多處優化,一些存在安全問題的依賴包也獲得解決了,最後上一張升級後我本地和我同事構建的時間。
個人電腦
別人家的電腦