4.2 性能避坑:查询设计、索引与监听滥用

Firestore 快不快,一半取决于它自己,另一半取决于你的查询长得像不像它希望的样子。这一节讲的都是我用流量和账单换来的教训,按查询、索引、监听、文档粒度四条线展开。

查询设计:别把数据库当数组用

最常见的反模式是:整个集合一把拉回客户端,再在内存里过滤排序。数据量小的时候毫无症状,等集合涨到几千上万条,首屏变慢、流量飙升、客户端内存告警会一起找上门。

Firestore 的强项就是服务端过滤,where、orderBy、limit 能在数据库层把数据裁到最小。宁可多花点心思设计查询,也别把过滤逻辑搬到客户端。

分页也是查询设计的一部分。limit 控制每页条数,翻下一页时用上一页最后一条文档做游标,也就是 startAfter 配合文档快照,而不是用偏移量思维硬算页码。数据在实时数据库里不停地变,基于位置的偏移天然不稳,基于文档的游标才是 Firestore 的原生姿势。

索引:便利与泛滥之间

每加一个组合查询,Firestore 就可能要求一条复合索引。控制台报错信息里直接放着一键创建的链接,点一下就解决,体验好到让人上瘾。

问题在于指数膨胀:每多一个查询组合,索引就多一条,写操作要同步维护的索引也就多一份。索引建多了,写放大、存储开销、排查问题时面对的长列表,全是日后的债。我的原则是:建索引之前先想清楚,这个查询是不是真的需要,能不能复用已有索引,能不能调整字段顺序少建一条。一键创建的便利留给原型期,进入正式开发后,索引变更应该和改表结构一样走评审。

监听滥用:实时不等于常驻

第二个上瘾点是实时监听。很多人在应用入口挂一个巨大的监听,订阅整个集合,从此不管用户在哪个页面、看不看这些数据,监听都活着。结果是客户端内存被长期占用、流量持续产生,断线重连风暴一来,体验雪崩。

更健康的模式是按页面订阅:进入页面建立监听,离开时退订,监听生命周期跟着视图走,全局只保留真正需要全程实时的一小撮数据。

方式 优点 代价
巨型常驻监听 写起来省事,数据随处可用 内存常驻、流量浪费、断线重连易雪崩
按页面订阅退订 资源跟着视图走,流量可控 需要认真管理监听生命周期

离线缓存也值得单独说一句。本地持久化带来的秒开与弱网体验非常香,但收益建立在监听覆盖合理的前提上;首次冷启动时本地没有缓存,该等的网络请求一样要等,别指望离线缓存把首屏问题一并解决。

文档粒度:热点文档是隐形天花板

Firestore 对单个文档的高并发写入有限制,这是很多人踩到才发现的硬约束。典型场景是计数器:热门内容的点赞数、浏览量都写进同一个文档,流量一上来就写入排队、频频失败,症状看起来像灵异事件。

解法是换思路:把一个计数拆成多条子文档分片,每次写随机命中一片,读取时汇总求和——这就是分布式计数的思路。写的时候多花一点聚合功夫,换来的是把写热点摊平。同理,任何被高频并发写的单文档设计,都应该先问一句:它扛得住吗?

老规矩,上表总结:

症状 病因 处方
集合变大后首屏变慢、流量飙升 整表拉回客户端再过滤 服务端 where 加 orderBy 加 limit,客户端只做展示
翻页时数据重复或跳变 用偏移量思维硬分页 limit 加文档游标,基于最后一条文档翻页
写入越来越慢,索引列表越滚越长 复合索引无节制膨胀 建前评审、复用已有索引、调整字段顺序
流量异常,断线后体验雪崩 大监听常驻全应用 按页面订阅退订,全局只留必要监听
计数器类写入排队、失败 热点文档高并发写 拆分片求和的分布式计数思路

性能问题大多不是突发的,而是随数据量增长慢慢显形。这些处方越早用上,代价越小。