先别吵跨端,我踩的坑在数据库
去年帮一个做冷链的小团队救火,司机端是 Android 7 的旧平板。 仓库 WiFi 时断时续,本地要存 3000 多条订单。 他们最早用 React Native 0.72 老架构 + AsyncStorage 存待同步 JSON。 司机点“完成”后列表卡 3-4 秒,偶尔直接白屏。 我一开始也以为换 FlashList、加 memo 就行,后来发现根本不是渲染问题,是 AsyncStorage 每次读整坨 JSON,再 parse,再写回。 拆成 SQLite 后,同样机型冷启动少了大概 1.8 秒,崩溃率从 2.3% 降到 0.4%(这是他们自己的埋点,样本不大)。 所以我现在看移动离线优先,顺序是:数据层 -> 同步协议 -> 后台任务 -> UI。 跨端框架只是最外面那层皮。
我用三台旧机跑了组不严谨对比
测试环境:Pixel 6a Android 14、红米 Note 8 Android 10、iPhone 12 iOS 17.5,全部 Release。 数据是 3000 条订单,每单 12 个字段,不含图片本体。 指标只看:冷启动到列表可滚动、插入 1000 行、更新 500 行、内存峰值、后台同步 200 条。 结果大概是这样:
- RN 0.76 新架构 + op-sqlite:Android 包体 18.6MB,冷启动 620-780ms,插入 1000 行 410ms,内存峰值 142MB,同步 200 条 6.8s。JSI 直调确实快,但 iOS 后台唤醒和扩展里用 SQLite 有坑,我遇到一次 WAL 文件没 checkpoint,后台进程被 kill 后恢复慢。
- Flutter 3.24 + Drift + sqlite3_flutter_libs:包体 23.4MB,冷启动 520-690ms,插入 1000 行 350ms,内存峰值 168MB,同步 7.4s。Drift 的编译期类型安全很省心,Isolate 跑查询也稳,但平台通道传大 JSON 时会有额外拷贝。
- KMP + SQLDelight + Compose:Android 包体 12.8MB,冷启动 480-610ms,插入 1000 行 300ms,内存峰值 126MB,同步 5.9s。Android 侧很舒服,iOS 还是要写 Swift 壳,团队得接受 Gradle KMP 那一套。 这些数字样本小、机型旧,别当基准。 真正影响体验的也不是插入速度,而是同步冲突和后台存活。 插入 1000 行差 100ms,用户根本感觉不到;但冲突处理错了,用户会丢单。
离线同步最常翻车的 5 个点
第一,用本地时间做 last-write-wins。设备时间可以手动改,时区也会变。 我现在建表至少加:server_updated_at、version、device_id。 第二,业务表和同步队列不在一个事务里。 正确做法是 outbox 表:id、entity、entity_id、op、payload、retry、next_at。 每次写 orders 和写 outbox 必须在同一个事务。 第三,没有死信队列。重试 5 次还失败就进 dead_letter,别无限重试把电量和流量吃光。 第四,分页用 OFFSET。数据一多就慢,改成 updated_at 游标,每页 200。 第五,数据库迁移没写 schemaVersion。我踩过一次 Drift 从 v1 到 v2 没写迁移,线上直接 crash,后来学乖了:每次改表先写迁移测试。
SQLite 参数也别乱抄。 我常用:PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA cache_size=-8000; PRAGMA page_size=4096。 建表字段可以这样:id TEXT PRIMARY KEY, updated_at INTEGER NOT NULL, server_updated_at INTEGER, version INTEGER DEFAULT 0, device_id TEXT, sync_state TEXT CHECK(sync_state IN ('local','syncing','synced','conflict')), deleted_at INTEGER。 索引至少两个:CREATE INDEX idx_orders_sync ON orders(sync_state, updated_at); CREATE INDEX idx_orders_updated ON orders(updated_at)。
冲突解决别一上来就 CRDT。 普通订单、工单、库存,字段级合并就够:数量、状态取服务端,备注取最新非空,地址取用户最后编辑。 只有多人同时编辑同一字段、还要求不丢字,才考虑 HLC 或 CRDT。 CRDT 不是银弹,它会把模型变复杂,后端也要配合。
后台同步和重试,参数直接给
Android 用 WorkManager,最小周期 15 分钟,实际会被 Doze 延迟。 约束加 setRequiredNetworkType(CONNECTED),需要不计量网络再加 UNMETERED。 紧急同步用 expedited,但有配额,别拿它当长连接。 iOS 的 BGAppRefreshTask 更玄学,系统说最早 15 分钟,实际几小时都可能。 要准时,还是靠静默推送,但低电量模式下也会被扣。 RN 可以用 react-native-background-fetch,Flutter 用 workmanager,KMP 里 Android 接 WorkManager、iOS 接 BGTaskScheduler。
网络参数我一般这么定:OkHttp/Retrofit 连接超时 10s,读超时 20s,指数退避 1s、2s、4s、8s、16s,最多 5 次,再加 0-300ms 随机 jitter。 图片上传分片 512KB,并发 2,失败重试 3 次。 同步拉取每页 200,服务端返回 next_cursor,不要用 OFFSET。 测试至少覆盖:飞行模式 30 分钟、WiFi 切 4G、上传中杀进程、系统时间改到 2027 年、数据库从 v1 升到 v3。
我的独立观点:2025 年别按 UI 选型,按数据层选
如果团队 3 人以内、已经会 React,我会选 RN 新架构 + op-sqlite,但坚决不用 AsyncStorage 或 MMKV 存业务实体。 MMKV 快,适合放开关和 token,不适合当订单库。 如果后台任务重、UI 一致性要求高,Flutter + Drift 更省心,代价是包体和内存高一点。 如果 Android 是主战场、iOS 只是配角,KMP + SQLDelight + Compose 最划算,但别为了跨端而跨端。 跨端省的是 UI 工时,离线同步省不了。 数据库 schema、outbox、冲突协议和迁移测试,才是移动端真正的护城河。 页面写得再顺,地下车库没信号时丢一单,用户只会骂你。
最后给你一张检查清单: 1)所有写操作是否在事务里? 2)有没有 outbox? 3)有没有 server_updated_at 和 device_id? 4)重试有没有上限和死信? 5)飞行模式、切网、杀进程、改时间有没有测? 6)数据库迁移有没有 v1 到 v2 的自动化测试? 这 6 条比选 Flutter 还是 RN 重要得多。