
1. 为什么现在敢删包了2026 年前端基建的底层变化过去几年前端项目里塞满了一堆“历史遗留”依赖很多人不是不想删而是不敢删。删了怕构建挂、怕兼容炸、怕同事半夜打电话。但到了 2026 年情况已经发生了根本性变化有几个底层条件成熟了才让“删包”这件事从冒险变成了常规操作。第一个变化是运行时和浏览器能力的大幅补齐。以前我们装core-js、regenerator-runtime是因为老浏览器不支持Promise、async/await、Array.prototype.flat这些。现在主流浏览器的基线已经非常统一ES2020甚至ES2022的特性在绝大多数用户环境里都是原生支持的。你打开caniuse看一眼就知道很多 polyfill 的覆盖率已经高到没必要再打包进去。第二个变化是构建工具的成熟。Vite、Rspack、Turbopack 这些工具在 2026 年已经非常稳定它们对package.json的依赖解析、tree-shaking、副作用标记都做得比五年前好太多。以前删一个包可能导致整个 chunk 报错现在构建工具会明确告诉你哪个模块缺失、哪条 import 路径断了排查成本大幅降低。第三个变化是Node.js 自身的标准库增强。node:test、node:util、node:fs/promises、fetch这些内置能力已经足够覆盖很多以前必须靠第三方包才能做的事。比如以前你要装node-fetch、rimraf、mkdirp现在 Node 原生就能干。第四个变化是社区共识的转移。以前大家习惯“能装包就装包”现在越来越多团队开始推行“零依赖优先”策略。尤其是工具链层面的包维护者自己都在推迁移方案比如request早就废弃了node-sass被sass取代tslint被eslint取代。你不删反而是在背技术债。所以这篇文章不是教你“为了删而删”而是帮你识别哪些包在 2026 年已经完成了历史使命继续留着只会增加安装体积、安全审计噪音和升级负担。下面我会逐个拆解 5 个典型包讲清楚它们为什么可以删、删了之后用什么替代、以及删的过程中容易踩哪些坑。提示删包之前一定要先跑一遍完整的构建和测试流程确认当前项目没有隐式依赖这些包的副作用。很多包被删后报错不是因为功能缺失而是因为某处代码偷偷用了它的全局注入。2. 第一个该删的包core-js 的按需引入策略已经过时2.1 core-js 到底在做什么core-js是前端最经典的 polyfill 库它给老浏览器补上了Promise、Symbol、Map、Set、Array.from、Object.assign这些现代 API。在 Babel 主导的年代babel/preset-env配合core-js3几乎是标配useBuiltIns: usage一开Babel 会自动帮你按需引入需要的 polyfill 模块。但问题在于这套机制在 2026 年已经显得非常笨重。core-js本身有几百个内部模块即使按需引入构建产物里仍然会带上一堆你根本用不到的检测代码。而且它的版本更新非常频繁每次升级都要重新验证兼容性维护成本不低。2.2 为什么现在可以删核心原因是浏览器基线已经足够高。根据 2026 年的主流浏览器支持情况ES2020的所有特性在 Chrome、Edge、Firefox、Safari 的最新几个大版本里都是原生支持的。你如果做的是面向现代浏览器的项目根本不需要为Promise、async/await、可选链、空值合并这些语法准备 polyfill。另一个原因是构建工具已经内置了更聪明的降级方案。Vite 的build.target和 Rspack 的target配置可以直接指定目标环境构建时会自动处理语法降级不需要你再手动引入core-js。对于确实需要兼容老环境的项目也可以只针对特定 API 做局部 polyfill而不是全量引入。2.3 删掉之后怎么替代如果你只是需要语法降级保留babel/preset-env或esbuild的 target 配置就够了。如果确实需要某个特定 API 的 polyfill比如Array.prototype.at可以直接在入口文件里手写一个几行的实现或者只引入core-js/actual/array/at这种单模块路径而不是整个core-js。// 以前的做法全量引入 import core-js/stable; import regenerator-runtime/runtime; // 现在的做法按需单模块引入或者干脆不引入 if (!Array.prototype.at) { Array.prototype.at function (index) { const len this.length; const relativeIndex index 0 ? index : len index; return this[relativeIndex]; }; }2.4 删包时的注意事项删core-js之前先检查babel.config.js或.browserslistrc里有没有配置useBuiltIns。如果有要同步改掉否则 Babel 会继续尝试注入 polyfill 但找不到包构建直接报错。另外检查package.json里有没有regenerator-runtime它通常和core-js一起出现删的时候要一起处理。还有一个容易忽略的点有些第三方库内部依赖了core-js你删掉顶层依赖后npm 会自动把它提升到node_modules的嵌套层级里构建时仍然可能被打进去。这时候可以用npm ls core-js看一下依赖树确认没有其他包在引用它。3. 第二个该删的包node-sass 已经被 sass 完全取代3.1 node-sass 的历史包袱node-sass是早期 Sass 编译的主流方案它基于 LibSass 的 C 实现性能不错但有一个致命问题它和 Node.js 版本强绑定。每次 Node 大版本升级node-sass都要重新编译二进制文件Windows 用户经常遇到node-gyp编译失败、Python 版本不匹配、Visual Studio 构建工具缺失等问题。更麻烦的是node-sass在 2020 年之后基本停止维护LibSass 也被官方标记为 deprecated。继续用它等于把一个已经停止更新的 C 依赖留在项目里安全审计和升级都会很痛苦。3.2 sass 的纯 JS 实现已经足够快sass包以前叫dart-sass是 Dart 写的纯 JS 实现不依赖本地编译安装即用。它的编译速度在 2026 年已经优化得非常好对于绝大多数项目来说和node-sass的性能差距可以忽略不计。而且它支持最新的 Sass 语法包括use、forward、模块系统这些是node-sass根本不支持的。3.3 迁移步骤和配置调整迁移其实很简单把package.json里的node-sass换成sass然后检查构建配置。如果你用的是 Webpacksass-loader需要确认版本老版本的sass-loader默认找node-sass新版本会优先找sass。# 卸载旧包 npm uninstall node-sass # 安装新包 npm install -D sass # 检查依赖树确认没有残留 npm ls node-sass// webpack.config.js 里的 sass-loader 配置 module.exports { module: { rules: [ { test: /\.s[ac]ss$/i, use: [ style-loader, css-loader, { loader: sass-loader, options: { // 新版本 sass-loader 会自动使用 sass 包 implementation: require(sass), }, }, ], }, ], }, };3.4 迁移后的验证要点迁移完成后重点检查几个地方一是import语句是否还能正常工作虽然sass仍然支持import但官方推荐迁移到use二是除法运算node-sass和sass对/的处理有差异新版本要求用math.div()三是自定义函数和 mixin 的兼容性大部分情况下没问题但涉及底层 API 的地方需要测试。注意如果你的项目里用了node-sass的fibers选项来加速同步编译迁移到sass后这个选项不再支持需要改成异步编译或者用sass-embedded包。4. 第三个该删的包rimraf 和 mkdirp 已被 Node 原生能力覆盖4.1 这两个包以前解决什么问题rimraf是一个跨平台的递归删除工具因为 Windows 的rm -rf命令和 Unix 不一样Node 早期的fs.rmdir也不支持递归删除非空目录。mkdirp则是递归创建目录因为fs.mkdir早期也不支持recursive: true。这两个包在构建脚本里非常常见比如clean: rimraf dist、prebuild: mkdirp dist/assets。它们本身很小但属于典型的“历史补丁”现在 Node 已经原生支持了。4.2 Node 原生 API 的对应关系Node.js 从 14.14 开始fs.rm支持recursive: true和force: true可以完全替代rimraf。fs.mkdir从 10.12 开始支持recursive: true可以替代mkdirp。到了 2026 年这些 API 已经非常稳定没有任何理由继续用第三方包。// 以前用 rimraf const rimraf require(rimraf); rimraf.sync(dist); // 现在用 Node 原生 const fs require(fs); fs.rmSync(dist, { recursive: true, force: true }); // 以前用 mkdirp const mkdirp require(mkdirp); mkdirp.sync(dist/assets); // 现在用 Node 原生 fs.mkdirSync(dist/assets, { recursive: true });4.3 在 package.json 脚本里直接替换如果你只是在 npm scripts 里用这两个包可以直接改成 Node 的-e执行方式连脚本文件都不用建。{ scripts: { clean: node -e \fs.rmSync(dist, { recursive: true, force: true })\, prebuild: node -e \fs.mkdirSync(dist/assets, { recursive: true })\ } }4.4 删包时的边界情况有一个细节需要注意rimraf在处理符号链接和权限问题时有一些特殊逻辑比如它默认会重试删除失败的文件。Node 原生的fs.rmSync在遇到被占用的文件时会直接抛错。如果你的构建环境里有文件锁或者杀毒软件干扰可能需要加一个重试逻辑。function safeRm(dir, retries 3) { for (let i 0; i retries; i) { try { fs.rmSync(dir, { recursive: true, force: true }); return; } catch (err) { if (i retries - 1) throw err; // 等待一小段时间后重试 const start Date.now(); while (Date.now() - start 100) {} } } }另外mkdirp有一个mode选项可以设置目录权限Node 原生的fs.mkdirSync也支持mode参数迁移时如果原来有权限配置记得同步过去。5. 第四个该删的包node-fetch 在 Node 18 里已经多余5.1 node-fetch 的定位node-fetch是一个把浏览器fetchAPI 带到 Node.js 的包。在 Node 18 之前Node 没有内置fetch所以服务端发 HTTP 请求要么用http模块手写要么用axios、request、node-fetch这些第三方库。node-fetch因为 API 和浏览器一致在前端转全栈的团队里特别受欢迎。5.2 Node 原生 fetch 的成熟Node 18 开始内置了fetch基于 undici 实现性能比node-fetch更好而且 API 完全兼容。到了 2026 年Node 的 LTS 版本早就过了 18原生fetch已经是默认能力。你直接在 Node 脚本里写fetch(https://api.example.com)就能跑不需要任何 polyfill。// 以前用 node-fetch const fetch require(node-fetch); async function getData() { const res await fetch(https://api.example.com/data); return res.json(); } // 现在直接用原生 fetch async function getData() { const res await fetch(https://api.example.com/data); return res.json(); }5.3 迁移时的差异点虽然 API 基本一致但有几个细节需要注意。node-fetch的Response.body是一个 Node Stream而原生fetch的body是一个 Web Stream如果你用了.pipe()或者stream.pipeline()需要改成 Web Stream 的写法。另外原生fetch默认不跟随重定向的某些行为可能和node-fetch有细微差别涉及重定向的逻辑要测试一下。// node-fetch 的流式处理 const res await fetch(url); res.body.pipe(fs.createWriteStream(file.txt)); // 原生 fetch 的流式处理 const res await fetch(url); const fileStream fs.createWriteStream(file.txt); await stream.Readable.fromWeb(res.body).pipe(fileStream);5.4 什么情况下还需要保留如果你的项目需要兼容 Node 16 或更早版本那node-fetch还有存在价值。但 2026 年还在用 Node 16 的项目本身就是一个更大的问题。另外如果你需要node-fetch的一些高级特性比如自定义 agent、代理配置原生fetch通过dispatcher选项也能实现只是写法不同。提示删掉node-fetch后检查一下package.json里有没有form-data、whatwg-url这些它的依赖包它们通常也会一起被清理掉。6. 第五个该删的包tslint 早已被 eslint 生态吸收6.1 tslint 的终结tslint是 TypeScript 早期的官方 lint 工具但在 2019 年就被官方标记为 deprecated推荐迁移到eslint。到了 2026 年tslint已经彻底停止维护它的规则集也基本被typescript-eslint覆盖。继续留在项目里只会让编辑器插件冲突、规则重复、升级困难。6.2 迁移到 eslint 的核心步骤迁移tslint到eslint不是简单换个包需要重新配置规则。typescript-eslint提供了tslint-to-eslint-config工具可以自动转换大部分规则但转换后仍然需要手动调整。# 安装迁移工具 npx tslint-to-eslint-config # 安装 eslint 相关依赖 npm install -D eslint typescript-eslint/parser typescript-eslint/eslint-plugin// eslint.config.js扁平配置2026 年主流写法 import tseslint from typescript-eslint/eslint-plugin; import tsparser from typescript-eslint/parser; export default [ { files: [**/*.ts, **/*.tsx], languageOptions: { parser: tsparser, parserOptions: { project: ./tsconfig.json, }, }, plugins: { typescript-eslint: tseslint, }, rules: { typescript-eslint/no-unused-vars: warn, typescript-eslint/no-explicit-any: warn, // 其他规则按需配置 }, }, ];6.3 规则映射中的常见坑tslint和eslint的规则命名不一样比如tslint的no-unused-variable对应eslint的typescript-eslint/no-unused-vars。有些规则在eslint里被拆成了多个有些则合并了。自动转换工具能处理大部分但像member-ordering、typedef这种复杂规则转换后往往需要手动重写。另外tslint支持--fix自动修复eslint也支持但两者的修复范围不同。迁移后建议先跑一遍eslint --fix然后人工 review 改动避免自动修复引入意外变更。6.4 删掉 tslint 后的清理工作删掉tslint包后记得删除tslint.json配置文件以及package.json里的tslint相关脚本。如果项目里用了tslint-react、tslint-config-prettier这些扩展也要一并清理。编辑器方面VS Code 的 TSLint 插件需要禁用改用 ESLint 插件。旧包替代方案迁移难度主要风险core-js浏览器原生 按需 polyfill中Babel 配置残留导致构建报错node-sasssass低语法差异和 fibers 选项失效rimraf/mkdirpNode 原生 fs API低文件锁和权限边界情况node-fetchNode 原生 fetch低Web Stream 和 Node Stream 差异tslinteslint typescript-eslint高规则映射和自动修复范围差异7. 删包之后怎么验证没有把项目搞崩删包不是删完就完事验证环节才是真正体现经验的地方。我见过太多人删完包直接提交结果 CI 挂了才发现问题。下面是我自己常用的验证流程按顺序走一遍基本能覆盖 95% 的坑。7.1 依赖树完整性检查第一步永远是看依赖树。删掉顶层包之后用npm ls 包名确认没有其他包在引用它。如果输出里有UNMET DEPENDENCY或者invalid说明有包在依赖一个已经不存在的模块需要处理。# 检查单个包 npm ls core-js npm ls node-sass npm ls node-fetch # 检查所有依赖的完整性 npm ls --all 21 | grep -i unmet\|invalid7.2 构建产物对比删包前后各跑一次构建对比产物大小和 chunk 结构。如果删了包之后产物反而变大说明构建工具把某些依赖提升到了主 chunk 里需要检查optimizeDeps或splitChunks配置。# 构建并输出分析 npm run build # 如果用了 vite可以用 rollup-plugin-visualizer npx vite build --mode production7.3 运行时冒烟测试构建通过不代表运行时没问题。重点测试几个场景页面首次加载、路由切换、异步数据请求、表单提交、错误边界。如果项目有 E2E 测试跑一遍是最稳妥的。没有的话手动点一遍核心流程。7.4 CI 环境的特殊处理本地能跑不代表 CI 能跑。CI 环境通常是干净安装npm ci会严格按照package-lock.json安装。删包后一定要更新 lock 文件否则 CI 可能装到旧版本或者报错。# 删除 node_modules 和 lock 文件后重新安装 rm -rf node_modules package-lock.json npm install # 或者用 npm ci 验证 lock 文件一致性 npm ci注意如果项目用了 monorepo 或者 workspace删包的影响范围会更大。npm ls在 workspace 里默认只查当前包需要用--workspaces参数查全部。8. 我踩过的几个真实坑和最后的建议说几个我自己在删包过程中真实踩过的坑都是文档里不会写的。第一个坑是隐式全局注入。有些包在被引入时会偷偷往global或window上挂东西比如老版本的core-js会注入_babelPolyfill标记。你删掉包之后某段代码里判断window._babelPolyfill的逻辑就失效了导致按需加载的 chunk 重复执行。这种问题构建不会报错只有运行时才能发现。第二个坑是类型声明的残留。删掉node-fetch后types/node-fetch可能还在devDependencies里TypeScript 编译时仍然能找到类型定义但运行时模块不存在。这种“类型通过、运行报错”的问题特别隐蔽建议删包时把对应的types/*一起清理。第三个坑是锁文件版本漂移。删包后如果只跑了npm install而没有重新生成 lock 文件某些间接依赖的版本可能会被提升到不兼容的版本。我的习惯是删包后直接删掉node_modules和package-lock.json从头装一遍确保依赖树是干净的。最后一个建议不要一次性删完所有包。哪怕你确定这 5 个包都能删也建议分批次来每次删一个跑完验证流程再删下一个。这样出问题时能快速定位是哪个包引起的而不是在一堆变更里大海捞针。删包这件事稳比快重要。