Vue2老项目启动卡死排查指南:node-sass与Webpack兼容性修复
接手那个Vue2老项目的第一天我就领教了什么叫“启动卡死”。不是报错不是崩溃一切看起来都正常但启动命令发出去之后CPU直接拉满风扇轰鸣页面白屏等十分钟也没反应。最难受的是它没日志、没提示连卡在哪一步都看不出来。这类问题在维护老项目时太常见了。Vue2配Webpack3/4时代的技术栈放到今天的Node生态里到处都是兼容性地雷。今天这篇东西主要记录我曾经排查和解决“Vue2老项目启动卡死”的完整过程包括现象定位、常见病因、实用修复流程和速查表。适合正在维护Vue2老项目、或者刚接手老项目被启动问题折磨的同学照着一步步做基本能解决90%的卡死问题。1. 先分清卡死发生的阶段别急着乱改代码1.1 卡死到底卡在哪个环节我一开始犯的错误就是看到“卡死”就闷头查内存、改配置结果折腾半天没效果。后来我学会了一个笨但有效的方法先搞清楚卡死发生在哪个阶段。Vue2老项目的启动流程大致是安装依赖 → 解析配置 → 编译源码 → 启动DevServer → 浏览器打开 → 热更新挂载。每个阶段卡死现象完全不同。第一类npm install阶段就卡死。这种情况通常是依赖安装问题可能卡在某个包下载、卡在node-sass源码编译、或者卡在npm的依赖解析阶段。特征是终端长时间停在某个包名上不动网络看似繁忙但进度始终不涨或者CPU有一个进程持续跑50%以上。第二类npm run dev启动后编译阶段卡死。特征比较典型进程刚启动很快输出几条编译信息然后进度条卡在某个百分比之后什么都不再输出CPU居高不下甚至内存直线上升直到“heap out of memory”崩掉。第三类DevServer已经提示编译成功、地址也打印出来了但浏览器访问白屏或一直转圈。这种其实是代理、静态资源路径、或热更新socket连接出了问题表面上像是“启动卡死”但本质上是运行期挂起。第四类启动是好的但一修改代码就卡死。这类属于热更新链路问题比如文件监听数量超限、增量编译吃满CPU、eslint重跑全量检查等。判断方式很简单启动命令发出去盯着终端看卡在哪一步输出完了、后续落地上就有页面基本能锁定范围。另外打开任务管理器切到性能面板看CPU和内存的走势——编译阶段正常会有一个波动期但如果持续100%或者内存暴涨后回落不了说明有进程不对劲。1.2 用日志和进程监控做“初步体检”在动配置之前我建议先做两步体检。第一步给启动命令加最高级别的输出。比如Webpack3老项目常用的webpack-dev-server --inline --progress--progress会输出当前编译进度能告诉我们到底卡在哪个模块。如果用的是vue-cli2项目还可以临时改一下build/dev-server.js里的webpack配置把stats设置成verbose看到更多模块解析细节。但注意别一上来就开verbose输出太多反而难找关键点。第二步用系统工具盯资源。Windows上用资源监视器看哪个进程占CPU、哪个文件读写卡住macOS可以用top -o cpuLinux用top或htop。我遇到过一次非常隐蔽的卡死webpack在打包时某个文件所在的磁盘IO持续拉满代码本身没问题就是硬盘快满了导致编译临时文件写不进去表面看就像卡死。这类问题不看资源监视器真的难以发现细节。体检查清楚之后再判断该往哪个方向排查。第2节我会把最常遇到的几种病因逐个拆解每一类都给出判断特征和应对思路。2. 病因拆解老项目卡死的五类隐形杀手2.1 node-sass和Node版本的世纪恩怨如果有人问我Vue2老项目最大的坑是什么我第一票投给node-sass。这玩意儿是个C扩展包安装时需要通过node-gyp编译成本地二进制文件而它的编译强依赖Node对应的版本跟Node版本错位轻则安装超时重则启动直接卡死。比如老项目里写着node-sass: ^4.14.1这个版本搭配Node 12基本是安全的。但在Node 14、Node 16甚至更新的版本上安装时会尝试从源码编译过程非常慢经常让人以为卡住了。即使装上了启动时也可能因为bin文件与Node ABI不匹配加载阶段直接挂起。判断方法很简单命令行里执行node -v再看package.json里node-sass的版本然后去查一下这个组合在不在正常范围。另外npm install时如果看到类似node-gyp rebuild和一堆编译日志或者卡在libsass相关字样十有八九就是它。解法是用sassdart-sass替换node-sass。现在官方也推荐这么做而且老项目里这个切换成本不高后面实操部分我会给出具体改法。2.2 Webpack构建内存溢出老工具的时代局限早期Webpack3/4处理大规模项目时内存管理非常粗糙。源码多、依赖多、sourceMap全开编译过程会把大量中间结果丢在内存里。Node老版本默认的堆内存上限不高项目一大就“heap out of memory”。这种卡死有一个非常明显的特征终端先是一顿疯狂输出然后突然卡住过一会儿抛出一段FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory。有的环境不直接抛错而是进程在那无限循环地GC看起来就是CPU爆满但什么活儿也干不完。判断方法也不难卡死时打开资源监视器看node进程的内存占用是不是涨到1.5GB左右然后在那上上下下波动。如果是基本就是堆内存不够。应对思路是两个方向一是直接给Node增加堆内存上限二是从Webpack配置层面减少内存消耗比如关闭不必要的sourcemap、限制并行处理模块数量。2.3 依赖树异常旧lockfile与npm版本的错配老项目里有一类卡死发生在安装阶段也就是npm install跑半天不结束卡在resolving或者某个包的reify阶段。这其实是npm的依赖解析器和老旧的lockfile、混乱的依赖树之间产生了死循环式的反复试探。尤其麻烦的是当时项目可能是在不同npm版本下维护的。比如有人用npm 7重新装过依赖生成了新版lockfile后来换回npm 6解析流程对不上直接卡死。还有一些老的node_modules目录里残留了半损坏的目录结构即使删掉单个包也会引发连锁异常。判断方法是看卡住的包名如果npm install反复卡在同一个包或者同一个包名出现两次但版本号不同这基本就是依赖树紊乱。应对方式不复杂但很暴力把node_modules和lockfile清掉重建同时固定一个npm版本去安装。具体操作后面讲。2.4 ESLint挂载在编译链路里全量检查拖垮进程尤其vue-cli2/vue-cli3改造出来的老项目特别喜欢在webpack配置里挂上eslint-loader把它当成编译链路的一环。这个设计在项目刚起步时没什么毛病可等代码库堆到几十个组件、上万行代码后问题就出来了每次启动webpack都会先对全量源码做ESLint检查。老项目里历史遗留代码的warning和error数量非常吓人我以前经手过一个项目全量lint报告有几百条warning。在这种情况下启动阶段会长时间停留在“Module build failed”之前的静默期很像是卡死了但实际ESLint正在一个个文件地硬跑。这类问题的特征也比较独特CPU占用高、但webpack日志停在很靠前的位置不动而且你会在任务管理里看到多个node子进程都在同一时间点活跃。临时应对方法是先把ESLint从编译链路里摘出去让它只在git提交前跑或者干脆启动时先禁用跑起来了再单独做lint。2.5 本地代理配置不合理DevServer反向卡住这个坑比前面几个都隐蔽。通常不会把“页面白屏转圈”归类成“启动卡死”但实际排障时它就是你说的“项目打不开”很多人会在这上面耗半天。开发环境里的index.js经常配置了proxyTable把/api指向后端测试环境地址。如果后端地址已经不可用、或者需要内网才通那DevServer本身能跑起来页面HTML也能吐出来但页面里所有请求尤其是首屏数据请求全部处于pending状态浏览器看起来就是白屏。更有一些配置把静态资源请求也错误地走了代理导致localhost:8080/static/js/xxx.js这种请求也去请求远程地址自然卡到天荒地老。判断方法看控制台Network面板找到请求卡住的entry看它的发起URL是否被错误代理改写或者直接看devServer的代理日志。解法就是修正proxyTable配置或者临时把代理目标切到本地mock服务先保证页面能打开。3. 一套可以直接照做的修复流程3.1 统一Node版本用nvm锁定运行环境基线解决老项目问题第一原则是先固定Node版本再谈其他。因为node-sass、webpack3、旧依赖统统对Node版本敏感不同版本跑出来的结果可能完全不一样。我建议用nvmNode版本管理器把环境切到老项目相对安全的区间。以Vue2老项目常见的依赖组合为例Node 12.22.12是一个兼容性很好的选择。如果你的机器上没有nvmWindows可以用nvm-windowsmacOS/Linux直接用nvm脚本装装好后执行node -v # 查看当前版本 nvm install 12.22.12 # 安装指定版本 nvm use 12.22.12 # 切换版本切换完成后务必确认node -v和npm -v都正常。很多卡死情况在切到Node 12之后连重装依赖都不用直接就能跑起来。注意如果项目里之前使用过Node 16或更高版本并且生成了新版lockfile切回Node 12后建议先处理掉lockfile再安装否则npm版本差异会引发新的依赖解析问题。3.2 替换node-sass为dart-sass这是处理老项目依赖老化环节中最安全的操作。dart-sassnpm包名是sass是node-sass的官方替代方案功能对齐、安装稳定、不需要源码编译也不会因为Node版本直接罢工。第一步先卸载node-sassnpm uninstall node-sass第二步安装sass包老项目建议装~1.32.0版本这个版本对webpack3/4和vue-cli2时期的配置兼容得很好npm install -D sass~1.32.0第三步修改sass-loader的配置让它显式使用sass实现。如果项目用的是vue-cli2通常会在build/webpack.base.conf.js里找到类似下面这段{ loader: sass-loader, options: { implementation: require(sass) } }如果老项目没有显式配置sass-loader只是靠webpack自动解析那很可能需要看一下node_modules里是否还有残留的sass-loader配置引用。部分老项目在package.json里只写了node-sass但代码里引入了sass的scss语法此时把node-sass换成sass后一般直接可用。换完以后启动一次看是否还会卡在编译阶段。这里有个小细节如果终端卡住超过一分钟还没反应可以先按组合键CtrlC停掉检查是不是有旧进程占用端口再重新启动。3.3 给Node堆内存扩容顺手精简Webpack配置内存溢出问题可以用环境变量解决不需要改代码。但需要区分不同操作系统在macOS和Linux上export NODE_OPTIONS--max-old-space-size4096 npm run dev在Windows命令行里set NODE_OPTIONS--max-old-space-size4096 npm run dev如果要写死在package.json的scripts里建议用cross-env这个包这样可以做到跨平台统一。修改后大概是这样的scripts: { dev: cross-env NODE_OPTIONS--max-old-space-size4096 webpack-dev-server --inline --progress --config build/webpack.dev.conf.js }注意日志提示一般在最后几行然后进程挂起这类通常是操作系统的打开文件数、监听文件数不足后面会专门说。再说Webpack配置本身可以减少内存消耗的方法。第2节提到sourceMap全开会增加内存占用。老项目启动阶段往往不需要完整的sourceMap可以在webpack.dev.conf.js里先临时把devtool设置成false或cheap-module-eval-source-map降低内存压力。同时如果项目配置了CommonsChunkPlugin或类似拆包策略启动时没必要把所有chunk都优化到位可以先注释掉那些optimization相关的配置让devServer先跑起来。3.4 清理依赖、重建node_modules和lockfile如果确认依赖解析卡死了处理思路如下第一步删除旧的node_modulesrm -rf node_modules package-lock.jsonWindows下对应rm -rf node_modules package-lock.json如果旧项目里既有package-lock.json也有npm-shrinkwrap.json建议重点保留npm-shrinkwrap.json删除package-lock.json。因为npm-shrinkwrap.json通常被发布到私有源里时它的优先级更高、内容也更稳定。第二步清理npm缓存。npm的本地缓存里可能残留了半损坏的包npm cache verify如果verify过程有问题狠一点直接npm cache clean --force第三步固定npm版本。老项目兼容性最好的npm版本是6.x这个版本对Webpack2/3时代项目lockfile的处理比较温和npm install -g npm6第四步重新安装依赖。这里我习惯加一个参数减少噪音npm install --no-audit --no-fund安装完成后对比npm ls输出确认没有大量missing或invalid的依赖。如果有通常是版本范围内无法共存需要手动去package.json里调整版本号。3.5 调整DevServer配置关掉编译链路里的ESLint如果依赖干净了、Node版本合适了启动还是卡把矛头指向ESLint。在vue-cli2老项目里常见做法是在build/webpack.base.conf.js里有一段{ test: /\.(js|vue)$/, loader: eslint-loader, enforce: pre }临时调试阶段可以先把这段直接注释掉或者通过环境变量控制。比如在配置里这样写if (process.env.ESLINT_DISABLE) { // 不挂载eslint-loader } else { rules.push({ test: /\.(js|vue)$/, loader: eslint-loader, enforce: pre }); }然后在package.json里把启动脚本临时改成cross-env ESLINT_DISABLEtrue webpack-dev-server --inline --progress如果是vue-cli3/4创建但内核是Vue2的项目在vue.config.js里设置module.exports { lintOnSave: false }关掉ESLint后再启动CPU会明显降下来再次编译的速度也能快上好几倍。还有一个隐形杀手是HMR的文件监听Linux下会报Error: ENOSPC: System limit for number of file watchers reached。这个报错出现时进程不会死但会反复报错并中断热更新表现就像卡死。解决办法是调高系统监听上限echo fs.inotify.max_user_watches524288 | sudo tee -a /etc/sysctl.conf sudo sysctl -pmacOS用户一般不缺这个限制Windows的Node在支持度上反而稳定些遇到类似问题优先检查杀毒软件是否拦截了文件变更监听。3.6 核对代理和端口验证DevServer已经可用走到这一步项目基本能跑起来了但如果打开浏览器还是白屏重点看代理配置。在老项目的config/index.js里通常有proxyTable配置。我习惯在启动后先做一个最小验证打开浏览器直接访问http://localhost:8080看终端是否有请求日志。如果能看到GET /和GET /index.html返回200说明DevServer本身没问题此时再看Network面板找卡住的接口或静态资源请求。测试时可以把代理目标临时指向一些稳定的地址或者干脆注释掉proxyTable让页面走本地资源。确认页面能打开后再逐步恢复代理配置对比卡住的是哪类请求。另外排查端口占用也很重要。有时候8080端口被旧DevServer进程占用新起的进程占不到端口会表现为卡住不动。命令netstat -ano | findstr :8080 # Windows lsof -i :8080 # macOS/Linux看到占用进程后把它杀掉再重新启动。如果确认了服务器没毛病但浏览器一直处于“等待响应”状态还有一个细节值得检查devServer配置里的host。如果设置了0.0.0.0在某些代理环境下访问localhost会超时这时候改用127.0.0.1访问或者把disableHostCheck: true老webpack3项目用这个加上避免Host头校验导致请求挂起。4. 启动卡死的常见问题速查与排障心得4.1 症状-原因-对策速查表我在下面整理了一张速查表基本覆盖日常遇到的情况。每次接到“项目卡死”的反馈先对照这张表再针对性处理症状可能原因快速解法npm install卡住长时间停在某个包网络问题、node-sass源码编译、lockfile错配切换镜像源、替换node-sass为sass、固定npm 6后重装启动后CPU 100%日志停在早期阶段ESLint全量检查、sass编译卡住临时关ESLint、替换sass实现、看资源监视器定位子进程内存持续升高最终heap out of memoryNode堆内存上限太低、sourceMap开太大设置NODE_OPTIONS--max-old-space-size4096、关闭devtool编译完成但打开白屏、接口pending代理配置错误、Host头校验、静态资源路径错误核对proxyTable、加disableHostCheck、检查assetsPublicPath改代码触发热更新卡死文件监听数量超限、增量编译瓶颈调高inotify上限、关闭打包时lint、用watchOptions聚合改动启动时卡在“Starting development server”端口被旧进程占用查端口并杀掉旧进程多个node进程堆在终端不输出webpack并行处理超限或依赖解析减少并行模块数或临时改用--no-parallel跑一次构建4.2 几条能救命的排查习惯这些经验是我踩过很多坑换来的分享几个特别有用的习惯。第一改动配置前先备份。老项目没有版本管理意识的话非常危险。我处理老项目时会先给package.json、webpack.base.conf.js、config/index.js各拷贝一份带时间戳的备份文件。因为有时候只是临时验证一个假设改完没恢复后面翻车了连对比基线都找不到。第二每次只改一个变量。启动卡死的排查最忌讳同时改多个配置。我就干过同时升级webpack、换sass实现、加内存、关ESLint结果项目跑起来了但根本不知道是哪一项起了决定性作用。后面老老实实一次只改一个点跑一次记录结果再改下一个。这个过程虽然慢但每次结论都比较可靠也方便后续维护者看历史。第三给老项目做一个.nvmrc文件。在项目根目录创建文件里面只写一行12.22.12这样后来者只需要在项目目录执行nvm use就能自动切到匹配的Node版本能省掉很多入门阶段的环境坑。第四养成分阶段启动验证的习惯。不要直接npm run dev一条龙。可以先手动执行node build/build.js跑一次生产构建看能否无损编译再执行npm run dev把DevServer阶段独立出来。生产构建能过开发启动大概率是DevServer、HMR或ESLint层面的问题生产构建都过不了那就是源码级、依赖级问题优先解决编译错误。第五留意网络代理环境。有些老项目在公司内网开发npm源必须走内部镜像但DevServer的代理又是另一套。如果把npm镜像信息和DevServer代理混在一起排查很容易绕晕。原则是npm安装的问题一律看npm configDevServer代理的问题一律看proxyTable和Network面板两边分开查。4.3 最后分享一个实际场景的排障顺序写到最后我把自己处理老项目启动卡死的一套顺序分享出来。拿到项目先不启动先做三件事看package.json里的scripts、看node-sass和webpack版本、看是否存在.nvmrc。然后nvm use切到指定Node版本清理缓存重装依赖关ESLint加内存启动。跑不起来再逐层往下查代理、端口、文件监听。这种“从环境到配置从安装到运行”的顺序基本能覆盖绝大多数卡死原因。以前碰到一个特别邪门的例子项目在A机器上能正常启动换到B机器上就卡死配置、依赖、Node版本全都一样后来发现是B机器上某个旧node进程占用了CPU核心数webpack3的并行编译一跑系统直接卡死。杀掉那个进程后一切恢复正常。这类问题没有标准答案但排查思路和方法是通用的。希望这篇记录能帮正在被老项目折磨的同学少走点弯路。