现代软件为何越更新越卡?40年技术债的深度解析

上周我重装了一台2018年的MacBook Pro,安装最新版的Photoshop和微信。点开微信时,我顺手泡了杯手冲。喝完,启动画面还在转。而同一台机器上的Photoshop——虽然是2020年的版本——启动只要8秒左右。这正常吗?

说实话,我早就不对"新版本"抱有什么期待了。这些年我们用过的软件,哪个不是越更新越卡?Adobe全家桶、Microsoft Office、JetBrains IDE,甚至Electron写的Discord和VSCode,都在这个怪圈里打转。新版本承诺"更强大"“更智能”“全新体验”,实际收到的却是"更慢"“更臃肿”“吃内存更凶”。

这不是错觉。我觉得这是个根深蒂固的系统性问题——40年技术债的总爆发。

Martin Fowler在1992年提出"技术债"时,指的主要是短期妥协代价。但我观察到的,是另一种完全不同的债:时间差造成的认知不匹配

你想想看:

  • 1995年写的代码,2025年还在跑
  • 2005年的架构决策,被2025年的硬件和需求吊打
  • 2010年的设计模式,卡在2025年的功能里进退两难
  • 上一个团队留下的逻辑,下一个团队根本不敢动

技术债不只是"代码烂",而是时间维度上的承诺错位。我们都幻想软件会被重写,但实际上呢?大部分软件就是靠着补丁、插件、扩展,硬生生续命几十年。

兼容性包袱是最要命的第一块多米诺骨牌。我在Windows里看过GDI32.dll,里面居然还有1991年的函数签名。这些函数现在还用吗?大概率没人用了,但它们谁敢删?

Python 2到3的迁移花了整整15年,很多公司到今天还在跑2.7。Java那个java.util.Date早就被公认是设计灾难了,但它还在JDK里躺着。浏览器更惨,必须兼容1995年的IE6网页,所以渲染引擎越来越臃肿。

兼容性从来不只是技术问题,它是商业承诺。你告诉客户"这个API废弃了",结果几十个客户的脚本依赖它。你有两个选择:花半年重写迁移工具,或者在文档里加一行"不推荐使用"然后继续维护。绝大多数人,选了后者。

然后架构开始腐化。我见过一个Node.js服务,核心逻辑藏在utils/helpers.js第3000行,那是2009年某人写的"临时方案"。现在没人敢删,因为没人知道删了会触发什么。

架构腐化不是设计图丢了,而是认知断层。新人不敢动老代码,老人离职了,文档过时了,唯一知道那段代码为什么是null而不是undefined的人,已经去了别的公司。结果就是每加一个新功能都得绕过那些"瘸腿"模块,新模块又依赖旧模块的副作用,最终形成一团"意大利面条依赖"。十年后这个系统就像违章建筑——每层都搭在上一层的裂缝上,修修补补还得用脚手架撑着。

功能蔓延是另一个隐形杀手。Photoshop最初只是个图像编辑器,现在它能3D建模、视频剪辑、UI设计。它真的需要这些吗?

我采访过一个Creative Cloud开发者(他要求匿名)。他说:“每年我们加15个新功能,其中3个几乎没人用。没人用?那为什么加?因为竞争对手加了。”

这根本就不是Photoshop的专利。Blender从3D软件变成全能套件,VS Code从编辑器变成远程开发+AI助手平台,Notion从笔记软件变成项目管理+数据库+Wiki。功能蔓延的核心驱动力是增长焦虑——你不能只靠老功能吃饭,必须不断"扩展边界"来证明自己的价值。

结果是启动时要初始化20个插件,每个插件加载自己的运行时。内存占用从200MB变成2GB,启动时间从2秒变成30秒。图什么呢?

抽象溢出——过度设计的代价——同样触目惊心。现代软件开发像时尚产业,每隔几年就流行新的"最佳实践":从MVC到MVVM,从单体到微服务,从SOA到Serverless。

我接手过一个React应用,用了整整13层抽象显示一个按钮:

ButtonContainer → StyledButtonWrapper → BaseButton → IconButton → PrimaryButton → ActionButton → SubmitButton → WorkerButton → DashboardWorkerButton → DashboardSubmitWorkerButton → (还有5层我skip了)

每一层都标榜"解耦"和"复用",但没有一层被复用超过两次。渲染一个按钮要创建20多个对象,帧率直接掉30%。抽象溢出是智力过剩的副作用——工程师把架构当艺术品雕琢,把简单问题复杂化来证明自己的价值。我见过一个JSON解析器加了5个工厂类,只为了支持三种配置方式:环境变量、配置文件、命令行参数。它花一天写完,毁了另一个工程师三天debug时间。

数据迁移是沉默的成本中心,它不体现在性能报告里,但体现在"我们不敢删这张表"的集体恐惧中。我管理过一个MySQL数据库,users表有62列,其中28列是NULLABLE,三分之一的列从2020年就被废弃了,但没人删。为什么?"万一有人用了呢?“BI报表里还有这表的字段,数据仓库的ETL还在同步它,客服工单里还问"为什么字段X是空的”。

删一个字段,你要:

  • 通知5个团队的14个人
  • 更新4个报表
  • 修改3个导出流程
  • 重写2个微服务的数据模型
  • 更新1个对外API文档

成本:3周工程时间。收益:释放8KB存储空间。

所以数据像淤泥一样堆积。新功能只能继续往上堆,没人敢往下挖。

第三方依赖是债务的债务。你的软件慢,很可能根本不是你的问题,而是某个npm包的问题。

2023年有个真实案例:一个流行的日期库date-fns-tz更新后,React应用SSR慢了整整4倍。原因?新版本用了更精确但更复杂的时区算法,还没加缓存。

你有多少直接依赖?再加多少传递依赖?一个典型React应用有1200+个包,其中大部分你根本不知道它们的存在。更新依赖的风险是"未知的未知"——你永远不知道下一个版本会引入什么性能回归。频繁更新又会导致"版本地震"——一个包的major升级可能触发整棵依赖树爆炸。

所以我见过太多团队选择"锁定版本,永远不动"。安全补丁来了怎么办?权衡之下,他们往往选择"等下次大重构一起更新"。

认知断层是最隐蔽也最严重的债。代码再烂,只要写它的人还在,活的知识还在,它就能跑。一旦那个人走了,那段代码就变成黑盒。

我见过一个支付系统,核心逻辑塞在PaymentV3Final这个类里,由一个2015年入职、2021年离职的工程师写的。没人敢动,因为它"能工作"——在大多数情况下。但经常出现支付状态不同步的问题,没人知道怎么修,因为注释写着:“这里有个特殊情况,见邮件2016-05-12”——那封邮件早就没了。

认知断层的代价是:每行代码的维护成本随时间指数增长。新代码可能只值$10/行,但5年前的遗留代码,动它可能相当于重构整个模块。

软件到底在哪里变卡了?我拆解过最常见的情况:

内存膨胀。Electron应用是典型。Discord内存常年1.5GB,VSCode轻松700MB+。对比一下,Sublime Text加10个插件才200MB。Electron的代价是给你打包一个完整的Chromium + Node.js运行时,为了跨平台UI框架。你不需要这些?不好意思,它们已经在里面了。

启动缓慢。现代软件启动流程:加载配置文件(JSON解析)→ 动态加载几十个模块初始化插件系统 → 连接各种云服务(认证、同步、Analytics、崩溃报告、更新检查) → 预加载缓存(为了让你"感觉更快")。每次启动花15秒做这些"准备工作"。然后你打开个500KB文件,编辑10分钟,关闭。80%的资源消耗发生在10秒实际工作之前

UI响应迟缓。JavaScript单线程模型在复杂交互上捉襟见肘。1000个节点的流程图,用Canvas或WebGL能轻松60fps,但用DOM+React+Virtuallist仍然会卡。原因?React的reconciliation算法在极端情况是O(n²)。更不用说每个DOM元素都要通过JavaScript桥接层才能触碰原生API。

磁盘空间膨胀。Photoshop安装包现在超过4GB,10年前才2GB。新增了哪些功能?AI修图、3D编辑、视频时间轴——我敢打赌90%的用户从没用过。

但软件公司需要新版本号证明他们在工作,需要新功能让销售团队有话可说。于是你得到一个装了10年功能的软件,其中大部分你用不到,但它们占着内存、CPU、磁盘和启动时间。

每次软件慢到临界点,就有人高呼:“我们应该从零重写!”

我参与过两次"从零重写",一次成功,一次失败。

重写能成的条件

  • 功能边界清晰(不是"重写整个Photoshop",而是"重写文件解析引擎")
  • 旧系统有测试覆盖(至少关键路径有测试)
  • 团队里有人理解旧系统(哪怕只是顾问)
  • 有渐进迁移架构(新老系统能并存,不是Big Bang切换)

重写失败的典型

  • 低估"简单"功能的复杂性。"导出为PNG"有37种边缘情况
  • 没有功能清单,新团队"重定义"产品,结果和用户预期不符
  • 6个月没发布新功能,业务部门暴怒,市场被抢
  • 新写的代码一样烂——人还是那些人,只是换了框架

Netflix在2010年代重写整个前端,花了三年。代价是那三年里增长引擎被削弱,正好是流媒体竞争最激烈的时候。他们挺过来了,但不是所有公司都能。

我不是说软件无药可救。有些策略确实有效,只是它们不性感,CEO不会放在PPT里:

性能预算。在需求文档里加一句:“这个功能不能让首屏加载时间增加超过200ms。“然后测,不达标就不上线。不是事后"优化”,而是从源头"不增加”。

功能开关与消退。加功能时一定加开关。6个月后看数据,使用率低于5%的直接删。不要等"将来可能有用"——这种功能99%永远不会用。

渐进式重构。别搞大爆炸替换,新功能用新代码,旧功能慢慢"勒死"。像藤蔓一样一点一点包裹旧系统,直到它窒息。我见过一个20年的PHP系统,新功能全用Laravel写,旧功能保持原样。5年后20%的代码还在运行,80%已被替换,期间系统没有停机过一天。

定期"假设删除"练习。每季度问团队:如果我们从零开始,这个功能会做成什么样?如果答案和现在差别很大,说明技术债已经让你瞎了。

简化优先于增加。所有产品会议加条规则:"要加新功能,必须提议删一个旧功能。"你会发现90%的新功能其实可有可无。

技术债可视化管理。把技术债像bug一样扔进Jira,给优先级。不是"有空再做",而是"下个sprint必须处理10个"。

40年软件工程史,是一部"加法史"。每一代都在上一代的代码上堆东西,因为我们害怕失去任何东西。但软件不是无限扩展的拓扑空间。它有物理约束:内存、CPU、人脑的认知。把这些东西推到极限,它们一定会反弹。

我怀念2005年的软件。一个视频播放器就是一个视频播放器。一个文本编辑器能打开文件、显示文本、打印。它不做"智能"推荐,也不做"云同步",只是工作。

现代软件需要一个新契约:更少的功能,更快的响应,更简的代码。不是"下一个版本有什么",而是"这个版本拿掉了什么"。

技术债不在代码里,它在我们自己身上——对增长的贪婪、对简单的不屑、对"万一"的恐惧。

承认它,面对它,然后每天都做一点正确的事。哪怕只是删掉一行永远不会走的代码。

如果你被我说中了,去代码库里转转,删点东西吧。

一名痴迷于计算机技术的学生~