离线也能用:vibe 项目的 local-first 架构实战
AI 默认给你云端优先架构,断网即白屏。这篇指南讲独立开发者如何把 vibe 项目改造成 local-first:四问决策、同步引擎选型矩阵、落地六步、五个反模式与断网测试清单——把网络请出 UI 层。

先讲一个断网的早晨
想象这个场景:你用 Claude Code 花了三个晚上 vibe 出一个记账应用,界面漂亮、分类智能、图表丝滑。你很满意,发给朋友试用。第二天早高峰,朋友在地铁里打开它——隧道里信号时断时续,应用卡在加载动画上转了十秒,最后弹出一行冰冷的 "Failed to fetch"。他关掉了,再也没打开过。
这不是你代码写得烂,这是架构选错了。AI 编程工具默认给你的是"云端优先"架构:UI 只是云数据库的一面镜子,镜子后面那台服务器一断,镜面就碎了。而你的用户——通勤族、差旅党、海外用户、工厂车间里的人——恰恰大量生活在弱网和无网环境里。
local-first(本地优先)就是专门治这个病的架构:把本地数据库当作数据的"真源",云端退化成一个同步副本。有网没网,应用照常用;网络恢复,数据悄悄对齐。这篇文章不讲概念,讲一个独立开发者怎么在自己的 vibe 项目里真正落地它:要不要上、选什么引擎、哪六步、哪五个坑千万别踩。
核心判断先摆出来:local-first 不是"加个缓存",而是一次架构宣誓——从此你的 UI 只跟本地数据库说话,永远不直接等网络。接受不了这条,就别上,上了半截比不上更痛苦。
local-first 到底是什么:三句话讲清
第一句:本地数据库是真源(source of truth)。所有读写先落本地,UI 永远从本地读,所以渲染速度是内存级的,断网零感知。
第二句:云端是同步副本,不是主人。网络可用时,一个同步引擎在后台把本地变更推上去、把云端变更拉下来。同步是"最终一致",不是"实时强一致"。
第三句:冲突是常态,不是异常。同一个待办在手机和电脑上同时被改,系统必须有一套事先定好的规则决定听谁的——而不是弹个报错让用户选(用户不会选,也不想选)。
它和你熟悉的"离线模式补丁"有本质区别:补丁式离线是在云端优先架构上打绷带——加个 loading 缓存、失败重试、"您已离线"横幅,断网时应用大面积功能不可用;local-first 是地基,断网时应用 100% 可用,只是同步暂停。绷带能救急,盖不了楼。
标准三层长什么样
UI 层 → 本地数据库(SQLite / IndexedDB)→ 同步引擎 → 云端数据库。注意这个箭头的方向:UI 永远不直接碰网络,所有网络出入都经过同步引擎这一道门。很多 vibe 项目翻车就翻在让 agent 偷偷开了后门——某个页面为了"快"直接调了云端 API,结果断网时只有这个页面白屏,查了一周才发现。
要不要上:四个问题走到哪算哪
local-first 是重架构,别为架构而架构。按下面四个问题顺序回答,第一个答"否"就停:
问题一:用户会在无网 / 弱网环境下用吗?
地铁、飞机、电梯、海外漫游、工厂车间、山区——只要你的核心场景沾上一个,答案就是"是"。反例:一个纯桌面端的内部数据看板,用户永远坐在公司内网前,上 local-first 纯属给自己加戏。诚实一点:大多数 toC 的 vibe 项目,这个问题都是"是"。
问题二:数据以用户私有、单设备读写为主吗?
记账、笔记、待办、健身记录、个人知识库——典型的"是",local-first 的甜点区。如果你的应用是强多用户协作(比如多人同时编辑同一块画布、抢单系统),冲突会从"偶发"变成"日常",同步引擎的复杂度会吃掉你所有时间。这时候更诚实的选择是:先上离线读缓存,写操作仍走云端,把实时协作交给专门的方案。
问题三:数据量和表规模可控吗?
经验线:单用户数据几十 MB 以内、表不到 20 张,local-first 的运维成本是一个人能扛住的。超过这个规模,不是不能做,而是你要分阶段——先把最核心的 3~5 张表做成离线,剩下的保持云端。一次性全量迁移是独立开发者最常见的死法:三个月后你会发现自己在维护一个自制的分布式数据库,而不是在做产品。
问题四:你能接受"同步"是长期维护项吗?
这是最扎心的一问。上线只是开始,此后每一次 schema 变更、每一次冲突规则调整、每一个 tombstone(后面会讲)的清理策略,都是你的工作。local-first 把"网络不可靠"这个复杂度从用户侧搬到了开发者侧——用户爽了,你要还债。如果你现在是一个人、产品还没验证 PMF,折中方案完全体面:离线只读 + 写操作排队重试,用户感知能拿到 70 分,成本只有完整方案的两成。
一句话总结决策:高频弱网场景 + 私有小数据 + 你愿意长期还债 = 上;缺任何一条,就选流程图右侧的折中路线,不丢人。
选型:同步引擎矩阵与本地存储
这是全文最值得收藏的一节。2026 年的生态里,你不需要自研同步引擎(千万别),在下面几家里选:
| 引擎 | 后端绑定 | 冲突解决 | 学习成本 | 适合谁 |
|---|---|---|---|---|
| ElectricSQL | Postgres | CRDT 自动合并 | 中 | 已有 Postgres,想渐进改造的项目 |
| PowerSync | Postgres / MySQL / Mongo | 服务端规则 + 客户端合并 | 中低 | 移动端为主(Flutter / RN),要现成 SDK 的 solo 开发者 |
| Zero | 自带(或对接 Postgres) | 服务端权威 + 乐观更新 | 中 | React 技术栈,追求"像写本地 state 一样写同步" |
| Automerge | 无(需自建同步服务) | CRDT,数学上保证收敛 | 高 | 文档/JSON 类数据,能接受自建同步层的极客 |
| Yjs | 无(需自建) | CRDT,文本协同最强 | 中高 | 富文本、画布类协同场景(注意:这是协同引擎,不是通用同步) |
| 土制 LWW | 任意 | last-write-wins(时间戳) | 低 | 谁都别选——除非你想亲手体验数据丢失(见反模式一) |
给独立开发者的选型建议,按优先级排序:
- 已经有一个 Postgres 后端:优先 ElectricSQL 或 PowerSync。why:不用换数据库,同步层是增量接入,失败了可以退回去。trade-off:你要接受它的同步语义(比如 ElectricSQL 的 shape 订阅模型),别试图绕过它手写 SQL 同步。
- 从零开始的新项目:看前端栈。React 选 Zero,Flutter / React Native 选 PowerSync。why:SDK 成熟度直接决定你踩坑的数量,生态的 issue 区就是你的免费 QA。trade-off:被绑定一家引擎,迁移成本高——所以选之前先读它的"不支持什么"文档,而不是"能做什么"。
- 数据是文档/JSON 形态:Automerge。why:CRDT 在文档合并上是降维打击,两个离线编辑的版本能数学保证合并且不丢字。trade-off:你要自己搭同步服务(或用它家的托管),而且关系型查询是它的弱项,别拿它当数据库用。
本地存储怎么选
- Web 端:SQLite 编译到 WASM(如 wa-sqlite / sql.js 持久化版)是首选,why:真 SQL、真事务,agent 生成的 SQL 迁移脚本能直接跑;IndexedDB(Dexie 封装)是备选,why:API 简单但查询能力弱,表关联一多就抓瞎。trade-off:WASM 版 SQLite 首次加载多几百 KB,对首屏敏感的应用要做懒加载。
- 移动端:原生 SQLite(expo-sqlite / OP-SQLite),别用 AsyncStorage 存结构化数据——那是键值存储,不是数据库,三个月后你的"查询"会变成全量扫描。
- 一句话红线:本地存储必须支持事务。没有事务的"数据库",在同步中断恢复时一定会给你造出半写的数据,debug 到哭。
一份可以直接喂给 agent 的选型约束
选型最怕的不是选错,而是 agent 在你没注意时混用。把下面这段存成项目的 AGENTS.md 片段,每次让 agent 动数据层前它都会读到:"本项目为 local-first 架构:UI 层禁止直接调用云端 API,所有数据读写必须经过本地 SQLite;新增表默认 local-only,需要同步的表必须在 migration 注释中声明 sync=true 并说明理由;删除一律写 tombstone,禁止物理删除;冲突解决策略见 README 冲突矩阵表,agent 不得自创策略。" why:agent 的默认倾向是"最快让功能跑起来",也就是直连云端——这条约束把它的捷径堵死,逼它走正门。trade-off:前期你会多花 10% 的时间跟 agent 掰扯"为什么不能直调",但这 10% 买的是架构不腐化,值。
落地六步:从云端优先改造成 local-first
假设你已经有一个跑起来的 vibe 项目(云端优先),下面是改造成 local-first 的六步。每一步都标注了"为什么"和"代价",以及一句可以直接喂给 agent 的 prompt 片段——因为这类改造最适合拆成小任务让 agent 并行做,而你负责验收。
第一步:本地 schema 先行,云端 schema 跟随
从今天起,建表、改字段先写本地 SQLite 的 migration,云端 schema 由同步引擎反向对齐(或保持兼容)。why:真源在本地,schema 的权威版本必须在本地,否则你会陷入"两边 schema 谁说了算"的泥潭。trade-off:你失去了"直接在云端 console 改表"的爽感——所有结构变更走 migration 文件,慢,但可回滚。给 agent 的 prompt:"为本地 SQLite 生成 migration v3:todos 表新增 due_date 字段(可空),要求幂等、可回滚,并同步更新 TypeScript 类型定义。"
第二步:删除必须变成 tombstone
local-first 里不允许物理删除。删一条数据 = 写入一条"墓碑"(tombstone:{id, deleted_at}),同步引擎靠墓碑把删除传播到云端和其他设备,过期后由 GC 清理。why:A 设备离线时删了一条待办,B 设备离线时改了同一条——没有墓碑,同步时 B 的修改会"复活"已删除的数据,这是 local-first 新手第一大灵异事件。trade-off:表里会躺着墓碑数据,查询要处处带上 where deleted_at is null 的过滤;给 agent 的第一条铁律就是"所有查询默认排除墓碑", code review 时重点查这一条。
第三步:把冲突解决策略写下来,逐表定
不要全局一套策略。建议的默认分级:① 用户私有数据(笔记正文、设置):字段级 LWW + 保留冲突副本(conflict copy),why:丢字比弹框可怕,Google Docs 就是这么干的;② 计数/状态类(点赞数、订单状态):服务端权威,客户端乐观更新失败就回滚,why:这类数据"错了"比"丢了"更贵;③ 金钱/权限类:禁止离线写,必须在线,why:有些钱花不得,这是底线不是保守。把这张表写进 README,让 agent 每次生成数据层代码前先读它。
第四步:离线写队列与幂等
断网时的写操作进本地队列表(outbox),按序重放。两个硬要求:每条操作带客户端生成的幂等键(idempotency key,UUID),重放时服务端按 key 去重——why:网络抖动会导致"发出去但没收到回执",没有幂等键就会重复扣款/重复建单;队列操作必须是原子入队,why:应用在"写本地成功、入队前"崩溃,数据就丢了,用户会以为是同步 bug,其实是你的 bug。
第五步:认证与同步通道解耦
token 过期不能导致同步引擎把本地数据清空或停止写入。正确做法:同步通道只负责传数据,认证状态机独立维护;token 失效时同步暂停、本地照写,等重新登录后继续推。why:vibe 项目最常见的翻车是 agent 把"401 → 清空本地缓存并跳登录"写进拦截器,在 local-first 里这等于用户一掉线就丢数据。trade-off:你要多维护一个"待同步"状态指示器,但这是值得的——用户看到"3 条更改待同步"时的安心感,是留存率。
第六步:版本迁移与旧客户端
schema 升级时,旧版本客户端可能还在用旧结构写数据。策略:同步协议带版本号,云端保留 N-1 版本的兼容读取至少一个发版周期;本地做"懒迁移"——读到旧数据时当场升级再写回。why:独立开发者没有强制升级的本钱,用户三个月不更新是常态。trade-off:兼容代码是技术债,要在 roadmap 里明确写下"v5 发版后删除 v3 兼容层",否则它会永远烂在那里。
五个反模式:每一条都是真金白银的学费
反模式一:用时间戳做 last-write-wins
两台设备的时钟永远对不准,差几秒是常态,差几小时(用户手动改时间、时区 bug)也不罕见。靠 updated_at 裁决冲突,等于把数据正确性交给运气。正确做法:用逻辑时钟(Lamport timestamp)或混合逻辑时钟,实在要用时间戳,也只做参考排序、不做裁决依据。
反模式二:把所有表都同步
日志表、埋点表、临时缓存表也配了同步——结果用户手机里躺着 800MB 的埋点数据,同步永远跑不完,电量哗哗掉。原则:只有"用户在新设备登录后希望看到的数据"才进同步集,其余一律不同步。给 agent 的审查 prompt:"列出本次 migration 中新增的表,逐张说明它是否需要同步及理由,不需要的一律标记为 local-only。"
反模式三:同步通道里塞业务逻辑
在同步回调里做"同步完成后发推送""同步时顺手算统计"——同步是基础设施,它的失败重试、乱序到达会把你的业务逻辑触发 N 次。铁律:同步引擎只负责"数据最终一致",任何副作用走独立的、幂等的任务队列。
反模式四:上线前没做过一次真正的断网测试
模拟器上点一下"离线"不算。要做的是:真机开飞行模式用三天。90% 的 local-first bug(墓碑复活、队列乱序、token 失效清数据)只在这种测试里现形。下一节就是清单。
反模式五:冲突解决靠"让用户选"
弹个框"检测到冲突,请选择保留哪个版本"——用户会骂娘然后卸载。99% 的冲突应该由预设策略自动解决并留下可追溯记录(conflict log),只有涉及钱和权限的极少数场景才打扰用户。记住:把复杂度留给自己,把确定性留给用户,这就是你拿 local-first 换留存率的代价。
断网测试清单:发布前的 7 项
- 飞行模式三天:真机、全功能用三天,记录所有异常。这是性价比最高的测试,没有之一。
- 墓碑复活测试:A 设备离线删数据,B 设备离线改同一条,先后上线,确认删除胜出且 B 的修改进了 conflict log 而不是复活。
- 队列重放测试:离线做 20 个写操作(含建、改、删混合),恢复网络后确认顺序正确、无重复(查幂等键去重生效)。
- 弱网抖动测试:用网络限速工具模拟 2G + 30% 丢包跑一小时,确认应用不卡死、同步最终收敛。
- token 失效测试:离线期间让 token 过期,确认本地数据完好、同步暂停而非清空,重登后继续。
- 跨版本测试:旧版本客户端写的数据,新版本能读、能迁移;反过来新版本写的数据,旧版本不崩溃(至少能忽略未知字段)。
- 存储配额测试:把本地数据灌到浏览器/系统配额上限(移动端约几百 MB 到几 GB 不等),确认应用给出清理提示而不是静默写失败。
诚实账:成本、收益与什么时候别做
说点得罪人的实话。local-first 对独立开发者的真实成本:首次改造成 2~4 周(一个熟手 + agent 打下手),之后每个 schema 变更多花 30% 时间处理兼容,冲突策略是一次性设计、长期微调。收益:弱网场景留存率提升是数量级的(离线可用的应用在通勤场景下就是降维打击),客服里"没网用不了"的投诉直接消失,以及一个意外的 bonus——因为 UI 只读本地,你的页面渲染速度会快到不真实,性能优化那章的很多活儿不用干了。
什么时候别做:① 产品还没验证 PMF——先让 100 个人愿意在有网时用,再谈没网;② 团队(就是你)连 migration 都嫌麻烦——local-first 会把这种嫌麻烦放大十倍;③ 数据天然中心化(行情、排行榜、实时竞价)——离线了一样没意义,不如把精力花在弱网降级体验上。
最后给一句可执行的收尾:今晚就可以做的是——打开你的项目,让 agent 全局搜索所有直接调用云端 API 的 UI 代码,列成清单。这张清单就是你的 local-first 改造地图,第一行从"把读操作切到本地"开始。架构宣誓,从把网络请出 UI 层开始。
评论 (0)
相关文章

AI 选技术栈时优化的是"最快跑起来",而不是"三年后还合适"。本文给独立开发者第三条路:既不硬扛也不推倒重写——一套六步换栈实战流程,旧系统全程可运行,每一步都可回滚。

模型是租来的,数据是自己的。这篇指南给一人团队完整的数据飞轮实战:四个环节与最小闭环、采集三问、轻量标注管线、数据集版本与 eval 集生长、按成本排序的四条回流路径、冷启动数据来源,以及一页纸的每周检查清单。

CLI 是开发者工具的母语。当用户是开发者、操作高频重复、任务适合进管道时,网页是多余的中间层。这篇指南给从 0 到 1 发布 CLI 的完整实战:三问决策、--help 即文档的命令行 UX、human/--json 双模式输出、Node/Python/Go/Rust 选型、npm/pip/brew/cargo 分发、自动更新、配置与密钥管理、快照测试,以及首版最小可用清单。