【翻译】性能问题并非技术问题

4 阅读6分钟

原文链接:shud.in/thoughts/pe…

作者:Shu Ding

在Vercel的七年间,我提交了约400个专注于性能优化的拉取请求,占比近十分之一。瀑布图消失了,包体积缩减了,缓存键修复了,重复渲染被移除了。

人们常问我为何如此执着。他们以为我在修复缺陷代码。我曾也这么认为。

但随着时间推移,我领悟到:这些并非技术问题。相同的错误反复出现——不同工程师、不同代码库、不同时间点。

我们拥有更快的框架、更优的编译器、更智能的代码检查工具,性能却仍在持续恶化。

问题不在代码,不在工具。

问题在于。而熵无法通过补丁修复。

I. 约束

我们常自我安慰:只要工程师"专注"且"编写更优质的代码",应用程序就能保持高效运行。但事实并非如此

每位工程师——无论经验多么丰富——大脑能承载的信息量终究有限。现代代码库呈指数级增长:依赖项、状态机、异步流程、缓存层层叠加。

代码库的增长速度远超任何个人能追踪的极限。工程师在不同功能间切换焦点,上下文逐渐淡忘。随着团队规模扩大,知识变得分散且稀释。

你不可能在砌一块砖头时,同时将整座大教堂的设计牢记于心。

上下文无法随规模扩展。

II. 幻象

若人无法成为守护者,我们便转向工具。我们说服自己:只要抽象足够智能,产品自然会快。这也错了。

以下是生产环境代码库中真实存在的模式,出自经验丰富的工程师之手:

示例1:抽象掩盖了代价

function Popup() {
  useOnClickOutside(onClose)  // Adds a global event listener
  // ...
}

一个可复用的弹出钩子,用于添加全局点击监听器。

抽象层看似简洁,成本却隐而不显。每个实例都会添加一个全局监听器。100个实例意味着每次点击都会触发100个回调。技术修复很简单——去重监听器即可。但真正的问题在于系统性缺陷:没有任何机制能阻止这种模式蔓延。重复使用钩子的工程师在生产环境运行前根本无从知晓其代价。

示例2:脆弱的抽象层

// Week 1: Engineer A adds caching
export const getUser = React.cache(async (userId: string) => {
  return await db.user.findUnique({ where: { id: userId } })
})

// Week 4: Engineer B extends it
export const getUser = React.cache(async ({ userId, includeRole }: { userId: string; includeRole: boolean }) => {
  return await db.user.findUnique({ 
    where: { id: userId },
    include: { role: includeRole }
  })
})

缓存函数在扩展为对象参数时失效。

工程师B做了一个合理的改动——添加参数,扩展功能。代码编译通过,测试也通过。但React.cache()使用引用相等性。每次调用{ userId, includeRole }都会创建新的对象引用,彻底破坏缓存。技术知识在文档里都有说明。系统性问题在于没有任何机制强制执行这个契约。类型安全也无济于事。缓存会悄无声息地停止工作。

例3:抽象变得晦涩难懂

async function processCheckout() {
  // ... 500 lines of complex business logic ...
  
  // Someone adds a feature: apply coupon for new users
  const hasCoupon = await getCouponStatus(user.id)
  if (hasCoupon) {
    cart.discount = await applyCoupon(cart.id)
  }
  
  // ... 500 more lines of other features...
}

添加功能会在长函数中产生意外的瀑布效应。

优惠券检查会阻塞其下方的所有代码。从技术上讲,它本可并行执行或被提升处理。但工程师只专注于解决局部问题:"添加优惠券支持",并未考虑全局的异步流程。在由多人耗时数月构建的千行函数中,瀑布式流程已不可见。这并非知识缺口——而是上下文缺口。优化所需的信息确实存在,却散落于整个函数之中。

示例4:抽象开销

const historySize = useMemo(() => history.length, [history])

在不进行备忘的情况下,属性访问速度更快。

读取 .length 瞬间完成——这属于属性访问。而创建备忘闭包、追踪依赖关系并在每次渲染时进行比较则不然。技术成本是可测量的。但系统性问题在于:没有任何环节要求进行测量。抽象机制本应用于优化高成本工作,却无人验证该工作是否高成本。系统允许在无需需求证明的情况下进行优化。

抽象机制的失败并非源于设计缺陷,而在于其需要全局上下文——而工程师在进行局部修改时往往无法掌握全局。

系统性问题无法通过编程手段解决。

III. 现实

性能如同花园,若不持续除草,终将荒芜。

你可以进行性能分析,可以优化代码,可以编写完美的抽象层。但下一位工程师不会看到这些,下一个截止日期会迫使他们偷工减料,下一个功能需求又会堆砌冗余代码。

我们总在应用程序"感觉"变慢时才着手修复——这属于被动应对。卓越的产品需要主动构建

熵终将获胜。并非工程师疏忽,而是系统允许如此。

解决方案在于建立能强制执行的系统——那些纪律无法企及的领域。

IV. 变革

我们终于拥有了能在检查每块砖时调用整座大教堂设计图的系统。它们永不疲倦,始终如一地执行标准,绝不会因截止日期而省略检查。

这并非人工智能代写代码,而是精准把握学习时机进行指导。

当有人编写瀑布式流程时,系统不仅会标记错误——更会指导用户:"此处阻塞请求,请按此方式并行化。"当缓存被破坏时,它会解释原因。

如何构建这样的系统?你需要提炼模式。每个错误都转化为规则,每条规则不仅记录故障现象,更揭示根本原因与修复方案。最终使系统具备可查询性、可维护性与强制执行力。

这并非理论推演。我们观察到的模式——缓存中断、瀑布效应、不必要的重新渲染——都能提炼成结构化知识,供开发者实时调用。当他们审查代码时,会应用这些模式;当发现违规时,则予以说明。

早期成果证明了其有效性。团队正在发现人工审查中遗漏的关键问题。

编程无法消除复杂性,但你能构建记住人类遗忘之物的系统。

这便是遏制熵增之道。

软件天生趋向混乱,此即熵增。 意志力无法对抗它, 唯有系统方能抗衡。