某一天,我忽然发现构建项目会常常失败,直接报错: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 进行了多处优化,一些存在安全问题的依赖包也获得解决了,最后上一张升级后我本地和我同事构建的时间。
个人电脑
别人家的电脑