前言
同学,你好!我是 嘟老板。最近接手了个集团一体化前端项目,因为承载了多个产品线的业务模块,代码量巨大,本地启服务时总是内存溢出,启动老是失败。今天给大家分享下解决内存溢出问题的心路历程。
阅读本文您将收获:
- 处理内存溢出的常用手段。
- 特定于项目的内存溢出原因及解决方法。
- 了解 source-map 及其生效原理。
- 总结 webpack devtool 常见配置及相关效果。
什么是内存溢出
不同的系统,对于 V8 的内存限制也有所不同。默认情况下,V8 在 32位 系统上的内存限制为 512 MB,在 64位 系统上的内存限制为 1 GB。
在启动服务时,如果当前进程占用的内存过大,超出了内存限制,系统就会终止当前进程。
通常终端会报 “JavaScript heap out of memory” 错误,即 JavaScript 堆内存溢出。
处理内存溢出的常用手段
解决问题的根本,无非就是 增加 node 进程可用的内存。
目前比较常用的方法有以下两种:
--max-old-space-size 手动增加内存
我们可以通过手动设置 --max-old-space-size 增加内存。
- 若使用 node 命令运行服务,可以执行
node --max-old-space-size=10240
。 - (windows)若使用 npm script 运行服务,可以在 node_modules/.bin 下的 webpack-dev-server.cmd 和 webpack.cmd 文件中添加设置 --max-old-space-size 的代码。
webpack-dev-server.cmd:
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe --max-old-space-size=10240" "%~dp0\..\webpack-dev-server\bin\webpack-dev-server.js" %*
) ELSE (
@SETLOCAL @SET PATHEXT=%PATHEXT:;.JS;=;% node --max-old-space-size=10240 "%~dp0\..\webpack-dev-server\bin\webpack-dev-server.js" %*
)
webpack.cmd:
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe --max-old-space-size=10240" "%~dp0\..\webpack\bin\webpack.js" %*
) ELSE (
@SETLOCAL @SET PATHEXT=%PATHEXT:;.JS;=;% node --max-old-space-size=10240 "%~dp0\..\webpack\bin\webpack.js" %*
)
- 设置 NODE_OPTIONS 环境变量
NODE_OPTIONS 是 nodejs 提供的环境变量,包含一些 V8 相关的配置项,--max-old-space-size 就是其中之一,可以通过设置该配置项修改内存限制。
系统不同,执行的命令也不同。以下命令可在终端输入执行。
windows:
set NODE_OPTIONS=--max-old-space-size=10240
类unix系统(Linux/macOS):
export NODE_OPTIONS=--max-old-space-size=10240
跨平台通用方式:
全局安装 cross-env,终端输入以下命令,回车执行:
npm i -g cross-env
安装完成后,终端输入以下命令,回车执行:
cross-env NODE_OPTIONS=--max-old-space-size=10240
以上命令都是在项目根目录下执行,也可以将命令添加到 npm script 中,如下:
在 package.json 文件的 script 选项选项中新增以下命令:
"node:space": "cross-env NODE_OPTIONS=--max_old_space_size=10240"
然后在项目根目录下执行:npm run node:space
即可。
increase-memory-limit
需要先全局安装 increase-memory-limit。
终端输入 npm i -g increase-memory-limit
,回车执行。
安装成功后,终端输入 increase-memory-limit
,回车执行。
执行成功后,会在项目的 /node_modules/.bin 下的文件拼接 --max-old-space-size=10240
,其实就是自动为我们做了第一种方法的事情。
本项目中导致内存溢出无法解决的根源
然而,对于我手里的这个项目来说,即便用上面的方式将内存增加到 10240MB,仍然无法解决问题。
这是怎么回事呢?
通过仔细查看终端日志,发现内存溢出都是在执行 SourceMapDevToolPlugin 的时候出现的:
由此推断,可能是 source-map 导致的,于是我就跑去检查 vue.config.js 配置文件,发现了这么一段代码:
本项目使用 vuecli 创建,所以配置文件是 vue.config.js。检查对应的配置文件即可,无需纠结是 vue.config 还是 webpack.config。
chainWebpack(config) {
config
.when(process.env.NODE_ENV === 'development', (config) =>
config.devtool('source-map')
)
}
意思就是当启动开发环境时,设置 webpack devtool 配置项为 souce-map。了解 webpack 的同学应该知道,使用 source-map 选项编译,调试时可以精准定位到源代码的 行、列,但精准的前提是,编译后的代码需要映射到未经任何处理 的源代码,这也意味着,生成的 .map 文件会更大,占用更多的内存空间。
于是将这里的 source-map 改为更适合开发环境的 eval-cheap-module-source-map,内存溢出的问题就解决了。调试时同样可以定位到源代码,并且极大的改善了编译速度,开发效率和调试效率都得到提升。
chainWebpack(config) {
config
.when(process.env.NODE_ENV === 'development', (config) =>
config.devtool('eval-cheap-module-source-map')
)
}
source-map 及常见配置
什么是 source-map
为保证我们的代码能够在浏览器正常运行,通常需要对代码进行压缩、混淆、合并以及兼容性处理,这也就导致线上代码和我们本地源代码差异很大,调试难度 up up。
source-map 为我们提供了调试线上代码的有效途径,通过线上代码与源代码之间的映射关系,可以很容易的定位源代码的位置。
source-map 生效原理
SourceMap 其实就是一个映射文件,保存着编译后代码的位置及对应的源代码位置。
若启用 webpack 的 source-map,编译后会生成一个 .map 的文件,保存以下信息:
- version: SourceMap 版本。
- sources: 源文件列表。
- names: 源文件中的变量名。
- mappings: 编译后的代码与源代码的位置信息映射。
- file: SourceMap 对应的文件名称。
- sourcesContent: 源代码字符串列表,用于调试时展示源代码,与 sources 内容对应。
- sourceRoot: 源文件根目录,会加在每个源文件之前。
编译后的文件代码结尾,会多一个 SourceMap 文件位置的注释 sourceMappingURL。
比如:
后续浏览器加载 app.js
时,会通过 sourceMappingURL 加载对应的 app.js.map 文件,根据文件中 sources 字段,在浏览器的 Sources 面板下生成相应的目录结构,然后再将 sourcesContent 内容对应写入 Sources 下生成的对应文件,这样在调试时就可以通过编译后的代码定位到源代码位置。
webpack 中关于 SourceMap 的常见配置
webpack 中通过 devtool 配置,启用 SourceMap。
devtool 常用配置值例举
以下只列举部分配置值,更多详细见官网。
devtool 配置值 | 性能 | 是否适合生产环境 | 编译质量 |
---|---|---|---|
(none) | build: fastest rebuild: fastest | 是 | 打包后的代码,一整个文件,模块不分离 |
eval | build: fast rebuild: fastest | 否 | 编译生成的代码,模块分离,未经 loader 处理 |
source-map | build: slowest rebuild: slowest | 是 | 源代码,未经任何处理,调试时可以定位到 行、列 |
eval-source-map | build: slowest rebuild: ok | 否 | 源代码,未经任何处理,调试时可以定位到 行、列 |
cheap-source-map | build: ok rebuild: slow | 否 | 编译生成的代码,模块分离,经过了 loader 处理 |
inline-source-map | build: slowest rebuild: slowest | 否 | 源代码,未经任何处理,调试时可以定位到 行、列 |
nosources-source-map | build: slowest rebuild: slowest | 是 | 源代码,未经任何处理,调试时可以定位到 行、列 |
hidden-source-map | build: slowest rebuild: slowest | 是 | 源代码,未经任何处理,调试时可以定位到 行、列 |
eval-cheap-module-source-map | build: slow rebuild: fast | 否 | 源代码,调试时可以定位到行,列信息被忽略 |
devtool 配置值格式:
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
inline-、hidden-、eval-、nosources-、cheap-[module-] 等前缀自由组合,按需使用。
分别对应的基础效果如下:
-
inline-:编译后不会生成 SourceMap 文件,映射文件会内联到编译后的代码文件中。
-
hidden-:生成 SourceMap 文件,但编译后的代码中不包含 sourceMappingURL,即浏览器在加载编译后代码资源时,不会同时请求 SourceMap 文件,仅适用于错误报告场景定位问题代码位置。
-
eval-:使用 eval 包裹编译后的代码及 SourceMap 字符串,速度快但安全性较低,仅适用于开发环境。
-
nosources-:生成的 SourceMap 文件不包含 sourcesContent 内容,即调试时只能定位到错误文件及位置信息,无法跳转源代码。
-
cheap-[module-]:调试时只能定位到源代码的行,列信息被忽略。若无 module-,则定位到的源代码是 loader 处理后的代码;若有 module-,则定位到的源代码是 loader 处理前的代码。
不同环境使用建议
以下是官方针对不同环境的推荐配置。
生产环境
- (none)
- source-map
- hidden-source-map
- nosources-source-map
生产环境根据项目需求,酌情配置,不过要保证安全,若需要 SourceMap,则应该控制前端对于 SourceMap 文件的访问,避免源代码泄露。
开发环境
- eval
- eval-source-map
- eval-cheap-source-map
- eval-cheap-module-source-map (多数选择)
开发环境更注重开发效率,打包速度、调试方便程度等。
结语
本文重点介绍了前端内存溢出的解决方式及 SourceMap 相关内容,从个人解决实际项目问题的角度出发,旨在帮助同学们加深对于相关问题的印象,掌握多种解决方法以及 SourceMap 的应用理解。希望对您有所帮助。
如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。