Skip to content

Commit 4de8fba

Browse files
authored
update stale OCC guidance in performance audit skill (#5)
* update performance audit skill for current no-op write behavior Replace stale no-op write guidance with invalidation-focused advice and remove outdated OCC recommendations now that unchanged writes already no-op. Made-with: Cursor * refine deferred work example in occ conflicts skill Swap the scheduled work example to a clearer user-name update flow so the guidance focuses on deferring obviously secondary analytics work off the hot path. Made-with: Cursor
1 parent 56b304a commit 4de8fba

3 files changed

Lines changed: 34 additions & 48 deletions

File tree

skills/convex-performance-audit/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ After finding one problem, inspect both sibling readers and sibling writers for
107107
Examples:
108108

109109
- If one list query switches from full docs to a digest table, inspect the other list queries for that table
110-
- If one mutation needs no-op write protection, inspect the other writers to the same table
110+
- If one mutation isolates a frequently-updated field or splits a hot document, inspect the other writers to the same table
111111
- If one read path needs a migration-safe rollout for an unbackfilled field, inspect sibling reads for the same rollout risk
112112

113113
Do not leave one path fixed and another path on the old pattern unless there is a clear product reason.
@@ -119,7 +119,7 @@ Confirm all of these:
119119
1. Results are the same as before, no dropped records
120120
2. Eliminated reads or writes are no longer in the path where expected
121121
3. Fallback behavior works when denormalized or indexed fields are missing
122-
4. New writes avoid unnecessary invalidation when data is unchanged
122+
4. Frequently-updated fields are isolated from widely-read documents where needed
123123
5. Every relevant sibling reader and writer was inspected, not just the original function
124124

125125
## Reference Files

skills/convex-performance-audit/references/hot-path-rules.md

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -241,35 +241,33 @@ const projects = await ctx.db
241241
.take(20);
242242
```
243243

244-
## 4. Skip No-Op Writes
244+
## 4. Isolate Frequently-Updated Fields
245245

246-
No-op writes still cost work in Convex:
246+
Convex already no-ops unchanged writes. The invalidation problem here is real writes hitting documents that many queries subscribe to.
247247

248-
- invalidation
249-
- replication
250-
- trigger execution
251-
- downstream sync
248+
Move high-churn fields like `lastSeen`, counters, presence, or ephemeral status off widely-read documents when most readers do not need them.
252249

253-
Before `patch` or `replace`, compare against the existing document and skip the write if nothing changed.
254-
255-
Apply this across sibling writers too. One careful writer does not help much if three other mutations still patch unconditionally.
250+
Apply this across sibling writers too. Splitting one write path does not help much if three other mutations still update the same widely-read document.
256251

257252
```ts
258-
// Bad: patching unchanged values still triggers invalidation and downstream work
259-
await ctx.db.patch(settings._id, {
260-
theme: args.theme,
261-
locale: args.locale,
253+
// Bad: every presence heartbeat invalidates subscribers to the whole profile
254+
await ctx.db.patch(user._id, {
255+
name: args.name,
256+
avatarUrl: args.avatarUrl,
257+
lastSeen: Date.now(),
262258
});
263259
```
264260

265261
```ts
266-
// Good: only write when something actually changed
267-
if (settings.theme !== args.theme || settings.locale !== args.locale) {
268-
await ctx.db.patch(settings._id, {
269-
theme: args.theme,
270-
locale: args.locale,
271-
});
272-
}
262+
// Good: keep profile reads stable, move heartbeat updates to a separate document
263+
await ctx.db.patch(user._id, {
264+
name: args.name,
265+
avatarUrl: args.avatarUrl,
266+
});
267+
268+
await ctx.db.patch(presence._id, {
269+
lastSeen: Date.now(),
270+
});
273271
```
274272

275273
## 5. Match Consistency To Read Patterns

skills/convex-performance-audit/references/occ-conflicts.md

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -73,42 +73,30 @@ await ctx.db.patch(shardId, { count: shard!.count + 1 });
7373

7474
Aggregate the shards in a query or scheduled job when you need the total.
7575

76-
### 3. Skip no-op writes
76+
### 3. Move non-critical work to scheduled functions
7777

78-
Writes that do not change data still participate in conflict detection and trigger invalidation.
78+
If a mutation does primary work plus secondary bookkeeping (analytics, non-critical notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set.
7979

8080
```ts
81-
// Bad: patches even when nothing changed
82-
await ctx.db.patch(doc._id, { status: args.status });
83-
```
84-
85-
```ts
86-
// Good: only write when the value actually differs
87-
if (doc.status !== args.status) {
88-
await ctx.db.patch(doc._id, { status: args.status });
89-
}
90-
```
91-
92-
### 4. Move non-critical work to scheduled functions
93-
94-
If a mutation does primary work plus secondary bookkeeping (analytics, notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set.
95-
96-
```ts
97-
// Bad: analytics update in the same transaction as the user action
98-
await ctx.db.patch(userId, { lastActiveAt: Date.now() });
99-
await ctx.db.insert("analytics", { event: "action", userId, ts: Date.now() });
81+
// Bad: canonical write and derived work happen in the same transaction
82+
await ctx.db.patch(userId, { name: args.name });
83+
await ctx.db.insert("userUpdateAnalytics", {
84+
userId,
85+
kind: "name_changed",
86+
name: args.name,
87+
});
10088
```
10189

10290
```ts
103-
// Good: schedule the bookkeeping so the primary transaction is smaller
104-
await ctx.db.patch(userId, { lastActiveAt: Date.now() });
105-
await ctx.scheduler.runAfter(0, internal.analytics.recordEvent, {
106-
event: "action",
91+
// Good: keep the primary write small, defer the analytics work
92+
await ctx.db.patch(userId, { name: args.name });
93+
await ctx.scheduler.runAfter(0, internal.users.recordNameChangeAnalytics, {
10794
userId,
95+
name: args.name,
10896
});
10997
```
11098

111-
### 5. Combine competing writes
99+
### 4. Combine competing writes
112100

113101
If two mutations must update the same document atomically, consider whether they can be combined into a single mutation call from the client, reducing round trips and conflict windows.
114102

0 commit comments

Comments
 (0)