这两年我帮不少团队看过性能问题,几乎每次开场都会听到一句话:硬件已经这么强了,软件为什么还能卡?手机动辄八核、内存上 12G、闪存早就用上 NVMe,结果打开一个常用 App 还要转圈一两秒;后台服务从物理机迁到更高配的容器,首屏接口反而更慢。把问题归到“硬件不够快”是最容易的解释,但多数时候我们是在用错误的指标理解用户感知。真正吃掉时间的,往往不是芯片算力不足,而是软件内部层层叠叠的隐性成本。
这篇文章说的“臃肿”,不只是安装包体积变大,更指依赖关系的积累、初始化路径的延长、抽象层级的堆叠,以及大量“看起来顺手加一点无所谓”的运行时开销。它们不会出现在需求文档里,也不会被测试用例直接暴露,却在每次启动、点击、滚动时静默扣费。我写下这些,是想分享自己排查性能问题时的真实经验,以及一套能长期对抗软件肥胖症的方法。如果你正在为 App 启动慢、Web 首屏白屏、后端接口 P99 持续恶化发愁,这篇内容应该能给你一些思路。
1. 为什么硬件规格“变强”了,使用体感却没有“变快”
1.1 你把 CPU 用满了,不代表用户等待时间变短了
很多人习惯用 CPU 占用率判断系统是否繁忙,但用户感知的“卡顿”和 CPU 是否跑满并不是一回事。现代芯片确实有很多核,可某一个点击动作带来的任务,往往串行地堆积在一条链路上。你可以在多核设备上同时做很多事,但用户等待的那个单一请求,每一毫秒都得按顺序走完关键路径。
我常用一个生活例子解释这个问题:马路从两车道拓宽到八车道,但如果每一个路口还是只给两秒绿灯,一辆车从 A 点到 B 点的总耗时并不会明显缩短。硬件升级解决的是“同一时间能跑多少车”的吞吐量问题,而用户关心的是“我这辆车几点能到”的延迟问题。很多软件团队恰恰把优化重点放在了吞吐量上,增加缓存、并发、批量处理,却没有缩短任何一次真实操作的关键路径。
一个下单请求经过网关、用户服务、库存服务、支付服务、消息通知五个环节,即便每个环节只有 5ms,串行也要 25ms;一旦中间环节出现线程池排队、远程调用超时重试、分布式锁等待,时间立刻膨胀到几百毫秒甚至几秒。这时候你去看机器监控,CPU 可能只有 20%,内存也没爆,但用户就是在那里干等。这不是硬件慢,是软件把关键路径设计得太长了。
1.2 隐性成本的一部分是“负担迁移”而不是被消除
过去客户端相对瘦,很多业务逻辑放在服务端;后来为了交互体验更好,客户端开始承载越来越多东西——路由框架、网络层、数据层、埋点 SDK、推送 SDK、热更新、各种中间件。每一个框架单独看都合理,但叠加起来之后,框架自身的启动和初始化时间也被搬到了用户最关键的操作路径上。
从客户端启动的角度看,进程创建后要加载各类动态库、初始化插件、注册路由表、解析配置、拉起线程池、建立网络连接。这些工作在过去可能分布在页面真正用到时才做,现在为了实现“秒开”“离线包”“统一配置”等能力,很多团队把它们集中到了启动阶段。于是出现一种奇怪现象:热启动与冷启动的耗时差距越来越小,因为即使是热启动,也要重新执行大量初始化任务。
团队在架构评审时往往只评估“这个框架能带来什么能力”,很少评估“这个框架让用户每次启动都要额外付出多少”。等到框架越来越多,启动链路越来越长,才回过头来发现优化空间已经被各种基础设施占满了。这种负担迁移比单纯的代码写得差更隐蔽,因为它看起来每一步都是正确的工程决策。
1.3 另一种“慢”:硬件只快了“快路径”,软件却越来越依赖“慢路径”
硬件持续变快是不可否认的事实,但硬件优化通常体现在顺序计算、连续读写、批量并行这些“快路径”上。软件行业却朝着相反方向走:更频繁的随机访问、更深的调用栈、更多的动态解析、更大的对象图。
举例来说,固态硬盘的顺序读取确实快得惊人,但应用启动时要读取的是大量分散的小文件、加载类元数据、做签名校验、映射 dex 或 jar,这些恰恰是闪存的“慢路径”。CPU 主频很高,但一次缓存未命中可能就要等待几百个时钟周期;如果代码构建出巨大的对象图,频繁触发垃圾回收,再强的 CPU 也要停下来等内存整理。
