xiaohongshu
xiaohongshu
fvtt-performance-safe-updates
This skill should be used when adding features that update actors or items, implementing hook handlers, modifying update logic, or replacing embedded documents. Covers ownership guards, no-op checks, batched updates, queueUpdate wrapper, atomic document operations, and letting Foundry handle rend...
Full skill instructions
Ensure document updates in Foundry VTT modules don't cause multi-client update storms or render cascades.
Invoke this skill when implementing ANY of the following in a Foundry VTT module:
Foundry VTT runs in multi-client sessions where hooks fire on ALL connected clients. Without proper guards:
Before any document update, ask: "Should this run on every client?"
// ❌ BAD: Runs on every connected client
Hooks.on("deleteItem", (item, options, userId) => {
item.parent.update({ "system.someField": newValue });
});
// ✅ GOOD: Only owner/GM performs the update
Hooks.on("deleteItem", (item, options, userId) => {
if (!item.parent?.isOwner) return;
item.parent.update({ "system.someField": newValue });
});
Common ownership checks:
item.isOwner - Current user owns this itemitem.parent?.isOwner - Current user owns the parent (actor/container)actor.isOwner - Current user owns this actorgame.user.isGM - Current user is the GMUse GM-only guards for:
Before calling update, check if the value actually changes:
// ❌ BAD: Always updates, even if value unchanged
await actor.update({ "system.selected_load_level": newLevel });
// ✅ GOOD: Skip if already set
if (actor.system.selected_load_level === newLevel) return;
await actor.update({ "system.selected_load_level": newLevel });
For flag-based updates:
// ✅ Skip if flag already matches target state
const currentProgress = actor.getFlag('bitd-alternate-sheets', 'abilityProgress') || {};
if (currentProgress[abilityId] === targetValue) return;
await actor.setFlag('bitd-alternate-sheets', 'abilityProgress', {
...currentProgress,
[abilityId]: targetValue
});
Combine multiple field changes into a single update call:
// ❌ BAD: Three separate updates (3x hooks, 3x database writes)
await actor.update({ "system.harm.level1.value": "Bruised" });
await actor.update({ "system.stress.value": 5 });
await actor.update({ "system.xp.value": 3 });
// ✅ GOOD: Single batched update
await actor.update({
"system.harm.level1.value": "Bruised",
"system.stress.value": 5,
"system.xp.value": 3
});
Wrap ALL document updates in queueUpdate to prevent concurrent update collisions:
import { queueUpdate } from "./update-queue.js";
// ✅ Prevents race conditions in multi-client sessions
await queueUpdate(async () => {
await this.actor.update(updates);
});
What queueUpdate does:
When to use:
When replacing embedded documents (items, effects), NEVER use delete+create:
// ❌ BAD: Delete + Create causes UI flicker and race conditions
await actor.deleteEmbeddedDocuments("Item", [oldItemId]);
await actor.createEmbeddedDocuments("Item", [newItemData]);
// UI renders "empty state" between these calls!
// ✅ GOOD: Update in place (atomic operation)
await actor.updateEmbeddedDocuments("Item", [{
_id: oldItemId,
name: newItemData.name,
img: newItemData.img,
system: newItemData.system
}]);
Use cases:
Only rerender sheets that are owned and currently visible:
// ❌ BAD: Rerenders ALL character sheets (including closed/unowned)
Hooks.on("renderBladesClockSheet", (sheet, html, data) => {
game.actors.forEach(actor => {
actor.sheet.render(false);
});
});
// ✅ GOOD: Only rerender owned, open sheets
Hooks.on("renderBladesClockSheet", (sheet, html, data) => {
game.actors.forEach(actor => {
if (actor.isOwner && actor.sheet.rendered) {
actor.sheet.render(false);
}
});
});
Default behavior: When document.update() is called, Foundry automatically re-renders all registered sheets on ALL connected clients. This is the correct behavior for multi-client synchronization.
Understanding Foundry's render flow:
When document.update() is called, Foundry:
updateActor/updateItem hooks on each clientrender() on sheets registered in doc.appsCritical: The { render: false } option suppresses step 4 on ALL clients, not just the initiating client. This breaks multi-client synchronization.
// ❌ BAD: Suppresses render on ALL clients, breaking multi-client sync
await actor.update({ "system.value": newValue }, { render: false });
// Other players' sheets won't update!
// ✅ GOOD: Let Foundry handle renders automatically
await queueUpdate(async () => {
await actor.update({ "system.value": newValue });
});
// All clients re-render automatically, staying in sync
Only exception - Data Migrations in getData():
When migrating data inside getData(), you must suppress render to prevent infinite loops:
// In getData() - migration MUST suppress render to avoid infinite loop
async getData() {
// Detect old data format that needs migration
if (this.actor.system.oldField !== undefined) {
queueUpdate(() => this.actor.update({
"system.newField": this.actor.system.oldField,
"system.-=oldField": null
}, { render: false }));
}
// ... rest of getData
}
With proper caching, Foundry sheet renders are fast (~2-3ms). There's no need for "optimistic UI" patterns that manipulate DOM before/after updates.
Combine all guards into a single helper:
/**
* Safely updates a document with ownership and no-op guards.
* Lets Foundry handle re-renders automatically for multi-client sync.
*/
export async function safeUpdate(doc, updateData, options = {}) {
// 1. Ownership guard - only owner should update
if (!doc?.isOwner) return false;
// 2. Empty update guard
const entries = Object.entries(updateData || {});
if (entries.length === 0) return false;
// 3. No-op detection - skip if values unchanged
const hasChange = entries.some(([key, value]) => {
// Objects always treated as changes (too complex to deep-compare)
if (value !== null && typeof value === "object") return true;
const currentValue = foundry.utils.getProperty(doc, key);
return currentValue !== value;
});
if (!hasChange) return false;
// 4. Queued update - let Foundry handle renders
await queueUpdate(async () => {
await doc.update(updateData, options);
});
return true;
}
Usage:
// Standard pattern: handles all guards, Foundry re-renders all clients
await safeUpdate(doc, { "system.value": newValue });
// Only use render: false for data migrations in getData()
await safeUpdate(doc, migrationData, { render: false });
For handlers that run frequently (keyup, mousemove), add debouncing:
import { debounce } from "./utils.js";
// ❌ BAD: Updates on every keystroke
html.find("input").on("keyup", async (ev) => {
await actor.update({ "system.notes": ev.target.value });
});
// ✅ GOOD: Debounce to reduce update frequency
html.find("input").on("keyup", debounce(async (ev) => {
await queueUpdate(async () => {
await actor.update({ "system.notes": ev.target.value });
});
}, 300));
Before submitting any code that updates documents, verify:
if (!item.parent?.isOwner) return; or if (!game.user.isGM) return; where appropriatequeueUpdate(async () => { ... })updateEmbeddedDocuments() instead of delete+create for replacements{ render: false } (breaks multi-client sync)html.find(".toggle-something").on("click", async (ev) => {
const currentValue = this.actor.system.someFlag;
const newValue = !currentValue;
// Skip if unchanged
if (currentValue === newValue) return;
await queueUpdate(async () => {
await this.actor.update({ "system.someFlag": newValue });
});
// No manual render - hook handles it
});
Hooks.on("deleteItem", (item, options, userId) => {
// Guard: Only owner performs side effects
if (!item.parent?.isOwner) return;
// Check if update needed
const needsUpdate = /* your logic */;
if (!needsUpdate) return;
// Perform update
queueUpdate(async () => {
await item.parent.update({ /* changes */ });
});
});
async replaceAbility(oldAbilityId, newAbilityData) {
const oldAbility = this.actor.items.get(oldAbilityId);
if (!oldAbility) return;
// Update in place (atomic)
await queueUpdate(async () => {
await this.actor.updateEmbeddedDocuments("Item", [{
_id: oldAbilityId,
name: newAbilityData.name,
img: newAbilityData.img,
system: newAbilityData.system
}]);
});
}
// Every client updates, causing N × clients database writes
Hooks.on("deleteItem", (item) => {
item.parent.update({ ... }); // Missing ownership guard!
});
// Rerenders ALL sheets, including unowned/closed
Hooks.on("updateActor", (actor) => {
game.actors.forEach(a => a.sheet.render(false));
});
// UI flickers; race condition between delete and create
await actor.deleteEmbeddedDocuments("Item", [id]);
await actor.createEmbeddedDocuments("Item", [data]);
// Updates even if value unchanged (wasted database writes)
await actor.update({ "system.xp": actor.system.xp });
// Suppresses render on ALL connected clients, not just initiating client!
await actor.update({ "system.value": newValue }, { render: false });
// Other players' sheets won't update - they'll see stale data
// DOM manipulation before persist causes desync if update fails
checkbox.checked = newValue; // Update DOM first (optimistic)
await actor.update({ "system.equipped": newValue }); // Then persist
// If update fails, DOM shows wrong state; other clients may not sync
After implementing updates, test with multiple clients:
render{ render: false } pattern extensively in migrationsupdateEmbeddedDocuments vs delete+createImplementation notes:
queueUpdate and safeUpdate helpers typically live in a utils moduleLast Updated: 2026-01-14
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience