做了十多年前端,我最后学会了删代码

2026 年 3 月的一个下午,我提交了一个 commit:删除三千一百四十七行,新增二百零六行。CI 跑完,测试全绿,构建产物小了百分之九点三,首屏快了大约一百八十毫秒。我在工位上又坐了一会儿,没等来想象中的畅快,反而有点难过。那三千多行里,有两千多行是我自己写的:有的是三年前写的,有的是去年写的,每一行在写下的时候,看起来都那么有必要。

那是我做前端的第十四个年头。回头看,前面十来年我一直在做加法:加一个工具函数,加一层抽象,加一个开关,加一段兼容逻辑,加一套新的状态管理,加一层微前端容器。我以为这就是专业,是经验,是资深工程师该有的手感。直到某个时刻我才承认,我把一个本来跑得挺好的系统,养成了一间堆满东西的储藏室——你不是不能用,但你每次进去找东西,都要先挪开一堆不知道是谁放的东西。

这篇文章想说的其实只有一件事:前端做到后面,真正的分野不在于你能写出多少代码,而在于你敢删掉多少代码。而删,比写难得多——难在技术上,也难在心理上、组织上,甚至有点政治。

一、加法的手感,是从哪一年开始变坏的

我记得刚开始写页面那几年,做加法是有快感的。那时候一个页面几十行 jQuery,写完就能跑,用户真的会用。后来组件化来了,我们把界面切成一块一块,每一块都取个名字,那种「把混乱变成秩序」的感觉非常爽。再后来,工程化来了,构建、打包、转译、热更新,每一层都在解决真实的问题,也都在你的项目里留下一层新的东西。

问题就在这个「留下一层新的东西」上。我们非常擅长引入,非常不擅长退场。新的方案来了,旧的方案不会走,它只是从主角变成了配角,继续待在仓库里,继续参与构建,继续在升级依赖时把你绊一跤。

我见过一个项目的 webpack 配置,从最初的三十多行涨到四百多行,又涨到九百多行。每一条配置都有它的来历:为了修一个只在某台机器上出现的路径问题,为了兼容某个已经下线的 CDN,为了绕过某个版本的工具链 bug。这些配置单看都对,合起来就是一份没人敢动的考古地层。

我也见过工具目录从「一个 utils.js」长成十二个文件,里面躺着五个 formatDate。它们格式略有不同,一个处理时区,一个处理相对时间,一个是别人从上一个项目复制过来的。每次有新需求,大家的做法不是去找一个能用的,而是再写第六个——因为找的成本高于写的成本。

我们还特别爱留「以防万一」。设计稿改版了,旧组件不删,注释一句「旧版样式,可能以后还用得上」。功能下线了,入口早就摘掉了,代码留在那里,理由是「万一要回滚」。开关关掉了,逻辑还在,理由是「先留着,别动」。于是「万一」越攒越多,最后变成了必然:你每次改一行,都要先理解一百行跟你无关的东西。

我们习惯把这一切叫做「历史包袱」。但历史不是别人写的,是我们自己一天一天堆出来的。所谓技术债,绝大多数不是借来的,是自己签的字。

二、第一次删代码,手是抖的

我第一次真正意义上的删代码,是在一个已经下线的功能上。那个功能上线八个月,数据一直不好,产品决定关掉。入口摘了,路由删了,但底下那一整套组件、状态、请求封装、埋点、测试用例,全都还在。它们不再被使用,却依然要参与每次依赖升级、每次类型检查、每次构建。

我在那个模块上磨了三天。第一天只是看,看它到底和谁连着;第二天试着注释掉一部分,跑构建,看谁报错;第三天我才敢真正下手。删完之后我发现一件很震动我的事:没有任何人察觉。没有一个人来找我说,「哎,那个东西怎么没了」。这说明它其实很久以前就已经死了,我只是过去八个月一直在给一具尸体化妆。

这件事让我对「删除」这件事的理解彻底变了。删除不是破坏,删除是一种确认:确认什么还活着。你不动它,它就会一直假装自己活着,占用你的注意力、你的构建时间、你的认知带宽。

后来我读到一句话,大意是:园艺的本质不是让植物生长,而是决定什么该剪掉。园艺师不种树,树自己会种自己;园艺师真正的工作,是判断哪一根枝条在抢走整棵树的光。前端工程在很多时候,就是园艺。

三、重复不是最贵的,错误的抽象才是

我们这行有一个共识:重复代码是坏味道。于是大家见到三行相似就抽函数,见到三个页面相似就抽组件,见到三个组件相似就抽一层「通用能力」。抽完之后,代码行数确实变少了,可维护性是不是变好了,却未必。

因为重复是显性债务,你看得见它,也容易衡量它;错误的抽象是隐性债务,它长着一副最佳实践的脸,谁都不好意思反对。

我接手过一个表格组件,它有一份令人尊敬的历史:四十七个 props。支持行内编辑、支持虚拟滚动、支持树形展开、支持列拖拽、支持列宽持久化、支持服务端排序、支持本地排序、支持空态、支持加载态、支持错误态,还支持三种完全不同的主题。它被用在全站四十七个地方,而其中四十六个地方,只用了其中的三到五个 props。

你要为一个新需求改它,就得先读懂那四十七个 props 互相之间的约束关系;你要为一个老需求修 bug,就担心改动会不会影响另外四十六处。它看起来是个抽象,实际上是一个只有作者本人理解的配置系统。作者早就离职了。

类似的还有「统一请求层」。为了兼容三种后端风格,它包了三层,每层都在做一点点转换,最后谁也说不清一个请求到底走了哪条路。还有「通用表单引擎」,配置驱动、JSON 描述、动态渲染,一开始确实很优雅,后来每个人都在绕过它,因为总有一个需求它表达不了,而改它的风险高于自己写一个页面。

我现在对抽象有一条很朴素的规则:重复出现三次,才考虑提取;而提取之前,先问一句,能不能把其中两处直接删掉。因为在很多情况下,三处相似代码之所以相似,恰恰是巧合;而抽象一旦建立,它就把后来所有人的手脚都绑在了一个当下的假设上。

抽象的成本不在于写起来多难,而在于它替你做了一个关于未来的判断,而这个判断通常是错的。

四、我们为什么不敢删

如果说删代码只是技术问题,那它早就被解决了。真正让它难的,是四个层面的东西。

第一层是激励。写代码有产出,删代码没有。周报里你写「本周新增用户中心模块」,看起来就是有进展;你写「本周删除两千行无效代码」,别人会问你这两千行是怎么来的。看板上永远是加法好看,删除是隐形的。我们被考核什么,就会优化什么。

第二层是恐惧。万一以后要用呢?这个「万一」听起来很谨慎,实际上很不理性。留着它的成本是确定发生的:它要参与构建,要跟着框架升级,要在你搜索代码时干扰你,要让新同事多读一遍。而它被重新用上的概率,低到可以忽略。我们用确定的成本,去对冲一个几乎不存在的收益。

第三层是风险不对称。删对了,没人知道;删错了,全公司都知道。这种不对称让理性的人也会选择不动。在一个把「不出事」当作最高评价的环境里,保守是唯一正确的策略,而删除永远是保守的反面。

第四层是身份。这一点很少有人直说。代码是工程师的作品,我们对自己的代码有感情。删掉自己写的东西,某种程度上等于承认当年的判断不够好。我删那三千行的时候,最难过的不是损失,而是我知道里面有一半是我的自以为是。承认这一点,比写新代码难多了。

还有第五层,是组织的:代码是公共的,责任是私人的。每个模块都有一个主人,但没有人对整体负责。你不敢删别人的模块,别人也不想让你删他的。于是所有人的东西都留着,系统就一直在变胖。

五、可删性,是一种被严重低估的架构能力

想清楚上面这些之后,我慢慢形成了一个也许有点反常的判断:一段代码写得好不好,一个重要标准是它好不好删。

好删的代码,通常具备几个特征。它的边界清晰,你对它负责什么、不负责什么心里有数。它的副作用少,删掉它不会在别的地方冒出一堆隐藏影响。它的依赖是单向的,你不需要顺着十几条线去解一个结。它的状态是集中的,不会有某个模块的数据在别处被悄悄修改。它没有隐式的全局约定,不靠「大家都知道要在这里先调一下那个函数」运行。

反过来,那些你一碰就崩的系统,往往不是因为它太重要,而是因为它太纠缠。重要和纠缠完全是两件事,但它们在现实里经常被混为一谈:因为改不动,所以显得重要;因为显得重要,所以更不敢改。这个循环一旦形成,系统就进入了一种只能加不能减的状态。

可删性的前提是理解。你无法安全地删除一个你不理解的东西——你可以删掉它,但你无法确认你删对了。所以删除其实是一种理解力的测试。当你能把一段代码干净地删掉,并且解释清楚为什么它不该存在,你才真正理解了它在系统里的位置。

从这个角度看,重构的最高形式不是重写,而是消失。

六、我现在的删代码清单

说了这么多观点,落到操作上,我大概会按这样的顺序过一遍一个老项目。

先找死代码。入口不存在的功能,永远不会为真的开关,路由表里已经摘掉但模块还留着的页面。这部分最安全,因为它已经不参与任何真实路径。

再找过期兼容。目标浏览器矩阵之外的 polyfill,早就修好的框架 bug 的 workaround,为了某个已经下线的第三方服务写的降级逻辑。判断标准很简单:把这段删掉,会有人受影响吗?如果那个「人」是 2019 年的某个浏览器,那就可以走。

然后是重复。三处以上的相同逻辑才考虑提取;而在提取之前,先看能不能直接把其中两处删掉——很多所谓重复,其实是被复制过来的历史,源头上那个需求已经没了。

接着是抽象。我给自己定了一条不算严谨但很好用的线:如果一个组件或者一个工具的配置项超过十个,它就不再是抽象,而是配置系统。配置系统本身没错,但你要承认它的性质变了,它的维护者必须专职,而不是「顺手维护一下」。如果没人专职维护,那它就是在借未来的债。

再往后是状态。能被推导出来的状态不要存。两份状态一定会不同步,只是时间问题。每次看到「这里也存了一份,为了性能」,我都会先问它的性能收益是不是真的被量过。

然后是依赖。每加一个包之前,我都会问两个问题:它能省下我多少行自己写的代码,它又会给我带来多少我不能控制的东西。这个问题的答案,在 2026 年比在 2019 年更重要,因为依赖树已经深到没有人能完整读过一遍。

最后,注释掉的代码,删掉。git 记得比你的注释清楚得多。

工具层面也有几个很实用的手段:用覆盖率报告找出从来没有被执行过的分支;用构建产物分析看包体积排名前二十的模块,挨个问它为什么这么大;用 git log 找那些半年没人改、却每次升级都要被迫改动一次的文件,那些通常就是该走的东西。

七、2026 年,写代码变便宜了,读代码没有

最近这两年,最明显的变化是:生成代码的成本正在趋近于零。你描述一段需求,几秒钟就能得到一份可运行的实现,样式、类型、测试,一应俱全。这在五年前是不可想象的。

但它带来一个副作用:代码总量的通胀。生成变快了,理解没有变快。人的阅读速度、审查速度、建立心智模型的速度,基本没变。于是瓶颈从「写不出来」转移到了「读不过来」。我们以前担心的是产能不足,现在担心的是产能过剩——过量的、看似正确的、没有人真正理解其存在理由的代码,堆在仓库里。

生成式工具最擅长的是加法。它不知道你的历史,不承担维护责任,也无法判断这段东西三年后还会不会被需要。它给的答案往往在语法上无可指摘,在语境上一无所知。所以在这个时代,工程师最稀缺的能力不再是「能不能写出来」,而是「能不能判断这段该不该存在」。

我甚至觉得,未来几年前端团队最重要的角色,可能不是写得最快的那个人,而是最会做减法的那个人。他能在一堆都能跑的方案里,选出那个将来最容易被删掉的。这不是保守,这是远见。

因为技术复杂度从来不是工具带来的。工具一直在进步,构建从几分钟变成几百毫秒,框架越来越聪明,类型系统越来越强。但项目有没有因此变简单?大体上没有。复杂度是我们自己带进来的,也只能由我们自己带出去。

八、删代码是一种组织能力

个人删代码的天花板很低。你把自己负责的模块删干净了,隔壁模块还堆着;你今天删了,下个季度又长回来。所以这件事到最后,是一个组织问题。

我见过做得好一些的团队,有几个共同做法。一是把删除写进考核,技术债看板上不只看「待还」,也看「已还」和「净减少」。二是把清理变成有仪式感的固定动作,比如每个季度留出两天,专门做删除,不接新需求。三是在 code review 里,把「这段能不能不写」当成一个和「这段有没有 bug」同等重要的问题。四是明确一个对整体负责的人,而不是每个模块都有主人、整体却无人认领。

当然,也有边界。删代码不是越少越好,不是为了删而删。有些复杂度是业务本身带来的,你删不掉,删了就是错。诚实比简洁更重要:承认一个系统就是复杂,比把它伪装得简单要负责得多。删除的目的是让复杂度对应真实的需求,而不是让代码看起来漂亮。

九、我最后学会的,其实不是删代码

回到 2026 年 3 月那个下午。那个 commit 合并之后,我并没有立刻进入下一个需求,而是把删掉的东西在脑子里过了一遍。我终于确认了一件事:我理解了这套系统里什么还活着,什么早就死了。

十几年里,我学过很多东西。从手写 DOM 到 jQuery,从 jQuery 到组件化,从组件化到工程化,从工程化到全栈,从全栈到现在的生成式辅助。每一代技术都会过时,每一套 API 都会被替代。唯一能留下来的,是判断力:分清什么该留、什么该走,什么是因为需要而存在,什么只是因为习惯而存在。

写代码是一种表达,删代码是一种判断。年轻的时候我们急着证明自己能写,写得越多越有安全感;后来才慢慢明白,克制比才华更难。你能加多少东西,说明你有多大的能力;你能减掉多少东西,说明你有多深的理解。

所以,如果你也做了很多年前端,也接手了一个越写越大的仓库,我建议你在下一次打开文件的时候,先别急着加。先问一句:这里有什么是可以删的?

如果答案是「有」,那大概率,那就是你今天最值钱的一次提交。