我让 AI 写了两版 Electron 缓存层,JSON 文件比 SQLite 快 4 倍——但最后一行代码我没敢合

📅 2026/7/1 13:07:55
我让 AI 写了两版 Electron 缓存层,JSON 文件比 SQLite 快 4 倍——但最后一行代码我没敢合
先别急着骂标题党。这个结论有前提而且前提比结论本身有意思得多。你大概听过无数遍Electron 本地数据用 SQLite 就对了这种说法。我之前也信。直到上周我让 Cursor 用两种方案各写了一版缓存层跑了个 benchmark数字直接把我干沉默了——JSON 文件方案在小数据量读取场景下比 SQLite 快了将近 4 倍。但这个4 倍后面藏着三个我自己差点踩进去的坑一个比一个阴。下面把整个对比过程摊开讲包括两版 AI 生成代码、benchmark 数据、以及我最后为什么没选那个更快的方案。事情的起因我手上有个 Electron 桌面应用需要在本地缓存一批用户最近浏览的记录——每条大概 200 字节的 JSON总量在 200-2000 条之间浮动。读写频率不高但每次冷启动要一次性拉全量数据渲染列表用户对首屏速度很敏感。原来这层缓存是裸写的 JSON 文件fs.readFileSync一把梭。功能没毛病但代码丑还没做任何并发保护。我想着让 AI 帮我重构成正经一点的方案就给了 Cursor 一个 prompt“为这个 Electron 应用实现一个本地缓存层要求支持全量读取、单条写入、单条删除。优先用 SQLite如果性能不合适再降级到 JSON 文件。给出完整实现和 benchmark 脚本。”Cursor 干活很利索十分钟不到吐了两版代码。SQLite 版用的是better-sqlite3JSON 版用的是fs.readFileSync/fs.writeFileSync 一个简单的写队列。两版代码都能直接跑编译没报错。然后我写了段 benchmark分别测 100/500/2000 条数据下的全量读取和单条写入。两版代码长什么样SQLite 版AI 生成的核心逻辑// sqlite-cache.tsimportDatabasefrombetter-sqlite3import{app}fromelectronimportpathfrompathconstdbPathpath.join(app.getPath(userData),cache.db)constdbnewDatabase(dbPath)db.exec(CREATE TABLE IF NOT EXISTS cache_items ( id TEXT PRIMARY KEY, data TEXT NOT NULL, updated_at INTEGER NOT NULL ))exportfunctiongetAll():CacheItem[]{constrowsdb.prepare(SELECT id, data, updated_at FROM cache_items ORDER BY updated_at DESC)returnrows.all().map((r:any)({id:r.id,...JSON.parse(r.data),updatedAt:r.updated_at}))}exportfunctioninsert(item:CacheItem):void{conststmtdb.prepare(INSERT OR REPLACE INTO cache_items (id, data, updated_at) VALUES (?, ?, ?))stmt.run(item.id,JSON.stringify(item),Date.now())}exportfunctionremove(id:string):void{db.prepare(DELETE FROM cache_items WHERE id ?).run(id)}JSON 文件版// json-cache.tsimport{app}fromelectronimportfsfromfsimportpathfrompathconstcachePathpath.join(app.getPath(userData),cache.json)typeCacheStoreRecordstring,CacheItemfunctionreadStore():CacheStore{try{constrawfs.readFileSync(cachePath,utf-8)returnJSON.parse(raw)}catch{return{}}}functionwriteStore(store:CacheStore):void{fs.writeFileSync(cachePath,JSON.stringify(store))}exportfunctiongetAll():CacheItem[]{conststorereadStore()returnObject.values(store).sort((a,b)b.updatedAt-a.updatedAt)}exportfunctioninsert(item:CacheItem):void{conststorereadStore()store[item.id]{...item,updatedAt:Date.now()}writeStore(store)}exportfunctionremove(id:string):void{conststorereadStore()deletestore[id]writeStore(store)}两段代码看着都挺干净对吧AI 写的注释和类型声明甚至比我手写的还规范。但性能差异大到离谱。benchmark 结果测试环境Electron 28Node 18Windows 11NVMe SSD。每项跑 100 次取中位数。数据量操作SQLite 版JSON 版倍数100 条全量读取3.2ms0.8msJSON 快 4x100 条单条写入0.5ms1.1msSQLite 快 2x500 条全量读取8.7ms2.3msJSON 快 3.8x500 条单条写入0.6ms5.4msSQLite 快 9x2000 条全量读取22.1ms8.9msJSON 快 2.5x2000 条单条写入0.7ms21.3msSQLite 快 30x数字摆出来你可能跟我一样第一反应是JSON 全量读取怎么这么快答案其实很无聊——readFileSync在小文件2000 条 × 200 字节 ≈ 400KB场景下本质是一次mmap 一次JSON.parseV8 对小 JSON 的解析做了高度优化底层的 JSON parser 直接走的是 SIMD 批量解码快得离谱。而 SQLite 每次查询要走prepare → bind → step → finalize的完整链路还要反序列化 TEXT 字段再JSON.parse链路长了一大截。我之前读 better-sqlite3 的文档时看到作者说它是同步 API 里最快的 SQLite binding这话没错但再快也快不过一次裸 mmap。但你注意到单条写入那列了吗500 条数据时 JSON 版已经慢了 9 倍2000 条时直接 30 倍。因为 JSON 版每次写入都要readFileSync全量 writeFileSync全量数据量越大越惨。你想想写一条记录要把整个文件序列化一遍再落盘这操作本质上就是 O(n) 的全量重写。SQLite 是原地更新一行快得多。等一下这里我漏说一个前提。上面的 benchmark 是纯同步操作没有并发。但真实场景里用户可能在列表滚动时触发写入标记已读同时冷启动在做全量读取。JSON 版的writeFileSync会阻塞主进程2000 条数据写一次卡 21ms掉帧肉眼可见。SQLite 的better-sqlite3也是同步 API但单行写入 0.7ms基本无感。我当时差点就拍板用 JSON 版了毕竟我的场景读取远多于写入。但跑到并发测试时JSON 版直接暴露了第二个问题——写覆盖。第二个坑写覆盖JSON 版的insert函数是readStore → 修改 → writeStore经典的 read-modify-write。两个异步操作同时进来后写的会覆盖先写的。我写了个并发测试验证// concurrency-test.tsimport*ascachefrom./json-cache// 模拟并发写入 100 条数据constitemsArray.from({length:100},(_,i)({id:item-${i},title:测试条目${i},content:x.repeat(200),updatedAt:0}))// 并发触发awaitPromise.all(items.map(itemnewPromise(resolve{setImmediate((){cache.insert(item)resolve(undefined)})})}))constallcache.getAll()console.log(写入 100 条实际存了${all.length}条)// 输出写入 100 条实际存了 3 条100 条写进去只剩 3 条。因为 97 条都被并发写覆盖了。我当时盯着这个数字看了好一会儿心想这要是上线了用户存了半天的浏览记录刷新一下全没了工单系统能给我打爆。SQLite 版不存在这个问题better-sqlite3的同步写入天然串行化不需要额外加锁。当然这不是 JSON 方案本身的锅是 AI 生成代码时没考虑并发。你给 JSON 版加个写队列就行// json-cache-fixed.tsletwriteQueue:PromisevoidPromise.resolve()functionwriteStoreQueued(store:CacheStore):Promisevoid{writeQueuewriteQueue.then((){writeStore(store)returnPromise.resolve()})returnwriteQueue}exportasyncfunctioninsertAsync(item:CacheItem):Promisevoid{conststorereadStore()store[item.id]{...item,updatedAt:Date.now()}awaitwriteStoreQueued(store)}但加完队列之后单条写入的延迟从 21ms 涨到了 24ms队列等待开销而 SQLite 还是 0.7ms。这时候选谁已经很明显了。第三个坑数据膨胀后的读取性能衰减这个坑 benchmark 没测出来是我后来手动拿 10000 条数据跑的。10000 条数据JSON 文件体积约 2MB。JSON.parse一个 2MB 的字符串大概要 45ms加上readFileSync本身的 I/O全量读取接近 60ms。SQLite 查 10000 行带排序22ms。数据量到这个级别JSON 版的读取优势完全消失了。而且 JSON 版有个隐藏问题——内存占用。每次getAll都要把全量数据 parse 成 JS 对象10000 条直接吃掉 15MB 堆内存。SQLite 是游标式遍历可以分批limit/offset内存可控。说白了JSON 文件方案的性能优势只存在于一个很窄的区间数据量小于 2000 条、读取远多于写入、且不需要并发写入。出了这个区间SQLite 全面碾压。我最后选了什么结论可能你已经猜到了——SQLite。虽然 JSON 版在我当前的数据量下读取更快但我没法保证数据量永远不超过 2000 条。一旦用户是重度使用者累积个几千条浏览记录很正常。与其等将来踩坑再迁移不如一开始就用稳的方案。那 3ms 的读取差异用户根本感知不到。倒是有一个意外收获。我在对比两版代码时发现AI 生成的 SQLite 版有个小问题它给updated_at建了普通索引但排序查询用的是ORDER BY updated_at DESC没利用到索引因为 SQLite 的索引默认是 ASC。手动加了个CREATE INDEX ... ON cache_items(updated_at DESC)之后全量读取从 22ms 降到了 16ms。你看AI 写的代码能跑但不代表跑得好。它知道要建索引但不知道方向得跟查询一致。这种细节你不亲自跑一遍 benchmark 是发现不了的。顺便一提我做的 App 叫雷达鸭这个缓存层就是给它加的华为应用市场能搜到。你要是也在纠结 Electron 本地缓存用什么方案别光听别人说SQLite 万能。拿你自己的数据量和读写比例跑一遍数字比任何人的经验都靠谱。反正我以后写缓存层之前第一件事就是先写 benchmark——这习惯是被 AI 搞出来的因为你不测就不知道它给你写的代码到底行不行。你有没有遇到过类似的选择两个方案都能跑纸面数据一边倒实际用起来完全是另一回事。欢迎留言聊聊。个人介绍老三10 年软件开发经验软件设计师 / 人工智能应用工程师。专注鸿蒙 ArkTS 北向开发与 Web 前端业余折腾 AI 自动化。不定期在 CSDN 分享鸿蒙和 AI 方向的技术笔记。本文遵循 MIT 协议转载请注明出处。