WebpackDevServer内存不足时如何强制Node.js使用Swap空间避免崩溃?
背景与现象
我在运行一个复杂React应用的WebpackDevServer时,核心执行代码为new WebpackDevServer(compiler, config).listen(),但经常遇到FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory错误导致崩溃。
我的package.json中start脚本配置如下:
package.json
{ ... "scripts": { "start": "node scripts/start.js", ... } ... }
我尝试过用yarn start --max-old-space-size=32000或node scripts/start.js --max-old-space-size=32000启动,但不确定yarn是否正确传递了参数。
通过watch -n1 free -m监控内存发现:当物理内存剩余约5.7GB时,WebpackDevServer会在1分钟内持续消耗内存,直到物理内存剩余仅200MB左右触发崩溃,期间Swap空间完全没有被使用。只有关闭其他大部分应用,让物理内存剩余约1GB时,才能成功启动服务。
系统信息:
[caleb@caleb-manjaro ui]$ lsb_release -a LSB Version: n/a Distributor ID: ManjaroLinux Description: Manjaro Linux Release: 23.0.0 Codename: Uranos
完整启动脚本如下:
'use strict'; process.env.BABEL_ENV = 'development'; process.env.NODE_ENV = 'development'; process.on('unhandledRejection', (err) => { throw err; }); require('../config/env'); const fs = require('fs'); const chalk = require('react-dev-utils/chalk'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const clearConsole = require('react-dev-utils/clearConsole'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); const { choosePort, createCompiler, prepareProxy, prepareUrls, } = require('react-dev-utils/WebpackDevServerUtils'); const semver = require('semver'); const paths = require('../config/paths'); const configFactory = require('../config/webpack.config'); const createDevServerConfig = require('../config/webpackDevServer.config'); const getClientEnvironment = require('../config/env'); const react = require(require.resolve('react', { paths: [paths.appPath] })); const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); const useYarn = fs.existsSync(paths.yarnLockFile); const isInteractive = process.stdout.isTTY; if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { process.exit(1); } const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; const HOST = process.env.HOST || '0.0.0.0'; if (process.env.HOST) { console.log( chalk.cyan( `Attempting to bind to HOST environment variable: ${chalk.yellow( chalk.bold(process.env.HOST) )}` ) ); console.log( `If this was unintentional, check that you haven't mistakenly set it in your shell.` ); console.log( `Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}` ); console.log(); } const { checkBrowsers } = require('react-dev-utils/browsersHelper'); checkBrowsers(paths.appPath, isInteractive) .then(() => { return choosePort(HOST, DEFAULT_PORT); }) .then((port) => { if (port == null) { return; } const config = configFactory('development'); const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; const appName = require(paths.appPackageJson).name; const useTypeScript = fs.existsSync(paths.appTsConfig); const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; const urls = prepareUrls( protocol, HOST, port, paths.publicUrlOrPath.slice(0, -1) ); const devSocket = { warnings: (warnings) => devServer.sockWrite(devServer.sockets, 'warnings', warnings), errors: (errors) => devServer.sockWrite(devServer.sockets, 'errors', errors), }; const compiler = createCompiler({ appName, config, devSocket, urls, useYarn, useTypeScript, tscCompileOnError, webpack, }); const proxySetting = require(paths.appPackageJson).proxy; const proxyConfig = prepareProxy( proxySetting, paths.appPublic, paths.publicUrlOrPath ); const serverConfig = createDevServerConfig( proxyConfig, urls.lanUrlForConfig ); const devServer = new WebpackDevServer(compiler, serverConfig); devServer.listen(port, HOST, (err) => { if (err) { return console.log(err); } if (isInteractive) { clearConsole(); } if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { console.log( chalk.yellow( `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.` ) ); } console.log(chalk.cyan('Starting the development server...\n')); }); ['SIGINT', 'SIGTERM'].forEach(function (sig) { process.on(sig, function () { devServer.close(); process.exit(); }); }); if (process.env.CI !== 'true') { process.stdin.on('end', function () { devServer.close(); process.exit(); }); } }) .catch((err) => { if (err && err.message) { console.log(err.message); } process.exit(1); });
核心问题
是否有办法确保Node.js在内存不足时分配Swap空间,而非直接崩溃?
1. 先确保Node.js参数正确传递
Yarn传参的正确方式
Yarn不会自动把命令行参数传递给脚本里的Node进程,必须用--分隔参数:
yarn start -- --max-old-space-size=32000
或者直接修改package.json的start脚本,把参数写死,这样不管用yarn还是npm启动都能生效:
{ "scripts": { "start": "node --max-old-space-size=32000 scripts/start.js" } }
2. 调整系统内存调度策略,让系统更早使用Swap
Node.js本身不直接控制Swap使用,这是操作系统的内存管理范畴,可以通过修改Linux内核参数调整:
修改vm.swappiness参数
swappiness值越高,系统越倾向于使用Swap。默认值通常是60,临时调高可以执行:
sudo sysctl vm.swappiness=80
要永久生效,编辑/etc/sysctl.conf,添加或修改:
vm.swappiness=80
然后执行sudo sysctl -p让配置生效。
调整vm.min_free_kbytes参数
这个参数定义系统必须保留的最小空闲内存量,降低它可以让系统在剩余内存更少时才触发OOM Killer,同时更早使用Swap。先查看当前值:
cat /proc/sys/vm/min_free_kbytes
临时修改(比如设为102400,即100MB):
sudo sysctl vm.min_free_kbytes=102400
永久修改同样编辑/etc/sysctl.conf,添加对应配置后执行sudo sysctl -p。
3. 优化Webpack构建减少内存占用
从根源上降低内存需求更有效,针对复杂应用可以做这些优化:
- 启用Webpack的
cache配置,缓存构建结果,减少重复编译的内存消耗 - 用
splitChunks拆分大型代码块,优化分包逻辑 - 禁用开发环境不必要的loader和plugin,只保留必需工具
- 开启
webpack-dev-server的lazy模式,按需编译代码
4. 验证Swap是否正常工作
修改系统参数后,可以这样验证:
- 启动WebpackDevServer,同时用
watch -n1 free -m监控内存变化 - 观察物理内存不足时,Swap的
used值是否开始增加 - 如果Swap仍未被使用,先检查Swap分区是否正常挂载:
swapon --show
如果没有输出,说明Swap未启用,需要先挂载Swap分区或创建Swap文件。
内容的提问来源于stack exchange,提问作者Caleb Jay

