乐于分享
好东西不私藏

百万下载量不等于可扩展:客户端到底什么是"可扩展"

百万下载量不等于可扩展:客户端到底什么是"可扩展"

我不断遇到同一个词被用来表达毫无意义的东西。

有人介绍自己是”构建可扩展移动应用的开发者”——证据是他参与过的应用下载量超过一百万。某处有篇文章标题大概是如何用 BLoC 构建可扩展的 Flutter 应用,而有人直接问了一个扎心的问题:”BLoC 到底怎么帮助扩展?换成其他状态管理不也一样吗,还是我漏掉了什么?”

你没漏掉任何东西。你的直觉是对的,这也是这篇文章存在的全部原因。

“可扩展”已经变成了移动工程领域的”协同效应”——它暗示某种东西很好,却从不承诺具体是什么。这种混乱其实不怪任何人——这个词原本在一个地方有精确的含义,被借用到客户端后,悄无声息地被剥夺了。

这个词从哪来

在后端,”扩展”有一个你可以用数字争论的定义。它是系统承受并发负载大幅增长而不崩溃、不龟速、不烧钱的能力。每秒一万请求变成十万,你的服务扛得住吗,代价是什么?这是架构的一个真实、可衡量的属性。水平扩展、连接池、分片、缓存层——这些存在是因为更多用户同时访问同一台服务器是一个真正的工程问题。

现在把这个定义搬到移动应用上,看着它烟消云散。

客户端是单租户的,这改变了一切

后端从共享基础设施为所有人提供服务。移动应用不是。每个用户在自己的设备上运行自己的私有副本,使用自己的 CPU 和内存。客户端上没有随着用户基数增长而变得更加拥挤的共享资源。你的第一万个用户运行的应用二进制文件,和第一个用户做的是完全相同的工作。

这就是为什么”我构建可扩展应用,因为我们达到了百万下载量”这句话一戳就破。

下载量衡量的是分发。它们是营销和产品成果,不是工程成果。它们还是累积的——这一百万包括所有曾经安装、打开、讨厌、卸载的人。它甚至不是月活,更不是并发负载。你可以用 setState 和祈祷勉强拼凑的代码库攒到百万下载,也可以架构完美的应用只有五百下载。这个数字告诉你营销奏效了。它对你工程师构建了什么只字不提。

而且这里有个被忽略的部分:当应用达到百万用户时,如果有任何东西扛不住了,那一定是后端——API、数据库、队列。客户端与这个负载的唯一关系,是它表现得像个好公民还是个坏公民。后面再展开。

所以第一个纠正是简单的。在客户端,”扩展”从来不是关于多少人使用应用。这个维度不属于你。

工具不能赋予扩展性,纪律才能

这就说到 BLoC,以及更广泛的”X 帮我构建了可扩展应用”这类说法。

评论者的问题值得一个直截了当的答案,所以这就是:**是的,任何有纪律的状态管理都一样。**BLoC 没有让那个应用变得可扩展。BLoC 给作者的是一种结构——强制分离业务逻辑和 UI、可预测的单向流、可测试的状态转换、能够限定重建范围的能力,这样改变一个东西不会重绘整个世界。这些属性确实能帮助应用在增长时保持可维护和高效。

但它们都不是 BLoC 独有的。Riverpod 给你同样的分离和细粒度重建。Signals 给你细粒度的响应性,在限定更新范围上 arguably 更好。甚至普通的 ChangeNotifier 加上有纪律的边界也能完成大部分工作。作者称赞的属性——可预测、可测试、有范围的状态——是纪律的属性,BLoC 只是对他们来说强制执行这种纪律的载体。

证明工具不是属性的证据:一个糟糕的 BLoC 实现比一个良好的 Signals 实现”扩展”得更差。一个包揽整个应用状态的 god-bloc,每次变化都发射、重建半棵树,比一个有清晰范围的 Signals 设置更难维护、更低效。同一个库,相反的结果。如果工具赋予了属性,这不可能发生。

所以当你读到”X 帮我构建了可扩展应用”时,翻译一下:”X 给了我一种结构,让我的代码库增长时仍然可维护,而且我有纪律地好好使用了它。”这是一个真实且有用的句子。”X 可扩展”不是。

那客户端的”扩展”到底是什么意思?

它确实意味着什么——只是它分成了三个与用户数量无关、而完全与什么在增长有关的维度。对任何说”可扩展”的人,诚实的提问是:沿着哪个轴扩展,瓶颈是什么?

**数据和使用量扩展——重负载下的单个用户。**这是唯一触及性能的维度,而且是按设备的。一个列表 50 条时流畅,50,000 条时卡死 UI 线程。一个无限图片流因为没有任何东西被释放、缓存还在解码全分辨率位图,逐渐走向内存溢出崩溃。一个本地数据库在全新安装时查询瞬间完成,一旦重度用户积累了几年的数据就开始卡顿。扩展这个维度是我们熟悉的手艺:用 ListView.builder 而不是全部构建、设置有尺寸的图片缓存(用 memCacheWidth/memCacheHeight 而不是把 4000px 的图片解码到 200px 的槽里)、重活推到 isolate、controller 及时释放、重建范围限定。注意,这些都不取决于你的下载量。一个拥有大量数据的用户就能触发所有问题。

**代码库扩展——功能集增长下的应用。*当项目只有三个屏幕时,全放在 lib/ 里没问题。到了八十个屏幕、十几个功能团队,同样的扁平结构就会崩溃成合并冲突和”不敢碰任何东西”的恐惧。这里的扩展意味着清晰的分层边界、以功能优先的组织方式、拆分成隔离的包以保持低耦合和合理的编译时间,以及让代码库可变更*——因为大型应用真正的考验不是它能不能运行,而是下个季度你加个功能要不要花三倍的时间。

**流程扩展——团队增长下的应用。**这是人们经常忘记、却往往真正咬人的维度。十五个工程师往同一个仓库提交代码,能不能不天天崩?这需要 CI/CD 在每个 PR 上跑测试和 lint、真正的测试金字塔(逻辑的快速单元测试、组件的 widget 测试、用 Patrol 或 Maestro 等工具做关键流程的薄层 E2E),以及共享设计系统,这样二十个开发者做二十个功能不会产出二十个微妙不同的按钮。手动测试和手动部署构建撑不起一个团队;这不是比喻,是事实。

不属于你的扩展

最后还有一件事值得点名,因为这就是最初混乱的根源。任何有后端的应用确实存在用户数量扩展问题——只是不在客户端。你在客户端的工作是别让它更糟。分页而不是全量拉取。遇到 struggling 的端点时退避重试而不是疯狂请求。缓存以避免重复请求已经有的东西。容忍离线。这些没有让你的应用扩展——它们让你的应用对真正需要扩展的东西表现得像个好公民。

这个区分就是全部要点。客户端不会向更多用户扩展。它在更多数据下保持快速,在更多功能下保持可维护,在更多开发者下保持可发布。

试金石

下次你要称什么东西”可扩展”——你的应用、你的架构、你最喜欢的库——停下来,把这句话说完:

沿着 ____ 轴可扩展,因为否则瓶颈会是 ____。

如果你能填上两个空,你说的有意义。如果你不能,你大概想说”构建良好”——这挺好的,那就直接说那个

精确在这里不是迂腐。那个评论者的问题之所以好,是因为他们拒绝让一个模糊的词蒙混过关,而这样做让他们比他们所质疑的文章更理解这个话题。做那个评论者。