npm run start 老是内存溢出,原来是 source-map 在作怪

2,225 阅读8分钟

前言

同学,你好!我是 嘟老板。最近接手了个集团一体化前端项目,因为承载了多个产品线的业务模块,代码量巨大,本地启服务时总是内存溢出,启动老是失败。今天给大家分享下解决内存溢出问题的心路历程。

阅读本文您将收获:

  1. 处理内存溢出的常用手段。
  2. 特定于项目的内存溢出原因及解决方法。
  3. 了解 source-map 及其生效原理。
  4. 总结 webpack devtool 常见配置及相关效果。

什么是内存溢出

不同的系统,对于 V8 的内存限制也有所不同。默认情况下,V832位 系统上的内存限制为 512 MB,在 64位 系统上的内存限制为 1 GB

在启动服务时,如果当前进程占用的内存过大,超出了内存限制,系统就会终止当前进程。

通常终端会报 “JavaScript heap out of memory” 错误,即 JavaScript 堆内存溢出

image.png

处理内存溢出的常用手段

解决问题的根本,无非就是 增加 node 进程可用的内存

目前比较常用的方法有以下两种:

--max-old-space-size 手动增加内存

我们可以通过手动设置 --max-old-space-size 增加内存。

  1. 若使用 node 命令运行服务,可以执行 node --max-old-space-size=10240
  2. (windows)若使用 npm script 运行服务,可以在 node_modules/.bin 下的 webpack-dev-server.cmdwebpack.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" %* 
)
  1. 设置 NODE_OPTIONS 环境变量

NODE_OPTIONSnodejs 提供的环境变量,包含一些 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,回车执行。

image.png

执行成功后,会在项目的 /node_modules/.bin 下的文件拼接 --max-old-space-size=10240,其实就是自动为我们做了第一种方法的事情。

image.png

本项目中导致内存溢出无法解决的根源

然而,对于我手里的这个项目来说,即便用上面的方式将内存增加到 10240MB,仍然无法解决问题。

这是怎么回事呢?

通过仔细查看终端日志,发现内存溢出都是在执行 SourceMapDevToolPlugin 的时候出现的:

image.png

由此推断,可能是 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 其实就是一个映射文件,保存着编译后代码的位置及对应的源代码位置。

若启用 webpacksource-map,编译后会生成一个 .map 的文件,保存以下信息:

  • version: SourceMap 版本。
  • sources: 源文件列表。
  • names: 源文件中的变量名。
  • mappings: 编译后的代码与源代码的位置信息映射。
  • file: SourceMap 对应的文件名称。
  • sourcesContent: 源代码字符串列表,用于调试时展示源代码,与 sources 内容对应。
  • sourceRoot: 源文件根目录,会加在每个源文件之前。

编译后的文件代码结尾,会多一个 SourceMap 文件位置的注释 sourceMappingURL

比如:

image.png

后续浏览器加载 app.js 时,会通过 sourceMappingURL 加载对应的 app.js.map 文件,根据文件中 sources 字段,在浏览器的 Sources 面板下生成相应的目录结构,然后再将 sourcesContent 内容对应写入 Sources 下生成的对应文件,这样在调试时就可以通过编译后的代码定位到源代码位置。

webpack 中关于 SourceMap 的常见配置

webpack 中通过 devtool 配置,启用 SourceMap

devtool 常用配置值例举

以下只列举部分配置值,更多详细见官网

devtool 配置值性能是否适合生产环境编译质量
(none)build: fastest rebuild: fastest打包后的代码,一整个文件,模块不分离
evalbuild: fast rebuild: fastest编译生成的代码,模块分离,未经 loader 处理
source-mapbuild: slowest rebuild: slowest源代码,未经任何处理,调试时可以定位到 行、列
eval-source-mapbuild: slowest rebuild: ok源代码,未经任何处理,调试时可以定位到 行、列
cheap-source-mapbuild: ok rebuild: slow编译生成的代码,模块分离,经过了 loader 处理
inline-source-mapbuild: slowest rebuild: slowest源代码,未经任何处理,调试时可以定位到 行、列
nosources-source-mapbuild: slowest rebuild: slowest源代码,未经任何处理,调试时可以定位到 行、列
hidden-source-mapbuild: slowest rebuild: slowest源代码,未经任何处理,调试时可以定位到 行、列
eval-cheap-module-source-mapbuild: 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 的应用理解。希望对您有所帮助。

如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期推荐