Performance Guidelines
Core Rules
- Never use
Flamework.createGuard<T>()on hot paths — extremely expensive at runtime - One operation per heartbeat tick — queue-based, never create AND unload in same frame
- Defer expensive work —
task.defer()chains for cell parsing, forest gen, cleanup - Client batch rendering — 25 MeshPart clones per frame max
- Heartbeat throttling — ChunkService 0.25s accumulator, visibility 0.1s interval
Do's
- Cache CollectionService results in Sets via component registration
- Use octrees for spatial queries (baseplates, danger zones)
- Batch network updates (25 entities per frame)
- Use LOD system for distant meshes
- Gate debug logging behind flag checks
- Use
PointToObjectSpacefor rotation-aware bounds checks
Don'ts
- Never call
CollectionService.GetTagged()per frame - Never create AND unload chunks in the same heartbeat tick
- Never use
FindFirstChildin hot loops — cache references - Never send large packets over remotes — batch and throttle
- Avoid
WaitForChildin systems that run per-frame
Throttling Patterns
Accumulator
private accum = 0;
RunService.Heartbeat.Connect((dt) => {
this.accum += dt;
if (this.accum < 0.25) return;
this.accum = 0;
// work here
});
Queue Processing
// One item per tick
if (this.pendingQueue.size() > 0) {
const item = this.pendingQueue.remove(0)!;
this.processItem(item);
}
Batch Rendering
const BATCH_SIZE = 25;
const count = math.min(BATCH_SIZE, this.pending.size());
for (let i = 0; i < count; i++) {
// clone and parent mesh
}
task.defer(() => this.renderNextBatch());
Monitoring
Key MicroProfiler labels to watch:
Script::deferredThreadsHeartbeat::RunService.Heartbeat
Target: Stay under 8ms per frame on Apple M4 / Studio v707.