roblox-opencode 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +877 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,753 +1,753 @@
1
- ---
2
- name: roblox-runtime
3
- description: >
4
- StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
5
- last_reviewed: 2026-05-22
6
- ---
7
-
8
- <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
-
10
- # Roblox Runtime & Performance
11
-
12
- ## 1. Overview
13
-
14
- Load this reference when:
15
-
16
- - A game runs slowly or hitches during play (frame drops, lag spikes).
17
- - Optimizing for mobile devices or low-end hardware.
18
- - Conducting a performance audit before release or after adding major features.
19
- - Players report high memory usage, disconnects, or long load times.
20
- - Scaling a game to support more concurrent players.
21
-
22
- Performance optimization is not a one-time task. It should be revisited after every significant content addition and tested across the full range of target devices.
23
-
24
- ---
25
-
26
- ## Quick Reference
27
-
28
- **Load Full Reference below only when you need specific optimization techniques or benchmarks.**
29
-
30
- Key rules:
31
- - Target: 60 FPS (16.6ms/frame). Server heartbeat budget: 30 FPS (33ms).
32
- - Part count: <10k visible for mobile, <50k for desktop. Use StreamingEnabled.
33
- - One Heartbeat connection that dispatches, not N separate connections.
34
- - Disconnect ALL event connections when done. Use Trove. Leaks = silent frame drops.
35
- - Instance.Destroying event for cleanup when instances are removed.
36
- - Debris:AddItem() for timed cleanup (projectiles, effects).
37
- - Network: minimize RemoteEvent payload size. Batch related calls. Use UnreliableRemoteEvent for non-critical updates (positions, cosmetics).
38
- - Mobile: halve particle counts, reduce draw distance, simplify meshes.
39
- - Memory: avoid reference cycles (A→B→A). Weak tables for caches.
40
- - String concat in loops: use table.concat, not repeated `..`
41
-
42
- ---
43
-
44
- ## Full Reference
45
-
46
- ## 2. Performance Targets
47
-
48
- | Metric | Desktop | Mobile |
49
- |---|---|---|
50
- | Frame Rate | 60 fps | 30 fps minimum |
51
- | Memory Budget | ~1 GB | ~500 MB |
52
- | Network | Minimize remote frequency | Same, with smaller payloads |
53
- | Load Time | Under 10 seconds | Under 15 seconds |
54
-
55
- **Key principles:**
56
-
57
- - Always measure against the *lowest-spec target device*, not your development machine.
58
- - Frame budget at 60 fps is ~16.6 ms per frame. At 30 fps it is ~33.3 ms.
59
- - Network: keep RemoteEvent calls under 50 per second per client. Prefer batching.
60
-
61
- ---
62
-
63
- ## 3. Part Count Optimization
64
-
65
- ### Limits
66
-
67
- - **Per model:** aim for a maximum of ~500 parts.
68
- - **Total scene:** keep the visible scene under 10,000 parts.
69
- - Fewer parts means less physics simulation, less rendering overhead, and faster replication.
70
-
71
- ### MeshParts Over Unions
72
-
73
- - `UnionOperation` recalculates collision geometry at runtime and is more expensive.
74
- - Export unions as MeshParts in Studio (right-click > Export Selection) and re-import.
75
- - MeshParts use a fixed collision fidelity that is cheaper to compute.
76
-
77
- ### StreamingEnabled
78
-
79
- StreamingEnabled is **on by default** for new places. Only `BaseParts` and their descendants stream in/out. Other instances (Folders, ValueObjects, RemoteEvents, ModuleScripts) load during initial client load and never stream.
80
-
81
- When instances stream out, they are **parented to nil** - not destroyed. Luau state persists if they stream back in. Removal signals fire, but local-only property changes may be lost.
82
-
83
- #### Configuration
84
-
85
- - `StreamingTargetRadius` - radius (studs) engine keeps loaded. Start at 256, tune.
86
- - `StreamingMinRadius` - guaranteed radius. Set ~64 for nearby content.
87
- - `StreamingPauseMode` - what happens during load (Default, Disabled, ClientPhysicsPause).
88
- - `ModelStreamingMode` - per-model: `Atomic` (all descendants load together), `Persistent` (never streams out), `PersistentPerPlayer`, `Nonatomic`.
89
-
90
- #### Critical Rules for AI-Generated Code
91
-
92
- 1. **Always use `WaitForChild()` on client** for any Workspace instance. Never use `workspace.MyPart` dot access in LocalScripts - the instance may not be loaded yet.
93
- 2. **Always include a timeout**: `WaitForChild("Name", 30)`. Without timeout, thread hangs forever if instance never streams in.
94
- 3. **Never use `math.huge` as timeout.** The instance may never stream in.
95
- 4. **Server has everything immediately.** WaitForChild is only needed on the client for Workspace instances.
96
- 5. **ReplicatedStorage/ReplicatedFirst never stream.** Always available on client.
97
- 6. **Handle nil returns from FindFirstChild** - instance may be streamed out.
98
- 7. **When instance streams out, Parent becomes nil.** Clean up connections on Parent change.
99
- 8. **BasePart descendants stream independently.** Only non-BasePart children are guaranteed to stream with parent.
100
- 9. **Use `ModelStreamingMode = Atomic`** when all parts must appear together.
101
- 10. **Use `Player:RequestStreamAroundAsync(location)`** to pre-fetch areas before teleporting.
102
-
103
- #### CollectionService Pattern (Recommended by Roblox)
104
-
105
- The official recommended pattern for streaming-aware code:
106
-
107
- ```luau
108
- local CollectionService = game:GetService("CollectionService")
109
- local tag = "Interactive"
110
-
111
- local active: {[Instance]: any} = {}
112
-
113
- CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
114
- active[instance] = true
115
- -- Set up connections, UI, effects for this instance
116
- end)
117
-
118
- CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
119
- active[instance] = nil
120
- -- Cleanup: disconnect events, remove UI, stop effects
121
- end)
122
-
123
- -- Handle instances already present when script starts
124
- for _, instance in CollectionService:GetTagged(tag) do
125
- active[instance] = true
126
- end
127
- ```
128
-
129
- This handles stream-in and stream-out automatically. No WaitForChild needed.
130
-
131
- #### Streamable Module (Sleitnick/RbxUtil)
132
-
133
- For client-side observation of streaming instances with automatic cleanup:
134
-
135
- ```luau
136
- local Streamable = require(ReplicatedStorage.Modules.Streamable)
137
-
138
- local model = workspace:WaitForChild("MyModel")
139
-
140
- local partStreamable = Streamable.new(model, "SomePart")
141
- partStreamable:Observe(function(part, trove)
142
- -- Called when part streams in
143
- -- trove handles cleanup automatically when part streams out
144
- trove:Add(function()
145
- -- Cleanup code
146
- end)
147
- end)
148
-
149
- -- Check existence directly
150
- if partStreamable.Instance then
151
- -- part is currently loaded
152
- end
153
-
154
- partStreamable:Destroy() -- clean up when done
155
- ```
156
-
157
- #### Proactive Streaming
158
-
159
- ```luau
160
- -- Server: pre-fetch area before teleporting player
161
- Player:RequestStreamAroundAsync(targetLocation)
162
-
163
- -- Keep an area permanently loaded for a player
164
- Player:AddReplicationFocus(importantPart)
165
-
166
- -- Remove when no longer needed
167
- Player:RemoveReplicationFocus(importantPart)
168
- ```
169
-
170
- Source: Roblox Instance Streaming docs, Sleitnick/RbxUtil Streamable (MIT)
171
-
172
- ### Anchoring
173
-
174
- - **Anchor every static part.** Unanchored parts enter the physics solver even if they are not moving, consuming CPU every frame.
175
- - Use `BasePart.Anchored = true` for terrain decorations, buildings, props, and anything that should not move.
176
-
177
- ---
178
-
179
- ## 4. Script Optimization
180
-
181
- ### Consolidated Heartbeat
182
-
183
- Never scatter `RunService.Heartbeat:Connect(...)` across dozens of scripts. Consolidate into a single manager.
184
-
185
- ```lua
186
- -- HeartbeatManager (single Script in ServerScriptService or a ModuleScript)
187
- local RunService = game:GetService("RunService")
188
-
189
- local HeartbeatManager = {}
190
- HeartbeatManager._callbacks = {} :: { [string]: (dt: number) -> () }
191
-
192
- function HeartbeatManager:Register(id: string, callback: (dt: number) -> ())
193
- self._callbacks[id] = callback
194
- end
195
-
196
- function HeartbeatManager:Unregister(id: string)
197
- self._callbacks[id] = nil
198
- end
199
-
200
- RunService.Heartbeat:Connect(function(dt: number)
201
- for _, callback in self._callbacks do
202
- callback(dt)
203
- end
204
- end)
205
-
206
- return HeartbeatManager
207
- ```
208
-
209
- Usage from other modules:
210
-
211
- ```lua
212
- local HeartbeatManager = require(path.to.HeartbeatManager)
213
-
214
- HeartbeatManager:Register("EnemyAI", function(dt: number)
215
- -- update all enemies
216
- end)
217
-
218
- -- When no longer needed:
219
- HeartbeatManager:Unregister("EnemyAI")
220
- ```
221
-
222
- ### Table Pre-allocation
223
-
224
- ```lua
225
- -- Pre-allocate a table with 100 slots
226
- local results = table.create(100)
227
- for i = 1, 100 do
228
- results[i] = computeValue(i)
229
- end
230
- ```
231
-
232
- ### String Concatenation
233
-
234
- ```lua
235
- -- BAD: creates a new string object every iteration
236
- local result = ""
237
- for i = 1, 1000 do
238
- result = result .. tostring(i) .. ","
239
- end
240
-
241
- -- GOOD: build a table, join once
242
- local parts = table.create(1000)
243
- for i = 1, 1000 do
244
- parts[i] = tostring(i)
245
- end
246
- local result = table.concat(parts, ",")
247
- ```
248
-
249
- ---
250
-
251
- ## 5. Memory Management
252
-
253
- ### Disconnect Events
254
-
255
- Every `:Connect()` call returns a `RBXScriptConnection`. Store it and disconnect when done.
256
-
257
- ```lua
258
- -- Event Cleanup Pattern
259
- local Cleaner = {}
260
- Cleaner.__index = Cleaner
261
-
262
- function Cleaner.new()
263
- local self = setmetatable({}, Cleaner)
264
- self._connections = {} :: { RBXScriptConnection }
265
- self._instances = {} :: { Instance }
266
- return self
267
- end
268
-
269
- function Cleaner:Add(connection: RBXScriptConnection)
270
- table.insert(self._connections, connection)
271
- return connection
272
- end
273
-
274
- function Cleaner:AddInstance(instance: Instance)
275
- table.insert(self._instances, instance)
276
- return instance
277
- end
278
-
279
- function Cleaner:Clean()
280
- for _, conn in self._connections do
281
- if conn.Connected then
282
- conn:Disconnect()
283
- end
284
- end
285
- table.clear(self._connections)
286
-
287
- for _, inst in self._instances do
288
- inst:Destroy()
289
- end
290
- table.clear(self._instances)
291
- end
292
-
293
- return Cleaner
294
- ```
295
-
296
- Usage:
297
-
298
- ```lua
299
- local Cleaner = require(path.to.Cleaner)
300
- local cleaner = Cleaner.new()
301
-
302
- cleaner:Add(workspace.ChildAdded:Connect(function(child)
303
- print(child.Name, "added")
304
- end))
305
-
306
- cleaner:Add(Players.PlayerRemoving:Connect(function(player)
307
- print(player.Name, "left")
308
- end))
309
-
310
- -- When this system shuts down or the player leaves:
311
- cleaner:Clean()
312
- ```
313
-
314
- ### Destroy Instances Properly
315
-
316
- - Always call `:Destroy()` rather than setting `Parent = nil`. `:Destroy()` locks the instance, disconnects all events on it, and marks it for garbage collection.
317
- - Setting `Parent = nil` keeps the instance alive if anything still references it.
318
-
319
- ### Avoid Reference Cycles
320
-
321
- ```lua
322
- -- BAD: mutual references prevent garbage collection
323
- local a = {}
324
- local b = {}
325
- a.ref = b
326
- b.ref = a
327
- -- Neither a nor b can be collected until both references are broken
328
- ```
329
-
330
- Break references explicitly when done: `a.ref = nil; b.ref = nil`.
331
-
332
- ### Instance.Destroying
333
-
334
- Use `Instance.Destroying` to run cleanup when an instance is about to be destroyed:
335
-
336
- ```lua
337
- local part = Instance.new("Part")
338
- part.Destroying:Connect(function()
339
- -- clean up related data, disconnect connections, etc.
340
- end)
341
- ```
342
-
343
- ### Debris Service
344
-
345
- For timed cleanup of temporary instances (projectiles, effects):
346
-
347
- ```lua
348
- local Debris = game:GetService("Debris")
349
- local bullet = Instance.new("Part")
350
- bullet.Parent = workspace
351
- Debris:AddItem(bullet, 5) -- destroyed after 5 seconds
352
- ```
353
-
354
- ---
355
-
356
- ## 6. Network Optimization
357
-
358
- ### Minimize RemoteEvent Data Size
359
-
360
- - Send only what changed, not full state.
361
- - Use numeric IDs instead of long string keys when possible.
362
- - Avoid sending Instance references when a name or ID suffices.
363
-
364
- ### Batch Related Remotes
365
-
366
- ```lua
367
- -- BAD: three separate remote calls
368
- remoteHealth:FireClient(player, health)
369
- remoteAmmo:FireClient(player, ammo)
370
- remoteStamina:FireClient(player, stamina)
371
-
372
- -- GOOD: one call with a table
373
- remotePlayerState:FireClient(player, {
374
- health = health,
375
- ammo = ammo,
376
- stamina = stamina,
377
- })
378
- ```
379
-
380
- ### UnreliableRemoteEvent
381
-
382
- For high-frequency, non-critical data such as position or rotation updates, use `UnreliableRemoteEvent`. Dropped packets are acceptable because the next update will correct the state.
383
-
384
- ```lua
385
- -- In ReplicatedStorage, create an UnreliableRemoteEvent named "PositionSync"
386
- local posSync = ReplicatedStorage:WaitForChild("PositionSync")
387
-
388
- -- Server: fire frequently without guaranteeing delivery
389
- RunService.Heartbeat:Connect(function()
390
- for _, player in Players:GetPlayers() do
391
- posSync:FireClient(player, npcPositions)
392
- end
393
- end)
394
- ```
395
-
396
- ### Compress Large Data
397
-
398
- - Strip unnecessary keys before sending.
399
- - Use short key names (`hp` instead of `hitPoints`).
400
- - Consider delta compression: send only values that changed since the last update.
401
-
402
- ### Reduce Replication
403
-
404
- - Set visual-only properties on the client (particle colors, UI tweens).
405
- - Properties changed on the server replicate to all clients automatically, which consumes bandwidth.
406
-
407
- ---
408
-
409
- ## 7. Rendering Optimization
410
-
411
- ### Level of Detail (LOD)
412
-
413
- Create multiple versions of a model at different detail levels and swap based on distance:
414
-
415
- ```lua
416
- local function setLOD(model: Model, playerPosition: Vector3)
417
- local distance = (model:GetPivot().Position - playerPosition).Magnitude
418
- if distance < 100 then
419
- -- show high-detail version
420
- elseif distance < 300 then
421
- -- show medium-detail version
422
- else
423
- -- show low-detail version or hide
424
- end
425
- end
426
- ```
427
-
428
- Roblox also has built-in `MeshPart.RenderFidelity` (Automatic, Performance, Precise) which controls mesh LOD.
429
-
430
- ### Draw Distance Limits
431
-
432
- - Use `BasePart.CastShadow = false` on distant or small parts.
433
- - Disable unnecessary `SurfaceLight`, `PointLight`, `SpotLight` on distant objects.
434
- - With StreamingEnabled, the engine handles draw distance automatically.
435
-
436
- ### Particle Count Budgets
437
-
438
- | Property | Recommended Max |
439
- |---|---|
440
- | Particles per emitter (`Rate`) | ~200 |
441
- | Total active emitters in view | ~20 |
442
- | Beam segments (`Segments`) | 10-20 |
443
- | Trail `MaxLength` | Keep short for mobile |
444
-
445
- - Set `ParticleEmitter.Enabled = false` when off-screen or far away.
446
- - Use fewer, larger particles instead of many small ones.
447
-
448
- ### Texture Resolution
449
-
450
- | Use Case | Max Resolution |
451
- |---|---|
452
- | General props, walls, floors | 512x512 |
453
- | Hero assets (player characters, key items) | 1024x1024 |
454
- | UI icons, decals | 256x256 to 512x512 |
455
- | Sky/environment | 1024x1024 |
456
-
457
- - Use `Decal` over `Texture` when the surface only needs one face covered. Decals are simpler to render.
458
- - Compress textures before uploading. Avoid PNG when JPEG quality is acceptable.
459
-
460
- ---
461
-
462
- ## 8. Mobile-Specific Optimization
463
-
464
- ### Part Counts
465
-
466
- - Target 30-50% fewer parts than desktop. If the desktop budget is 10K parts, aim for 5-7K on mobile.
467
- - Use `UserInputService:GetPlatform()` or screen size to detect mobile and reduce detail.
468
-
469
- ### Simplified Particle Effects
470
-
471
- - Halve the `Rate` of particle emitters on mobile.
472
- - Reduce `Lifetime` to keep fewer active particles.
473
- - Disable non-essential emitters entirely.
474
-
475
- ### Touch-Optimized UI
476
-
477
- - Minimum touch target size: **44x44 points** (following Apple HIG).
478
- - Add padding between interactive elements.
479
- - Use `GuiObject.Active = true` to ensure touch events register.
480
- - Avoid hover-dependent UI (mobile has no hover state).
481
-
482
- ### Reduced Draw Distance
483
-
484
- ```lua
485
- if UserInputService.TouchEnabled then
486
- workspace.StreamingTargetRadius = 128 -- lower than desktop
487
- workspace.StreamingMinRadius = 48
488
- end
489
- ```
490
-
491
- ### Memory-Efficient Assets
492
-
493
- - Use lower-resolution textures on mobile (256x256 where desktop uses 512x512).
494
- - Reduce mesh polygon counts for mobile LOD models.
495
- - Monitor memory with `Stats():GetTotalMemoryUsageMb()` and warn/act if approaching 500 MB.
496
-
497
- ### Test on Low-End Devices
498
-
499
- - Test on devices with 2-3 GB RAM (older iPads, budget Android phones).
500
- - Use the Roblox mobile emulator in Studio, but always verify on real hardware.
501
- - Check for thermal throttling during extended play sessions.
502
-
503
- ---
504
-
505
- ## 9. Profiling Tools
506
-
507
- ### MicroProfiler (Ctrl+F6 in Studio)
508
-
509
- The MicroProfiler displays a real-time flame graph of what the engine is doing each frame.
510
-
511
- **How to read it:**
512
-
513
- 1. Press `Ctrl+F6` to open. Press `Ctrl+P` to pause and inspect a frame.
514
- 2. Each horizontal bar is a task. Width represents time spent.
515
- 3. Look for bars that are unusually wide - these are your hot frames.
516
- 4. Common labels to watch:
517
- - `Heartbeat` - your Heartbeat scripts. If wide, your per-frame logic is too heavy.
518
- - `Physics` - collision and simulation. Reduce unanchored parts.
519
- - `Render/Perform` - GPU-bound. Reduce draw calls, textures, particles.
520
- - `Replication` - network overhead. Reduce remote calls and replicated property changes.
521
- 5. Click a bar to see details: script name, line number, time in microseconds.
522
- 6. Use the `microprofiler` dump (`Ctrl+F6` > `Dump`) to save a `.html` file for offline analysis.
523
-
524
- ### F9 Developer Console
525
-
526
- - Press `F9` in-game or in Studio to open.
527
- - **Log** tab: errors, warnings, print output.
528
- - **Memory** tab: breakdown by category (Instances, PhysicsParts, Sounds, Scripts, Signals, etc.).
529
- - **Stats** tab: FPS, ping, data send/receive rates.
530
- - **Server Stats** (in-game): server heartbeat time, physics step time.
531
-
532
- ### Stats Service (Programmatic)
533
-
534
- ```lua
535
- local Stats = game:GetService("Stats")
536
-
537
- -- Total memory in MB
538
- local totalMemory = Stats:GetTotalMemoryUsageMb()
539
-
540
- -- Specific categories
541
- local instanceMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.Instances)
542
- local scriptMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.LuaHeap)
543
-
544
- print(string.format("Total: %.1f MB | Instances: %.1f MB | Lua: %.1f MB",
545
- totalMemory, instanceMemory, scriptMemory))
546
- ```
547
-
548
- ---
549
-
550
- ## 10. Best Practices
551
-
552
- ### Profile Before Optimizing
553
-
554
- Never guess where the bottleneck is. Use the MicroProfiler and memory stats to find the actual hot path before changing code.
555
-
556
- ### Optimize Hot Paths First
557
-
558
- Focus effort on code that runs every frame (Heartbeat, RenderStepped) or on every player action. Code that runs once at startup is rarely worth optimizing.
559
-
560
- ### Spatial Queries Over Brute Force
561
-
562
- ```lua
563
- -- BAD: loop over every part in workspace
564
- for _, part in workspace:GetDescendants() do
565
- if (part.Position - origin).Magnitude < 50 then
566
- -- ...
567
- end
568
- end
569
-
570
- -- GOOD: spatial query
571
- local params = OverlapParams.new()
572
- params.FilterType = Enum.RaycastFilterType.Include
573
- params.FilterDescendantsInstances = { workspace.Enemies }
574
-
575
- local parts = workspace:GetPartBoundsInBox(
576
- CFrame.new(origin),
577
- Vector3.new(100, 100, 100), -- 50-stud radius box
578
- params
579
- )
580
- ```
581
-
582
- ### Object Pooling
583
-
584
- Reuse instances instead of creating and destroying them repeatedly.
585
-
586
- ```lua
587
- -- Object Pool Pattern
588
- local ObjectPool = {}
589
- ObjectPool.__index = ObjectPool
590
-
591
- function ObjectPool.new(template: Instance, initialSize: number)
592
-
593
- local self = setmetatable({}, ObjectPool)
594
- self._template = template
595
- self._available = table.create(initialSize)
596
- self._active = {} :: { [Instance]: boolean }
597
-
598
- -- Pre-populate
599
- for i = 1, initialSize do
600
- local clone = template:Clone()
601
- clone.Parent = nil
602
- table.insert(self._available, clone)
603
- end
604
-
605
- return self
606
- end
607
-
608
- function ObjectPool:Get(): Instance
609
- local obj: Instance
610
- if #self._available > 0 then
611
- obj = table.remove(self._available)
612
- else
613
- obj = self._template:Clone()
614
- end
615
-
616
- self._active[obj] = true
617
- return obj
618
- end
619
-
620
- function ObjectPool:Return(obj: Instance)
621
- if not self._active[obj] then
622
- return
623
- end
624
-
625
- self._active[obj] = nil
626
- obj.Parent = nil
627
-
628
- -- Reset state as needed (position, visibility, etc.)
629
- if obj:IsA("BasePart") then
630
- obj.CFrame = CFrame.new(0, -1000, 0) -- move off-screen
631
- obj.Anchored = true
632
- obj.Velocity = Vector3.zero
633
- end
634
-
635
- table.insert(self._available, obj)
636
- end
637
-
638
- function ObjectPool:ReturnAll()
639
- for obj in self._active do
640
- self:Return(obj)
641
- end
642
- end
643
-
644
- function ObjectPool:Destroy()
645
- for obj in self._active do
646
- obj:Destroy()
647
- end
648
- for _, obj in self._available do
649
- obj:Destroy()
650
- end
651
- table.clear(self._active)
652
- table.clear(self._available)
653
- end
654
-
655
- return ObjectPool
656
- ```
657
-
658
- Usage:
659
-
660
- ```lua
661
- local ObjectPool = require(path.to.ObjectPool)
662
- local bulletTemplate = ReplicatedStorage.Assets.Bullet
663
- local bulletPool = ObjectPool.new(bulletTemplate, 50)
664
-
665
- -- Spawn a bullet
666
- local bullet = bulletPool:Get()
667
- bullet.CFrame = firePoint
668
- bullet.Parent = workspace
669
-
670
- -- Return when done
671
- bulletPool:Return(bullet)
672
- ```
673
-
674
- ### Lazy-Load Distant Content
675
-
676
- Do not load all assets at game start. Use StreamingEnabled or manually load content as the player approaches.
677
-
678
- ```lua
679
- local function onPlayerMoved(position: Vector3)
680
- for _, zone in zones do
681
- local distance = (zone.center - position).Magnitude
682
- if distance < zone.loadRadius and not zone.loaded then
683
- zone:Load()
684
- elseif distance > zone.unloadRadius and zone.loaded then
685
- zone:Unload()
686
- end
687
- end
688
- end
689
- ```
690
-
691
- ---
692
-
693
- ## 11. Anti-Patterns
694
-
695
- ### Premature Optimization
696
-
697
- Optimizing code that is not a bottleneck wastes development time and often makes code harder to read. Always profile first.
698
-
699
- ### Creating/Destroying Instances in Heartbeat
700
-
701
- ```lua
702
- -- ANTI-PATTERN: creating parts every frame
703
- RunService.Heartbeat:Connect(function()
704
- local part = Instance.new("Part") -- allocation every frame
705
- part.Parent = workspace
706
- task.delay(1, function()
707
- part:Destroy()
708
- end)
709
- end)
710
-
711
- -- FIX: use an object pool (see section 10)
712
- ```
713
-
714
- ### Large Uncompressed Data Over Remotes
715
-
716
- ```lua
717
- -- ANTI-PATTERN: sending entire inventory every update
718
- remote:FireClient(player, fullInventoryTable) -- could be thousands of entries
719
-
720
- -- FIX: send only changes
721
- remote:FireClient(player, { added = { itemId }, removed = { oldItemId } })
722
- ```
723
-
724
- ### Not Testing on Mobile
725
-
726
- A game that runs at 60 fps on a gaming PC may run at 10 fps on a phone. Always test on actual mobile hardware, not just the Studio emulator.
727
-
728
- ### Ignoring Memory Leaks
729
-
730
- Common leak sources:
731
-
732
- - Event connections that are never disconnected.
733
- - Instances removed from the hierarchy but still referenced in a table.
734
- - Closures capturing large upvalues that outlive their usefulness.
735
- - Module-level tables that grow indefinitely without cleanup.
736
-
737
- Detect leaks by monitoring `Stats:GetTotalMemoryUsageMb()` over time. If memory grows continuously during gameplay without stabilizing, there is a leak.
738
-
739
- ```lua
740
- -- Simple memory monitor
741
- local lastMemory = 0
742
- task.spawn(function()
743
- while true do
744
- local current = game:GetService("Stats"):GetTotalMemoryUsageMb()
745
- local delta = current - lastMemory
746
- if delta > 10 then
747
- warn(string.format("Memory spike: +%.1f MB (total: %.1f MB)", delta, current))
748
- end
749
- lastMemory = current
750
- task.wait(10)
751
- end
752
- end)
753
- ```
1
+ ---
2
+ name: roblox-runtime
3
+ description: >
4
+ StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
5
+ last_reviewed: 2026-05-22
6
+ ---
7
+
8
+ <!-- Source: brockmartin/roblox-game-skill (MIT) -->
9
+
10
+ # Roblox Runtime & Performance
11
+
12
+ ## 1. Overview
13
+
14
+ Load this reference when:
15
+
16
+ - A game runs slowly or hitches during play (frame drops, lag spikes).
17
+ - Optimizing for mobile devices or low-end hardware.
18
+ - Conducting a performance audit before release or after adding major features.
19
+ - Players report high memory usage, disconnects, or long load times.
20
+ - Scaling a game to support more concurrent players.
21
+
22
+ Performance optimization is not a one-time task. It should be revisited after every significant content addition and tested across the full range of target devices.
23
+
24
+ ---
25
+
26
+ ## Quick Reference
27
+
28
+ **Load Full Reference below only when you need specific optimization techniques or benchmarks.**
29
+
30
+ Key rules:
31
+ - Target: 60 FPS (16.6ms/frame). Server heartbeat budget: 30 FPS (33ms).
32
+ - Part count: <10k visible for mobile, <50k for desktop. Use StreamingEnabled.
33
+ - One Heartbeat connection that dispatches, not N separate connections.
34
+ - Disconnect ALL event connections when done. Use Trove. Leaks = silent frame drops.
35
+ - Instance.Destroying event for cleanup when instances are removed.
36
+ - Debris:AddItem() for timed cleanup (projectiles, effects).
37
+ - Network: minimize RemoteEvent payload size. Batch related calls. Use UnreliableRemoteEvent for non-critical updates (positions, cosmetics).
38
+ - Mobile: halve particle counts, reduce draw distance, simplify meshes.
39
+ - Memory: avoid reference cycles (A→B→A). Weak tables for caches.
40
+ - String concat in loops: use table.concat, not repeated `..`
41
+
42
+ ---
43
+
44
+ ## Full Reference
45
+
46
+ ## 2. Performance Targets
47
+
48
+ | Metric | Desktop | Mobile |
49
+ |---|---|---|
50
+ | Frame Rate | 60 fps | 30 fps minimum |
51
+ | Memory Budget | ~1 GB | ~500 MB |
52
+ | Network | Minimize remote frequency | Same, with smaller payloads |
53
+ | Load Time | Under 10 seconds | Under 15 seconds |
54
+
55
+ **Key principles:**
56
+
57
+ - Always measure against the *lowest-spec target device*, not your development machine.
58
+ - Frame budget at 60 fps is ~16.6 ms per frame. At 30 fps it is ~33.3 ms.
59
+ - Network: keep RemoteEvent calls under 50 per second per client. Prefer batching.
60
+
61
+ ---
62
+
63
+ ## 3. Part Count Optimization
64
+
65
+ ### Limits
66
+
67
+ - **Per model:** aim for a maximum of ~500 parts.
68
+ - **Total scene:** keep the visible scene under 10,000 parts.
69
+ - Fewer parts means less physics simulation, less rendering overhead, and faster replication.
70
+
71
+ ### MeshParts Over Unions
72
+
73
+ - `UnionOperation` recalculates collision geometry at runtime and is more expensive.
74
+ - Export unions as MeshParts in Studio (right-click > Export Selection) and re-import.
75
+ - MeshParts use a fixed collision fidelity that is cheaper to compute.
76
+
77
+ ### StreamingEnabled
78
+
79
+ StreamingEnabled is **on by default** for new places. Only `BaseParts` and their descendants stream in/out. Other instances (Folders, ValueObjects, RemoteEvents, ModuleScripts) load during initial client load and never stream.
80
+
81
+ When instances stream out, they are **parented to nil** - not destroyed. Luau state persists if they stream back in. Removal signals fire, but local-only property changes may be lost.
82
+
83
+ #### Configuration
84
+
85
+ - `StreamingTargetRadius` - radius (studs) engine keeps loaded. Start at 256, tune.
86
+ - `StreamingMinRadius` - guaranteed radius. Set ~64 for nearby content.
87
+ - `StreamingPauseMode` - what happens during load (Default, Disabled, ClientPhysicsPause).
88
+ - `ModelStreamingMode` - per-model: `Atomic` (all descendants load together), `Persistent` (never streams out), `PersistentPerPlayer`, `Nonatomic`.
89
+
90
+ #### Critical Rules for AI-Generated Code
91
+
92
+ 1. **Always use `WaitForChild()` on client** for any Workspace instance. Never use `workspace.MyPart` dot access in LocalScripts - the instance may not be loaded yet.
93
+ 2. **Always include a timeout**: `WaitForChild("Name", 30)`. Without timeout, thread hangs forever if instance never streams in.
94
+ 3. **Never use `math.huge` as timeout.** The instance may never stream in.
95
+ 4. **Server has everything immediately.** WaitForChild is only needed on the client for Workspace instances.
96
+ 5. **ReplicatedStorage/ReplicatedFirst never stream.** Always available on client.
97
+ 6. **Handle nil returns from FindFirstChild** - instance may be streamed out.
98
+ 7. **When instance streams out, Parent becomes nil.** Clean up connections on Parent change.
99
+ 8. **BasePart descendants stream independently.** Only non-BasePart children are guaranteed to stream with parent.
100
+ 9. **Use `ModelStreamingMode = Atomic`** when all parts must appear together.
101
+ 10. **Use `Player:RequestStreamAroundAsync(location)`** to pre-fetch areas before teleporting.
102
+
103
+ #### CollectionService Pattern (Recommended by Roblox)
104
+
105
+ The official recommended pattern for streaming-aware code:
106
+
107
+ ```luau
108
+ local CollectionService = game:GetService("CollectionService")
109
+ local tag = "Interactive"
110
+
111
+ local active: {[Instance]: any} = {}
112
+
113
+ CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
114
+ active[instance] = true
115
+ -- Set up connections, UI, effects for this instance
116
+ end)
117
+
118
+ CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
119
+ active[instance] = nil
120
+ -- Cleanup: disconnect events, remove UI, stop effects
121
+ end)
122
+
123
+ -- Handle instances already present when script starts
124
+ for _, instance in CollectionService:GetTagged(tag) do
125
+ active[instance] = true
126
+ end
127
+ ```
128
+
129
+ This handles stream-in and stream-out automatically. No WaitForChild needed.
130
+
131
+ #### Streamable Module (Sleitnick/RbxUtil)
132
+
133
+ For client-side observation of streaming instances with automatic cleanup:
134
+
135
+ ```luau
136
+ local Streamable = require(ReplicatedStorage.Modules.Streamable)
137
+
138
+ local model = workspace:WaitForChild("MyModel")
139
+
140
+ local partStreamable = Streamable.new(model, "SomePart")
141
+ partStreamable:Observe(function(part, trove)
142
+ -- Called when part streams in
143
+ -- trove handles cleanup automatically when part streams out
144
+ trove:Add(function()
145
+ -- Cleanup code
146
+ end)
147
+ end)
148
+
149
+ -- Check existence directly
150
+ if partStreamable.Instance then
151
+ -- part is currently loaded
152
+ end
153
+
154
+ partStreamable:Destroy() -- clean up when done
155
+ ```
156
+
157
+ #### Proactive Streaming
158
+
159
+ ```luau
160
+ -- Server: pre-fetch area before teleporting player
161
+ Player:RequestStreamAroundAsync(targetLocation)
162
+
163
+ -- Keep an area permanently loaded for a player
164
+ Player:AddReplicationFocus(importantPart)
165
+
166
+ -- Remove when no longer needed
167
+ Player:RemoveReplicationFocus(importantPart)
168
+ ```
169
+
170
+ Source: Roblox Instance Streaming docs, Sleitnick/RbxUtil Streamable (MIT)
171
+
172
+ ### Anchoring
173
+
174
+ - **Anchor every static part.** Unanchored parts enter the physics solver even if they are not moving, consuming CPU every frame.
175
+ - Use `BasePart.Anchored = true` for terrain decorations, buildings, props, and anything that should not move.
176
+
177
+ ---
178
+
179
+ ## 4. Script Optimization
180
+
181
+ ### Consolidated Heartbeat
182
+
183
+ Never scatter `RunService.Heartbeat:Connect(...)` across dozens of scripts. Consolidate into a single manager.
184
+
185
+ ```lua
186
+ -- HeartbeatManager (single Script in ServerScriptService or a ModuleScript)
187
+ local RunService = game:GetService("RunService")
188
+
189
+ local HeartbeatManager = {}
190
+ HeartbeatManager._callbacks = {} :: { [string]: (dt: number) -> () }
191
+
192
+ function HeartbeatManager:Register(id: string, callback: (dt: number) -> ())
193
+ self._callbacks[id] = callback
194
+ end
195
+
196
+ function HeartbeatManager:Unregister(id: string)
197
+ self._callbacks[id] = nil
198
+ end
199
+
200
+ RunService.Heartbeat:Connect(function(dt: number)
201
+ for _, callback in self._callbacks do
202
+ callback(dt)
203
+ end
204
+ end)
205
+
206
+ return HeartbeatManager
207
+ ```
208
+
209
+ Usage from other modules:
210
+
211
+ ```lua
212
+ local HeartbeatManager = require(path.to.HeartbeatManager)
213
+
214
+ HeartbeatManager:Register("EnemyAI", function(dt: number)
215
+ -- update all enemies
216
+ end)
217
+
218
+ -- When no longer needed:
219
+ HeartbeatManager:Unregister("EnemyAI")
220
+ ```
221
+
222
+ ### Table Pre-allocation
223
+
224
+ ```lua
225
+ -- Pre-allocate a table with 100 slots
226
+ local results = table.create(100)
227
+ for i = 1, 100 do
228
+ results[i] = computeValue(i)
229
+ end
230
+ ```
231
+
232
+ ### String Concatenation
233
+
234
+ ```lua
235
+ -- BAD: creates a new string object every iteration
236
+ local result = ""
237
+ for i = 1, 1000 do
238
+ result = result .. tostring(i) .. ","
239
+ end
240
+
241
+ -- GOOD: build a table, join once
242
+ local parts = table.create(1000)
243
+ for i = 1, 1000 do
244
+ parts[i] = tostring(i)
245
+ end
246
+ local result = table.concat(parts, ",")
247
+ ```
248
+
249
+ ---
250
+
251
+ ## 5. Memory Management
252
+
253
+ ### Disconnect Events
254
+
255
+ Every `:Connect()` call returns a `RBXScriptConnection`. Store it and disconnect when done.
256
+
257
+ ```lua
258
+ -- Event Cleanup Pattern
259
+ local Cleaner = {}
260
+ Cleaner.__index = Cleaner
261
+
262
+ function Cleaner.new()
263
+ local self = setmetatable({}, Cleaner)
264
+ self._connections = {} :: { RBXScriptConnection }
265
+ self._instances = {} :: { Instance }
266
+ return self
267
+ end
268
+
269
+ function Cleaner:Add(connection: RBXScriptConnection)
270
+ table.insert(self._connections, connection)
271
+ return connection
272
+ end
273
+
274
+ function Cleaner:AddInstance(instance: Instance)
275
+ table.insert(self._instances, instance)
276
+ return instance
277
+ end
278
+
279
+ function Cleaner:Clean()
280
+ for _, conn in self._connections do
281
+ if conn.Connected then
282
+ conn:Disconnect()
283
+ end
284
+ end
285
+ table.clear(self._connections)
286
+
287
+ for _, inst in self._instances do
288
+ inst:Destroy()
289
+ end
290
+ table.clear(self._instances)
291
+ end
292
+
293
+ return Cleaner
294
+ ```
295
+
296
+ Usage:
297
+
298
+ ```lua
299
+ local Cleaner = require(path.to.Cleaner)
300
+ local cleaner = Cleaner.new()
301
+
302
+ cleaner:Add(workspace.ChildAdded:Connect(function(child)
303
+ print(child.Name, "added")
304
+ end))
305
+
306
+ cleaner:Add(Players.PlayerRemoving:Connect(function(player)
307
+ print(player.Name, "left")
308
+ end))
309
+
310
+ -- When this system shuts down or the player leaves:
311
+ cleaner:Clean()
312
+ ```
313
+
314
+ ### Destroy Instances Properly
315
+
316
+ - Always call `:Destroy()` rather than setting `Parent = nil`. `:Destroy()` locks the instance, disconnects all events on it, and marks it for garbage collection.
317
+ - Setting `Parent = nil` keeps the instance alive if anything still references it.
318
+
319
+ ### Avoid Reference Cycles
320
+
321
+ ```lua
322
+ -- BAD: mutual references prevent garbage collection
323
+ local a = {}
324
+ local b = {}
325
+ a.ref = b
326
+ b.ref = a
327
+ -- Neither a nor b can be collected until both references are broken
328
+ ```
329
+
330
+ Break references explicitly when done: `a.ref = nil; b.ref = nil`.
331
+
332
+ ### Instance.Destroying
333
+
334
+ Use `Instance.Destroying` to run cleanup when an instance is about to be destroyed:
335
+
336
+ ```lua
337
+ local part = Instance.new("Part")
338
+ part.Destroying:Connect(function()
339
+ -- clean up related data, disconnect connections, etc.
340
+ end)
341
+ ```
342
+
343
+ ### Debris Service
344
+
345
+ For timed cleanup of temporary instances (projectiles, effects):
346
+
347
+ ```lua
348
+ local Debris = game:GetService("Debris")
349
+ local bullet = Instance.new("Part")
350
+ bullet.Parent = workspace
351
+ Debris:AddItem(bullet, 5) -- destroyed after 5 seconds
352
+ ```
353
+
354
+ ---
355
+
356
+ ## 6. Network Optimization
357
+
358
+ ### Minimize RemoteEvent Data Size
359
+
360
+ - Send only what changed, not full state.
361
+ - Use numeric IDs instead of long string keys when possible.
362
+ - Avoid sending Instance references when a name or ID suffices.
363
+
364
+ ### Batch Related Remotes
365
+
366
+ ```lua
367
+ -- BAD: three separate remote calls
368
+ remoteHealth:FireClient(player, health)
369
+ remoteAmmo:FireClient(player, ammo)
370
+ remoteStamina:FireClient(player, stamina)
371
+
372
+ -- GOOD: one call with a table
373
+ remotePlayerState:FireClient(player, {
374
+ health = health,
375
+ ammo = ammo,
376
+ stamina = stamina,
377
+ })
378
+ ```
379
+
380
+ ### UnreliableRemoteEvent
381
+
382
+ For high-frequency, non-critical data such as position or rotation updates, use `UnreliableRemoteEvent`. Dropped packets are acceptable because the next update will correct the state.
383
+
384
+ ```lua
385
+ -- In ReplicatedStorage, create an UnreliableRemoteEvent named "PositionSync"
386
+ local posSync = ReplicatedStorage:WaitForChild("PositionSync")
387
+
388
+ -- Server: fire frequently without guaranteeing delivery
389
+ RunService.Heartbeat:Connect(function()
390
+ for _, player in Players:GetPlayers() do
391
+ posSync:FireClient(player, npcPositions)
392
+ end
393
+ end)
394
+ ```
395
+
396
+ ### Compress Large Data
397
+
398
+ - Strip unnecessary keys before sending.
399
+ - Use short key names (`hp` instead of `hitPoints`).
400
+ - Consider delta compression: send only values that changed since the last update.
401
+
402
+ ### Reduce Replication
403
+
404
+ - Set visual-only properties on the client (particle colors, UI tweens).
405
+ - Properties changed on the server replicate to all clients automatically, which consumes bandwidth.
406
+
407
+ ---
408
+
409
+ ## 7. Rendering Optimization
410
+
411
+ ### Level of Detail (LOD)
412
+
413
+ Create multiple versions of a model at different detail levels and swap based on distance:
414
+
415
+ ```lua
416
+ local function setLOD(model: Model, playerPosition: Vector3)
417
+ local distance = (model:GetPivot().Position - playerPosition).Magnitude
418
+ if distance < 100 then
419
+ -- show high-detail version
420
+ elseif distance < 300 then
421
+ -- show medium-detail version
422
+ else
423
+ -- show low-detail version or hide
424
+ end
425
+ end
426
+ ```
427
+
428
+ Roblox also has built-in `MeshPart.RenderFidelity` (Automatic, Performance, Precise) which controls mesh LOD.
429
+
430
+ ### Draw Distance Limits
431
+
432
+ - Use `BasePart.CastShadow = false` on distant or small parts.
433
+ - Disable unnecessary `SurfaceLight`, `PointLight`, `SpotLight` on distant objects.
434
+ - With StreamingEnabled, the engine handles draw distance automatically.
435
+
436
+ ### Particle Count Budgets
437
+
438
+ | Property | Recommended Max |
439
+ |---|---|
440
+ | Particles per emitter (`Rate`) | ~200 |
441
+ | Total active emitters in view | ~20 |
442
+ | Beam segments (`Segments`) | 10-20 |
443
+ | Trail `MaxLength` | Keep short for mobile |
444
+
445
+ - Set `ParticleEmitter.Enabled = false` when off-screen or far away.
446
+ - Use fewer, larger particles instead of many small ones.
447
+
448
+ ### Texture Resolution
449
+
450
+ | Use Case | Max Resolution |
451
+ |---|---|
452
+ | General props, walls, floors | 512x512 |
453
+ | Hero assets (player characters, key items) | 1024x1024 |
454
+ | UI icons, decals | 256x256 to 512x512 |
455
+ | Sky/environment | 1024x1024 |
456
+
457
+ - Use `Decal` over `Texture` when the surface only needs one face covered. Decals are simpler to render.
458
+ - Compress textures before uploading. Avoid PNG when JPEG quality is acceptable.
459
+
460
+ ---
461
+
462
+ ## 8. Mobile-Specific Optimization
463
+
464
+ ### Part Counts
465
+
466
+ - Target 30-50% fewer parts than desktop. If the desktop budget is 10K parts, aim for 5-7K on mobile.
467
+ - Use `UserInputService:GetPlatform()` or screen size to detect mobile and reduce detail.
468
+
469
+ ### Simplified Particle Effects
470
+
471
+ - Halve the `Rate` of particle emitters on mobile.
472
+ - Reduce `Lifetime` to keep fewer active particles.
473
+ - Disable non-essential emitters entirely.
474
+
475
+ ### Touch-Optimized UI
476
+
477
+ - Minimum touch target size: **44x44 points** (following Apple HIG).
478
+ - Add padding between interactive elements.
479
+ - Use `GuiObject.Active = true` to ensure touch events register.
480
+ - Avoid hover-dependent UI (mobile has no hover state).
481
+
482
+ ### Reduced Draw Distance
483
+
484
+ ```lua
485
+ if UserInputService.TouchEnabled then
486
+ workspace.StreamingTargetRadius = 128 -- lower than desktop
487
+ workspace.StreamingMinRadius = 48
488
+ end
489
+ ```
490
+
491
+ ### Memory-Efficient Assets
492
+
493
+ - Use lower-resolution textures on mobile (256x256 where desktop uses 512x512).
494
+ - Reduce mesh polygon counts for mobile LOD models.
495
+ - Monitor memory with `Stats():GetTotalMemoryUsageMb()` and warn/act if approaching 500 MB.
496
+
497
+ ### Test on Low-End Devices
498
+
499
+ - Test on devices with 2-3 GB RAM (older iPads, budget Android phones).
500
+ - Use the Roblox mobile emulator in Studio, but always verify on real hardware.
501
+ - Check for thermal throttling during extended play sessions.
502
+
503
+ ---
504
+
505
+ ## 9. Profiling Tools
506
+
507
+ ### MicroProfiler (Ctrl+F6 in Studio)
508
+
509
+ The MicroProfiler displays a real-time flame graph of what the engine is doing each frame.
510
+
511
+ **How to read it:**
512
+
513
+ 1. Press `Ctrl+F6` to open. Press `Ctrl+P` to pause and inspect a frame.
514
+ 2. Each horizontal bar is a task. Width represents time spent.
515
+ 3. Look for bars that are unusually wide - these are your hot frames.
516
+ 4. Common labels to watch:
517
+ - `Heartbeat` - your Heartbeat scripts. If wide, your per-frame logic is too heavy.
518
+ - `Physics` - collision and simulation. Reduce unanchored parts.
519
+ - `Render/Perform` - GPU-bound. Reduce draw calls, textures, particles.
520
+ - `Replication` - network overhead. Reduce remote calls and replicated property changes.
521
+ 5. Click a bar to see details: script name, line number, time in microseconds.
522
+ 6. Use the `microprofiler` dump (`Ctrl+F6` > `Dump`) to save a `.html` file for offline analysis.
523
+
524
+ ### F9 Developer Console
525
+
526
+ - Press `F9` in-game or in Studio to open.
527
+ - **Log** tab: errors, warnings, print output.
528
+ - **Memory** tab: breakdown by category (Instances, PhysicsParts, Sounds, Scripts, Signals, etc.).
529
+ - **Stats** tab: FPS, ping, data send/receive rates.
530
+ - **Server Stats** (in-game): server heartbeat time, physics step time.
531
+
532
+ ### Stats Service (Programmatic)
533
+
534
+ ```lua
535
+ local Stats = game:GetService("Stats")
536
+
537
+ -- Total memory in MB
538
+ local totalMemory = Stats:GetTotalMemoryUsageMb()
539
+
540
+ -- Specific categories
541
+ local instanceMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.Instances)
542
+ local scriptMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.LuaHeap)
543
+
544
+ print(string.format("Total: %.1f MB | Instances: %.1f MB | Lua: %.1f MB",
545
+ totalMemory, instanceMemory, scriptMemory))
546
+ ```
547
+
548
+ ---
549
+
550
+ ## 10. Best Practices
551
+
552
+ ### Profile Before Optimizing
553
+
554
+ Never guess where the bottleneck is. Use the MicroProfiler and memory stats to find the actual hot path before changing code.
555
+
556
+ ### Optimize Hot Paths First
557
+
558
+ Focus effort on code that runs every frame (Heartbeat, RenderStepped) or on every player action. Code that runs once at startup is rarely worth optimizing.
559
+
560
+ ### Spatial Queries Over Brute Force
561
+
562
+ ```lua
563
+ -- BAD: loop over every part in workspace
564
+ for _, part in workspace:GetDescendants() do
565
+ if (part.Position - origin).Magnitude < 50 then
566
+ -- ...
567
+ end
568
+ end
569
+
570
+ -- GOOD: spatial query
571
+ local params = OverlapParams.new()
572
+ params.FilterType = Enum.RaycastFilterType.Include
573
+ params.FilterDescendantsInstances = { workspace.Enemies }
574
+
575
+ local parts = workspace:GetPartBoundsInBox(
576
+ CFrame.new(origin),
577
+ Vector3.new(100, 100, 100), -- 50-stud radius box
578
+ params
579
+ )
580
+ ```
581
+
582
+ ### Object Pooling
583
+
584
+ Reuse instances instead of creating and destroying them repeatedly.
585
+
586
+ ```lua
587
+ -- Object Pool Pattern
588
+ local ObjectPool = {}
589
+ ObjectPool.__index = ObjectPool
590
+
591
+ function ObjectPool.new(template: Instance, initialSize: number)
592
+
593
+ local self = setmetatable({}, ObjectPool)
594
+ self._template = template
595
+ self._available = table.create(initialSize)
596
+ self._active = {} :: { [Instance]: boolean }
597
+
598
+ -- Pre-populate
599
+ for i = 1, initialSize do
600
+ local clone = template:Clone()
601
+ clone.Parent = nil
602
+ table.insert(self._available, clone)
603
+ end
604
+
605
+ return self
606
+ end
607
+
608
+ function ObjectPool:Get(): Instance
609
+ local obj: Instance
610
+ if #self._available > 0 then
611
+ obj = table.remove(self._available)
612
+ else
613
+ obj = self._template:Clone()
614
+ end
615
+
616
+ self._active[obj] = true
617
+ return obj
618
+ end
619
+
620
+ function ObjectPool:Return(obj: Instance)
621
+ if not self._active[obj] then
622
+ return
623
+ end
624
+
625
+ self._active[obj] = nil
626
+ obj.Parent = nil
627
+
628
+ -- Reset state as needed (position, visibility, etc.)
629
+ if obj:IsA("BasePart") then
630
+ obj.CFrame = CFrame.new(0, -1000, 0) -- move off-screen
631
+ obj.Anchored = true
632
+ obj.Velocity = Vector3.zero
633
+ end
634
+
635
+ table.insert(self._available, obj)
636
+ end
637
+
638
+ function ObjectPool:ReturnAll()
639
+ for obj in self._active do
640
+ self:Return(obj)
641
+ end
642
+ end
643
+
644
+ function ObjectPool:Destroy()
645
+ for obj in self._active do
646
+ obj:Destroy()
647
+ end
648
+ for _, obj in self._available do
649
+ obj:Destroy()
650
+ end
651
+ table.clear(self._active)
652
+ table.clear(self._available)
653
+ end
654
+
655
+ return ObjectPool
656
+ ```
657
+
658
+ Usage:
659
+
660
+ ```lua
661
+ local ObjectPool = require(path.to.ObjectPool)
662
+ local bulletTemplate = ReplicatedStorage.Assets.Bullet
663
+ local bulletPool = ObjectPool.new(bulletTemplate, 50)
664
+
665
+ -- Spawn a bullet
666
+ local bullet = bulletPool:Get()
667
+ bullet.CFrame = firePoint
668
+ bullet.Parent = workspace
669
+
670
+ -- Return when done
671
+ bulletPool:Return(bullet)
672
+ ```
673
+
674
+ ### Lazy-Load Distant Content
675
+
676
+ Do not load all assets at game start. Use StreamingEnabled or manually load content as the player approaches.
677
+
678
+ ```lua
679
+ local function onPlayerMoved(position: Vector3)
680
+ for _, zone in zones do
681
+ local distance = (zone.center - position).Magnitude
682
+ if distance < zone.loadRadius and not zone.loaded then
683
+ zone:Load()
684
+ elseif distance > zone.unloadRadius and zone.loaded then
685
+ zone:Unload()
686
+ end
687
+ end
688
+ end
689
+ ```
690
+
691
+ ---
692
+
693
+ ## 11. Anti-Patterns
694
+
695
+ ### Premature Optimization
696
+
697
+ Optimizing code that is not a bottleneck wastes development time and often makes code harder to read. Always profile first.
698
+
699
+ ### Creating/Destroying Instances in Heartbeat
700
+
701
+ ```lua
702
+ -- ANTI-PATTERN: creating parts every frame
703
+ RunService.Heartbeat:Connect(function()
704
+ local part = Instance.new("Part") -- allocation every frame
705
+ part.Parent = workspace
706
+ task.delay(1, function()
707
+ part:Destroy()
708
+ end)
709
+ end)
710
+
711
+ -- FIX: use an object pool (see section 10)
712
+ ```
713
+
714
+ ### Large Uncompressed Data Over Remotes
715
+
716
+ ```lua
717
+ -- ANTI-PATTERN: sending entire inventory every update
718
+ remote:FireClient(player, fullInventoryTable) -- could be thousands of entries
719
+
720
+ -- FIX: send only changes
721
+ remote:FireClient(player, { added = { itemId }, removed = { oldItemId } })
722
+ ```
723
+
724
+ ### Not Testing on Mobile
725
+
726
+ A game that runs at 60 fps on a gaming PC may run at 10 fps on a phone. Always test on actual mobile hardware, not just the Studio emulator.
727
+
728
+ ### Ignoring Memory Leaks
729
+
730
+ Common leak sources:
731
+
732
+ - Event connections that are never disconnected.
733
+ - Instances removed from the hierarchy but still referenced in a table.
734
+ - Closures capturing large upvalues that outlive their usefulness.
735
+ - Module-level tables that grow indefinitely without cleanup.
736
+
737
+ Detect leaks by monitoring `Stats:GetTotalMemoryUsageMb()` over time. If memory grows continuously during gameplay without stabilizing, there is a leak.
738
+
739
+ ```lua
740
+ -- Simple memory monitor
741
+ local lastMemory = 0
742
+ task.spawn(function()
743
+ while true do
744
+ local current = game:GetService("Stats"):GetTotalMemoryUsageMb()
745
+ local delta = current - lastMemory
746
+ if delta > 10 then
747
+ warn(string.format("Memory spike: +%.1f MB (total: %.1f MB)", delta, current))
748
+ end
749
+ lastMemory = current
750
+ task.wait(10)
751
+ end
752
+ end)
753
+ ```