
1. 為什么現(xiàn)在可以動手清理 package.json 了如果你維護(hù)過超過兩年的前端項(xiàng)目打開package.json的那一刻大概率會有一種翻舊倉庫的感覺有些依賴你根本想不起來當(dāng)初為什么裝有些依賴的 GitHub 倉庫已經(jīng)兩年沒提交還有些依賴你每次npm install都會看到一行黃色的 deprecated 警告但一直懶得處理。更麻煩的是這些“僵尸依賴”并不會安靜地待著它們會拖慢安裝速度、撐大node_modules、在 CI 里制造偶發(fā)失敗甚至在安全掃描報(bào)告里給你添幾條高危告警。我最近花了幾個晚上把手上三個項(xiàng)目的依賴樹從頭到尾過了一遍刪掉了十幾個包其中有一批是“曾經(jīng)必須、現(xiàn)在多余”的典型代表。這篇文章就圍繞其中最有代表性的 5 個 npm 包展開講清楚它們當(dāng)年為什么被裝進(jìn)來、現(xiàn)在為什么可以刪、刪掉之后用什么替代、以及刪除過程中容易踩的坑。適合所有還在維護(hù) JavaScript 項(xiàng)目的人看不管你是剛接手老項(xiàng)目的新人還是想給依賴做減法的老手都能直接照著操作。需要先說明一點(diǎn)這里說的“可以刪”不是絕對的而是指在現(xiàn)代運(yùn)行時(shí)環(huán)境和主流構(gòu)建工具鏈下這些包提供的功能已經(jīng)被語言本身或平臺原生能力覆蓋。如果你的項(xiàng)目還跑在非常老的 Node 版本上或者有特殊的兼容性要求那刪除前需要自己評估。下面每一節(jié)我都會把判斷依據(jù)講清楚你可以對照自己的項(xiàng)目決定。2. 五個可以優(yōu)先清理的 npm 包逐個拆解2.1node-domexception被運(yùn)行時(shí)原生能力取代的典型如果你在終端里見過這行警告npm warn deprecated node-domexception1.0.0: Use your platforms native DOMException instead那你項(xiàng)目里就躺著這個包。它的作用非常簡單在 Node 環(huán)境里提供一個DOMException的實(shí)現(xiàn)。早年 Node 沒有全局的DOMException而一些 Web API 風(fēng)格的庫比如某些 fetch 實(shí)現(xiàn)、流處理庫需要拋出符合規(guī)范的異常對象于是就有人做了這個 polyfill?,F(xiàn)在的情況是Node 17 開始就已經(jīng)內(nèi)置了全局DOMException主流 LTS 版本18、20、22全都有。也就是說這個包的存在意義已經(jīng)被運(yùn)行時(shí)本身接管了。它通常不是你直接安裝的而是某個上游依賴的依賴所以你在package.json里可能根本找不到它需要在node_modules里往上追。判斷方法很簡單在項(xiàng)目根目錄執(zhí)行npm ls node-domexception如果輸出顯示它是某個包的傳遞依賴那你要做的不是直接刪而是升級那個上游包。我遇到的情況是某個老版本的fetch相關(guān)庫鎖死了舊依賴升級到最新版之后node-domexception就自動消失了。這里有個經(jīng)驗(yàn)不要手動去node_modules里刪文件夾那樣下次安裝又會回來而且可能破壞依賴樹。正確做法是通過npm dedupe或者升級上游包來解決。注意如果你的項(xiàng)目需要支持 Node 16 及以下版本那這個包可能還有存在價(jià)值。但 Node 16 已經(jīng)停止維護(hù)繼續(xù)支持它的成本遠(yuǎn)高于收益建議借這個機(jī)會把運(yùn)行時(shí)版本要求提上去。2.2core-js的全量引入從“保險(xiǎn)”變成“負(fù)擔(dān)”core-js本身不是垃圾包它是 JavaScript 標(biāo)準(zhǔn)庫 polyfill 的中流砥柱。問題出在引入方式上。很多項(xiàng)目在入口文件里寫一行import core-js或者用babel的useBuiltIns: entry配置把整個 polyfill 庫全量打進(jìn)來。這一下就是幾百 KB 的代碼而你的項(xiàng)目可能只用到其中不到 5% 的特性?,F(xiàn)在可以刪的理由有兩個。第一現(xiàn)代瀏覽器對 ES2015 的支持已經(jīng)非常完整Promise、Array.from、Object.assign、展開運(yùn)算符這些當(dāng)年需要 polyfill 的東西現(xiàn)在全都是原生支持。第二構(gòu)建工具的能力上來了babel/preset-env配合useBuiltIns: usage可以做到按需注入只為你實(shí)際用到的 API 引入對應(yīng)的 polyfill而且browserslist配置能精確控制目標(biāo)環(huán)境。我自己的做法是先把browserslist更新到只覆蓋最近兩年的主流瀏覽器然后把core-js的全量引入改成按需模式最后跑一遍構(gòu)建對比產(chǎn)物大小。實(shí)測一個中等規(guī)模的項(xiàng)目JS 產(chǎn)物從 480KB 降到了 310KBgzip 后也有明顯下降。如果你的項(xiàng)目壓根不需要支持 IE 或老版本 Safari甚至可以直接把core-js從依賴?yán)镆瞥寴?gòu)建工具根據(jù)browserslist自動決定要不要注入。具體操作上檢查babel.config.js或.babelrc// 改造前 presets: [ [babel/preset-env, { useBuiltIns: entry, corejs: 3 }] ] // 改造后 presets: [ [babel/preset-env, { useBuiltIns: usage, corejs: 3 }] ]同時(shí)把入口文件里的import core-js刪掉。改完之后一定要跑完整的回歸測試因?yàn)榘葱枳⑷肱紶枙┑粢恍﹦討B(tài)調(diào)用的場景比如通過字符串拼接調(diào)用的方法。2.3moment體積與不可變性的雙重問題moment曾經(jīng)是日期處理的默認(rèn)選擇但它的問題這些年被討論得很充分體積大壓縮后約 70KBgzip 后約 20KB、不支持 tree-shaking、對象可變導(dǎo)致容易寫出 bug、時(shí)區(qū)處理需要額外加載數(shù)據(jù)。更重要的是官方自己已經(jīng)在文檔里建議新項(xiàng)目考慮其他方案?,F(xiàn)在替代方案很成熟。如果只是做簡單的格式化和計(jì)算date-fns是首選它按函數(shù)導(dǎo)入tree-shaking 友好而且所有操作都是純函數(shù)不會意外修改原對象。如果項(xiàng)目對時(shí)區(qū)有強(qiáng)需求Luxon或者Day.js配合時(shí)區(qū)插件都能覆蓋。Day.js的 API 和moment高度相似遷移成本最低體積卻只有 2KB 左右。遷移的時(shí)候有個細(xì)節(jié)要注意moment的format占位符和date-fns不完全一樣。比如moment用YYYY-MM-DDdate-fns也是yyyy-MM-dd大小寫有區(qū)別。我建議寫一個小的適配層把項(xiàng)目里常用的格式化、解析、比較操作封裝成統(tǒng)一函數(shù)底層換實(shí)現(xiàn)這樣業(yè)務(wù)代碼改動最小。// 適配層示例 import { format, parseISO, differenceInDays } from date-fns; export function formatDate(date, pattern yyyy-MM-dd) { return format(typeof date string ? parseISO(date) : date, pattern); }這樣遷移的時(shí)候只需要改這一個文件業(yè)務(wù)代碼里的調(diào)用方式保持不變。實(shí)測一個用了moment三年多的項(xiàng)目半天時(shí)間就完成了替換產(chǎn)物里直接少了一個大塊頭。2.4request早已停止維護(hù)的 HTTP 客戶端request這個包在 Node 生態(tài)里曾經(jīng)是神一樣的存在下載量長期霸榜。但它在 2020 年就已經(jīng)進(jìn)入維護(hù)停止?fàn)顟B(tài)官方明確表示不再添加新功能只做最低限度的安全修復(fù)。現(xiàn)在繼續(xù)用它等于把一個不再更新的網(wǎng)絡(luò)層放在項(xiàng)目最核心的位置。替代方案在 Node 18 里非常直接全局fetch已經(jīng)可用不需要任何依賴。如果項(xiàng)目需要更細(xì)粒度的控制比如攔截器、超時(shí)重試、代理配置undici是 Node 官方團(tuán)隊(duì)維護(hù)的底層實(shí)現(xiàn)axios也依然活躍且生態(tài)成熟。我的建議是新代碼一律用原生fetch老代碼逐步遷移。遷移request的難點(diǎn)在于它的 API 風(fēng)格和fetch差異較大。request用回調(diào)配置項(xiàng)叫uri、qs、form而fetch用 Promise配置項(xiàng)是url、searchParams、body。我通常寫一個薄封裝來抹平差異async function httpGet(url, options {}) { const res await fetch(url, { method: GET, ...options }); if (!res.ok) throw new Error(HTTP ${res.status}); return res.json(); }然后把項(xiàng)目里所有request.get(...)替換成httpGet(...)。這個過程比較機(jī)械但收益很明顯少了一個不再維護(hù)的依賴網(wǎng)絡(luò)層的行為也更符合現(xiàn)代標(biāo)準(zhǔn)。2.5tslint被 ESLint 完全取代的 lint 工具tslint是 TypeScript 早期的 lint 工具但它的使命已經(jīng)結(jié)束。TypeScript 官方在 2019 年就宣布轉(zhuǎn)向 ESLinttslint進(jìn)入廢棄狀態(tài)?,F(xiàn)在typescript-eslint提供了完整的規(guī)則集而且能和 ESLint 生態(tài)里的其他插件React、Vue、import 排序等無縫配合。如果你的項(xiàng)目還在用tslint遷移到 ESLint 是遲早的事。好消息是官方提供了tslint-to-eslint-config工具可以自動轉(zhuǎn)換大部分配置。操作步驟大致是先安裝eslint、typescript-eslint/parser、typescript-eslint/eslint-plugin然后運(yùn)行轉(zhuǎn)換工具生成.eslintrc最后逐條檢查規(guī)則映射有些tslint規(guī)則在 ESLint 里沒有直接對應(yīng)需要手動調(diào)整或放棄。我遷移過一個中型項(xiàng)目轉(zhuǎn)換工具覆蓋了大約 80% 的規(guī)則剩下的 20% 里有一部分是tslint特有的格式規(guī)則現(xiàn)在交給 Prettier 處理更合適。遷移完成后tslint.json和tslint依賴都可以刪掉CI 里的 lint 步驟也統(tǒng)一成一條eslint命令維護(hù)成本明顯下降。3. 刪除依賴前的安全操作流程3.1 先摸清依賴樹的真實(shí)結(jié)構(gòu)直接打開package.json刪行是最危險(xiǎn)的做法因?yàn)槟憧吹降闹皇侵苯右蕾囌嬲膯栴}往往藏在傳遞依賴?yán)?。動手之前先用幾條命令把依賴樹摸清楚。# 查看某個包為什么被安裝 npm ls package-name # 查看所有過時(shí)的包 npm outdated # 查看所有 deprecated 的包 npm ls --all 21 | grep -i deprecatednpm ls的輸出會告訴你這個包是被誰引入的是直接依賴還是傳遞依賴。如果是傳遞依賴你要做的是升級上游包而不是直接刪。我習(xí)慣把輸出重定向到文件里慢慢分析因?yàn)榇笮晚?xiàng)目的依賴樹可能有幾千行。另外npm outdated能幫你發(fā)現(xiàn)哪些包有新版本可用。很多時(shí)候升級一個上游包就能順帶解決好幾個 deprecated 的傳遞依賴這比逐個手動處理高效得多。3.2 用鎖文件和 CI 保證刪除不破壞構(gòu)建刪除依賴之后package-lock.json必須同步更新。我的習(xí)慣是刪完package.json里的條目后執(zhí)行一次干凈的安裝rm -rf node_modules package-lock.json npm install這樣能確保鎖文件反映真實(shí)的依賴關(guān)系不會殘留已經(jīng)刪除的包的記錄。然后跑一遍完整的構(gòu)建和測試包括單元測試、集成測試、以及如果有的話端到端測試。CI 在這里的作用很關(guān)鍵。我會在刪除依賴的 PR 里特意觀察 CI 的安裝耗時(shí)和產(chǎn)物大小變化。通常刪掉幾個大包之后npm install能快十幾秒構(gòu)建產(chǎn)物也能小幾十 KB。這些數(shù)據(jù)可以作為刪除決策的佐證也能在代碼評審時(shí)說服同事。提示如果項(xiàng)目用了 monorepo 或者 workspace刪除依賴時(shí)要檢查所有子包的package.json別只改了根目錄。我踩過一次坑根目錄刪了moment結(jié)果某個子包還在用CI 直接報(bào)模塊找不到。3.3 灰度發(fā)布與回滾預(yù)案依賴變更屬于基礎(chǔ)設(shè)施層面的改動一旦出問題影響面比較大。我的做法是先把改動合并到一個獨(dú)立分支部署到預(yù)發(fā)環(huán)境跑一兩天觀察錯誤監(jiān)控和性能指標(biāo)。確認(rèn)沒問題再合并到主分支?;貪L預(yù)案也要提前準(zhǔn)備好。因?yàn)閜ackage-lock.json也變了回滾的時(shí)候不能只 revert 代碼還要把鎖文件一起恢復(fù)。我通常會在 PR 描述里寫清楚回滾步驟比如“revert 這個 commit 后執(zhí)行npm ci重新安裝”。這樣萬一線上出問題值班的人能快速操作。4. 替代方案選型與遷移實(shí)操4.1 日期庫遷移從 moment 到 date-fns 的完整對照日期處理是遷移工作量最大的部分因?yàn)閙oment的 API 太靈活了。我把常見的操作整理成對照表遷移的時(shí)候可以直接查。操作moment 寫法date-fns 寫法當(dāng)前時(shí)間moment()new Date()格式化moment(d).format(YYYY-MM-DD)format(d, yyyy-MM-dd)解析字符串moment(2026-01-01)parseISO(2026-01-01)加減天數(shù)moment(d).add(7, days)addDays(d, 7)差值moment(a).diff(b, days)differenceInDays(a, b)是否之前moment(a).isBefore(b)isBefore(a, b)注意date-fns的所有函數(shù)都是純函數(shù)不會修改傳入的日期對象。這其實(shí)是好事能避免很多隱蔽的 bug但遷移時(shí)要檢查業(yè)務(wù)代碼里有沒有依賴moment可變性的寫法。比如有些代碼會先const m moment()然后多次調(diào)用m.add(1, day)并期望m本身變化這種寫法在date-fns里必須改成接收返回值。4.2 HTTP 客戶端遷移request 到 fetch 的注意事項(xiàng)fetch和request最大的行為差異在于錯誤處理。request默認(rèn)把 4xx 和 5xx 也當(dāng)作正常響應(yīng)返回需要你自己判斷statusCode而fetch只有在網(wǎng)絡(luò)層面失敗時(shí)才 rejectHTTP 錯誤狀態(tài)碼不會觸發(fā) catch。所以遷移時(shí)一定要顯式檢查res.ok。另一個差異是超時(shí)。fetch沒有內(nèi)置的超時(shí)配置需要用AbortController實(shí)現(xiàn)async function fetchWithTimeout(url, options {}, timeout 5000) { const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); try { const res await fetch(url, { ...options, signal: controller.signal }); return res; } finally { clearTimeout(timer); } }還有一點(diǎn)fetch不會自動帶上 cookie需要設(shè)置credentials: include。如果原來的request配置里有jar: true遷移時(shí)別忘了這一項(xiàng)否則登錄態(tài)會丟失。這些都是我實(shí)際遷移時(shí)踩過的坑寫出來供你參考。4.3 lint 工具遷移tslint 到 typescript-eslint 的配置轉(zhuǎn)換tslint的配置文件是tslint.json規(guī)則名用駝峰或短橫線ESLint 用.eslintrc.js或eslint.config.js規(guī)則名用斜杠分隔。官方轉(zhuǎn)換工具能處理大部分映射但有幾類規(guī)則需要特別注意。格式類規(guī)則比如max-line-length、quotemark、semicolon建議直接關(guān)掉交給 Prettier 處理。類型檢查類規(guī)則比如no-unnecessary-type-assertion在typescript-eslint里有對應(yīng)實(shí)現(xiàn)但需要配置parserOptions.project指向tsconfig.json否則規(guī)則不生效。還有一類tslint特有的規(guī)則比如member-ordering在 ESLint 里也有但配置格式不同需要手動調(diào)整。遷移完成后記得把tslint從devDependencies里刪掉同時(shí)刪除tslint.json。CI 配置里的tslint命令也要換成eslint。我建議在遷移的 PR 里單獨(dú)提交配置文件的改動和業(yè)務(wù)代碼分開這樣評審的時(shí)候更清晰。5. 常見問題與排查技巧實(shí)錄5.1 刪了包之后構(gòu)建報(bào)錯怎么辦最常見的原因是漏改了引用。比如刪了moment但某個工具函數(shù)里還有import moment from moment。排查方法是全局搜索包名grep -r from moment src/ grep -r require(moment) src/如果搜索結(jié)果顯示還有引用要么改掉要么暫時(shí)保留這個包。另一種情況是傳遞依賴被破壞比如你升級了某個包它依賴的另一個包版本變了導(dǎo)致 API 不兼容。這時(shí)候看構(gòu)建報(bào)錯的堆棧定位到具體是哪個包拋的錯然后查它的 changelog 找破壞性變更。還有一種比較隱蔽的情況某些包在運(yùn)行時(shí)動態(tài)加載靜態(tài)搜索搜不到。比如通過字符串拼接的require或者配置文件里寫的包名。這種只能靠完整的回歸測試來覆蓋所以刪除依賴后一定要跑全量測試不能只跑單元測試。5.2 npm install 變慢或報(bào)錯怎么處理刪除依賴后如果npm install反而變慢通常是鎖文件沒有正確更新導(dǎo)致 npm 在解析依賴樹時(shí)做了多余的工作。解決辦法是刪掉node_modules和package-lock.json重新安裝一次。如果用的是 npm 7 以上版本可以試試npm install --legacy-peer-deps看是不是 peer 依賴沖突導(dǎo)致的。如果報(bào)錯信息里有ERESOLVE說明依賴樹里有版本沖突。這時(shí)候不要盲目加--force而是用npm ls 沖突的包找到?jīng)_突源頭然后決定是升級還是降級。我遇到過一次ERESOLVE追下去發(fā)現(xiàn)是兩個包分別依賴了同一個庫的不同大版本最后通過升級其中一個包解決了。5.3 如何判斷一個包是否真的可以刪我總結(jié)了一個簡單的判斷清單你可以對照使用判斷維度可以刪的信號需要保留的信號維護(hù)狀態(tài)官方標(biāo)記 deprecated 或兩年無更新近期有活躍提交功能覆蓋運(yùn)行時(shí)或標(biāo)準(zhǔn)庫已原生支持功能獨(dú)特?zé)o替代使用頻率全局搜索引用少于 3 處核心鏈路大量使用體積占比壓縮后超過 20KB 且可替代體積小且無替代安全記錄有未修復(fù)的高危漏洞無已知漏洞按這個清單過一遍大部分“僵尸依賴”都能識別出來。我的經(jīng)驗(yàn)是一個項(xiàng)目里真正必需的依賴通常不超過直接依賴總數(shù)的一半剩下的要么是歷史遺留要么是可以用更輕量方案替代的。5.4 刪除依賴后如何驗(yàn)證沒有副作用驗(yàn)證分三層。第一層是靜態(tài)檢查跑eslint和tsc --noEmit確保沒有類型錯誤和未解析的導(dǎo)入。第二層是單元測試和集成測試覆蓋核心業(yè)務(wù)邏輯。第三層是端到端測試模擬真實(shí)用戶操作路徑。如果項(xiàng)目沒有端到端測試至少要在預(yù)發(fā)環(huán)境手動過一遍關(guān)鍵流程。我通常會列一個檢查清單包括登錄、核心功能、支付如果有、數(shù)據(jù)展示等逐項(xiàng)確認(rèn)。另外觀察錯誤監(jiān)控平臺一到兩天看有沒有新增的異常。這些步驟看起來繁瑣但比起線上出故障再回滾成本低得多。6. 把依賴清理變成常規(guī)動作依賴清理不應(yīng)該是一次性的運(yùn)動而應(yīng)該變成常規(guī)動作。我現(xiàn)在每個季度會花半天時(shí)間做一次依賴審查流程固定下來之后其實(shí)很快。先跑npm outdated和npm ls --all | grep deprecated把候選名單列出來然后按上面的判斷清單逐個評估能刪的刪能升級的升級。這樣做的好處是每次改動量都不大風(fēng)險(xiǎn)可控不會像攢了三年再一次性清理那樣牽一發(fā)而動全身。而且依賴樹保持精簡之后npm install快、構(gòu)建快、安全掃描干凈整個開發(fā)體驗(yàn)都會好很多。最后分享一個我常用的小技巧在package.json里加一個engines字段明確聲明項(xiàng)目支持的 Node 版本范圍。這樣團(tuán)隊(duì)成員和 CI 環(huán)境都會用統(tǒng)一的運(yùn)行時(shí)避免因?yàn)?Node 版本差異導(dǎo)致某些 polyfill 包“看起來需要、實(shí)際上不需要”的誤判。配合.nvmrc文件新同學(xué)克隆項(xiàng)目后一條nvm use就能對齊環(huán)境省去很多溝通成本。