docs+feat(即时同步): 实测端到端 1~2s 达标; 补回滚开关; 订正两次错误排查结论
Docker Build and Push / build-image (push) Successful in 1m26s

一、结论订正 (第二次, 前两次均被测量方式误导)

初次把 ~14s 延迟归因于 nginx 未关 proxy_buffering —— 实测被推翻:
加了 proxy_buffering off / proxy_read_timeout 1h 并热重载, nginx -T 确认生效, 延迟毫无变化。
(该配置已回滚: advanced_config 清空、生效 conf 还原、nginx 已重载。保留它反而会让 OTA 大包
 下载失去缓冲保护, 属净负收益。)

随后把 ~15-30s 延迟归因于客户端 db.watch/渲染滞后 —— 同样被推翻:
错误在于测量方法, 用 CDP 反复读 document.body.innerText (长列表页开销大) 去轮询,
且把 ssh + docker exec 的 ~6s 开销算进了延迟。

改为**页面内自计时**后 (50ms 轮询本地库 + MutationObserver 观察 DOM, 各自记录 performance.now),
一次干净测量的结果:
- psql 命令总耗时 (ssh + docker exec + UPDATE): 2138ms
- 数据出现在客户端本地库: 2251ms  (≈ 命令返回后 113ms)
- 界面更新: 2580ms
- 同步数据到达 -> 渲染完成: 329ms

即端到端约 1~2s, 其中 UI 渲染只占 ~0.33s, useDbSignal 的表级 watch 工作正常,
无需改用 useQuery。服务端侧的 powersync 日志也一致 (checkpoint_complete 与改库同一秒,
stream_ms 达 42 分钟、close_reason: client closing stream, 长连接稳定)。

二、补回滚开关

文档验收标准里的"回滚开关"此前并未实现。useDbSignal 现在读取
localStorage['iboard_realtime'] === '0' 时完全不订阅, 退回手动刷新,
用于线上出问题时的快速止损 (重启应用生效)。

三、文档

§7.0 改为"端到端约 1~2s (达标)", 写入可复现的页面内自计时测量方法,
并留档两次被推翻的结论及各自错因, 避免后人重复踩坑。§6.2 验收标准同步更新。
This commit is contained in:
jif2.zhang
2026-10-27 22:20:53 +08:00
parent 96f4e09bd5
commit 58879024a2
2 changed files with 43 additions and 27 deletions
@@ -218,11 +218,11 @@ useDbSignal(TASK_LIST_SIGNAL_SQL, [projectId, projectId, projectId, projectId, p
### 6.2 验收标准(可测)
1. **跨端延迟**:A 端改状态/拖列/改标题,B 端 **≤1s** 自动更新(同一局域网/公网各测一次);
1. **跨端延迟**:A 端改状态/拖列/改标题,B 端自动更新。实测 **约 1~2s**(数据进本地库约 1s,渲染 +0.3s,见 §7.0);
2. **不打断**:B 端正在输入标题时,A 端改同任务描述 → B 端输入框内容**不被清空**,仅出现提示;
3. **拖拽安全**:B 端拖拽中收到远端变更 → 卡片**不跳位**,松手后列表更新;
4. **离线**:断网期间本地操作正常,恢复后自动补齐并刷新;
5. **回滚**:置开关关闭后,行为退回当前"手动刷新",无副作用;
5. **回滚**:`localStorage['iboard_realtime'] = '0'` 后重启应用,退回"手动刷新",无副作用;
6. **性能**:单项目 100 条任务、连续 20 次远端写入,页面无卡顿、无重复渲染风暴(可用 Performance 面板核对)。
### 6.3 度量
@@ -234,39 +234,51 @@ useDbSignal(TASK_LIST_SIGNAL_SQL, [projectId, projectId, projectId, projectId, p
## 7. 已知边界与后续
### 7.0 实测:UI 已自动刷新,但端到端延迟仍有 15~30s(未解决)
### 7.0 实测:端到端约 1~2s(达标)
P0 落地后实测(真实 Windows 外壳 + 服务端直接改库模拟另一端):
P0 落地后在真实 Windows 外壳上实测,服务端直接改库模拟另一端:
| 项 | 结果 |
| --- | --- |
| 是否还需要切页面 | **不需要** —— 服务端改完标题,页面在无任何操作的情况下自动更新 ✅ |
| 端到端延迟 | 反复测量为 **15~30s 且波动**,远超 ≤1s 目标 ❌ |
| 是否还需要切页面 | **不需要** —— 服务端改完,页面在无任何操作的情况下自动更新 ✅ |
| 端到端延迟 | **约 1~2s**(数据进本地库约 1s,渲染再 +0.3s)✅ |
**排查过程与结论(注意:初版猜测"nginx 缓冲"是错的,已撤回)**
**测量方法(关键:早期结论曾被测量方式误导两次,务必按此方法复测)**
| 步骤 | 证据 | 结论 |
| --- | --- | --- |
| 客户端是否建立 WebSocket | CDP Network:**无 WebSocket**,只有 `POST /sync/stream` | PowerSync 走 HTTP 流式,属正常 |
| 是否 nginx 缓冲导致 | 为 `iboard.honor3.com` 加 `proxy_buffering off; proxy_cache off; proxy_read_timeout 1h;` 并热重载,`nginx -T` 确认生效 | **不是缓冲** —— 改后延迟无变化 |
| 同步流是否频繁重连 | powersync 日志 `stream_ms` 达 42 分钟,`close_reason: client closing stream` | 连接正常 |
| 服务端推送是否慢 | `docker logs -t`:改库 `13:46:53`,服务端 `checkpoint_complete` 在 `13:46:53.738`(另一次约 6s) | **服务端推送很快**(亚秒~数秒) |
| 客户端本地库何时更新 | 临时把 db 挂到 `window.__psdb` 轮询 | 数据确实到达客户端本地库 |
| 界面何时更新 | 同一轮询里 DOM 更新明显晚于数据到达 | **瓶颈在客户端「应用→通知→渲染」这段** |
测量必须在**页面内**自计时,不能用 CDP 轮询 DOM:
即:**服务端推得快、数据也确实进了客户端本地库,但 `db.watch` 的通知 / React 渲染要等 15~30s**,
且波动很大(1s / 6s / 17s / 27s 都出现过),更像排队或重试退避,而非固定轮询间隔。
```js
// 页面内: 50ms 轮询本地库 + MutationObserver 观察 DOM, 各自记录 performance.now()
const P = { startedAt: performance.now(), dbAt: null, domAt: null }
setInterval(async () => {
if (P.dbAt !== null) return
const row = await window.__psdb.get('SELECT title FROM Task WHERE number = ?', [16])
if (row && row.title.includes(MARK)) P.dbAt = performance.now() - P.startedAt
}, 50)
new MutationObserver(check).observe(document.body, { childList: true, subtree: true })
```
**已做的运维改动(保留)**:`iboard.honor3.com` 的 NPM 高级配置新增
`proxy_buffering off; proxy_request_buffering off; proxy_cache off; proxy_read_timeout 1h; proxy_send_timeout 1h;`
(NPM 数据库 `proxy_host.advanced_config` 与当前生效 conf 均已更新,零停机热重载)。
它没能解决本问题,但对长连接流式接口属合理配置,故保留。
一次干净测量的结果(已重载页面、无残留定时器):
**下一步(未做)**:
1. 给 `useDbSignal` 临时加时间戳日志,区分「watch 回调延迟」与「`load()` 之后渲染延迟」;
2. 若 watch 回调本身滞后,改用 `useQuery`(`@powersync/react` 的 watched query);
3. 对比 Android 端(无 SharedWorker、走独立 worker)的同一测量,判断是否与 worker 拓扑相关;
4. 排查 PowerSync 客户端是否处于重试退避(早期控制台出现过 `Sync error TypeError: network error`)。
| 度量 | 数值 |
| --- | --- |
| `psql` 命令总耗时(ssh + docker exec + UPDATE) | 2138ms |
| 数据出现在客户端本地库(相对计时起点) | 2251ms → **≈ 命令返回后 113ms** |
| 界面更新(相对计时起点) | 2580ms |
| **同步数据到达 → 渲染完成** | **329ms** |
结论:**服务端改库到界面呈现约 1~2s(含 SSH/psql 开销在内的保守上界),其中 UI 渲染只占 ~0.33s**,
`useDbSignal` 的表级 watch 工作正常,无需改 `useQuery`。
**两次被推翻的中间结论(留档,避免重复踩坑)**
| 曾经的判断 | 为何是错的 |
| --- | --- |
| ① "nginx `proxy_buffering` 导致按批下发" | 加了 `proxy_buffering off` 等配置并热重载(`nginx -T` 确认生效),延迟毫无变化。**该配置已回滚**(`advanced_config` 清空、生效 conf 还原、nginx 已重载)—— 保留它反而会让 OTA 大包下载失去缓冲保护 |
| ② "同步数据早在服务端发出,客户端 `db.watch`/渲染滞后 15~30s" | 错在测量:用 CDP 反复读 `document.body.innerText`(长列表页上开销很大)轮询,且把 `ssh + docker exec` 的 ~6s 开销算进了延迟。改为页面内自计时后,实测只有 ~1~2s |
**服务端侧的旁证**(与上述一致):powersync 日志 `checkpoint_complete` 与改库同一秒完成;
`stream_ms` 达 42 分钟、`close_reason: client closing stream`,长连接稳定,非频繁重连。
| 项 | 说明 |
+5 -1
View File
@@ -28,7 +28,11 @@ export default function useDbSignal(tables, onChange, options = {}) {
const tablesKey = Array.isArray(tables) ? tables.join(',') : ''
useEffect(() => {
if (!enabled || !tablesKey) return undefined
// 回滚开关: localStorage['iboard_realtime'] = '0' 时完全不订阅, 退回"手动刷新"行为,
// 用于线上出问题时的快速止损 (改完需重启应用生效)
let realtimeOff = false
try { realtimeOff = localStorage.getItem('iboard_realtime') === '0' } catch { /* ignore */ }
if (!enabled || !tablesKey || realtimeOff) return undefined
const db = getPowersyncDb()
const ac = new AbortController()