navigator.locks.query返回空数组不表示无锁阻塞,而是仅显示本页已获未释的锁,不包含其他上下文持有或等待中的锁;mode始终为"exclusive",无法区分读写;需用不同锁名、postmessage跨环境通信、轮询+performance打点综合诊断。

navigator.locks.query 返回空数组但页面明显卡住
这通常不是锁没生效,而是 navigator.locks.query 只返回当前页面(同一 origin + 同一 execution context)中**已获取且尚未释放**的锁,不包含其他 tab、iframe 或 service worker 持有的锁。如果你在调试一个被其他 tab 抢占了 "db-write" 锁的页面,query() 会返回 [],但它确实被阻塞了——因为 await navigator.locks.request("db-write", ...) 在等待,而 query 不管“等待中”的锁。
实操建议:
- 先用
chrome://inspect检查是否多个 tab 同时运行了相同锁逻辑,尤其注意后台静默刷新的 tab - 在
navigator.locks.request外层加console.time("lock-wait"),并在 callback 开头加console.timeEnd("lock-wait"),确认卡顿是否真发生在等待阶段 - 不要只依赖
query()判断“有没有锁”,它更适合验证本页锁是否意外残留,而非诊断阻塞源
query() 结果里 lock.mode 始终是 "exclusive",无法区分读写意图
navigator.locks 的设计不支持语义化锁类型。无论你传 { mode: "shared" } 还是 { mode: "exclusive" },query() 返回的 mode 字段**永远是 "exclusive"**——这是规范行为,不是 bug。浏览器内部只实现互斥语义,shared 模式仅用于协调(比如允许多个 reader 并发),但 query 不暴露该维度。
实操建议:
- 别把
mode当作调试依据;真正影响并发的是锁名(name)是否相同 + 请求时的mode组合规则 - 为读/写操作使用**不同锁名**,例如
"cache-read"和"cache-write",这样query()能直观反映哪类操作卡住了 - 如果必须共享锁名,可在
request()的ifAvailable回调里手动打日志,记录 mode 和上下文,弥补 query 信息缺失
在 service worker 中调用 query() 拿不到主页面持有的锁
navigator.locks 是按执行环境隔离的:主页面、iframe、service worker 各自维护独立锁空间。你在 service worker 里调用 navigator.locks.query(),只能看到该 service worker 内部通过 self.navigator.locks.request() 获取的锁,对主页面的 navigator.locks.request("sync") 完全不可见。
实操建议:
- 需要跨环境监控时,改用 postMessage 主动上报:主页面在
request()前后发消息给 SW,带上锁名、mode、timestamp - service worker 可监听
message事件聚合锁状态,再通过clients.matchAll()推送给所有页面做 UI 提示 - 避免在 SW 里直接依赖
query()做锁冲突决策,它看不到页面侧真实持有情况
query() 调用后立即返回,但锁状态其实已过期
navigator.locks.query() 是快照式同步调用,返回的是调用瞬间的状态。如果锁在返回后 1ms 就被释放或新请求抢占,这个结果就失效了。它不能替代持续观察——尤其当你要关联“页面卡顿”和“某锁长期占用”时,单次 query 几乎没用。
实操建议:
- 用
setInterval(() => navigator.locks.query().then(console.log), 500)做简易轮询,比单次调用有用得多 - 结合 performance.mark()/measure:在
request()前打点,在 callback 内打点,再用query()结果匹配耗时长的锁名 - 注意性能开销:高频轮询本身可能加重主线程压力,生产环境建议只在 devtools 打开时启用
死锁定位真正的难点不在 query 本身,而在于锁名设计是否正交、跨上下文协作是否有显式通信、以及是否混淆了“持有中”和“等待中”两种状态——后者 query 根本不体现,却恰恰是卡顿主因。










