Performance
tm.ui is designed to stay cheap when windows are large or busy. What it does about each cost:
- No exports on any path. The page is served under your resource name (
ui_page 'https://cfx-nui-tm_lib/ui-windows/dist/index.html'), soSendNUIMessage,RegisterNUICallbackandSetNuiFocusare plain native calls. There is no serialize, call and deserialize round trip per message. - One message per open. A window goes out as a single JSON message, so the cost does not scale with component count.
- Nothing per keystroke. Typing, validation, tabs,
when,disabledWhen, menu navigation and paging all run in the page. Lua hears only named events. - Batched patches.
:setand:setNodecalls in the same tick are merged into one message.
Rules of thumb
- Keep state in the page. Don't use
notify = trueunless Lua really needs each change. Where it does, changes are debounced (150 ms), so a slider drag sends one event. - Batch updates. Several
win:set(...)calls in one tick cost one message. Preferwin:set({ a = 1, b = 2 })orwin:update(schema)over patching fields one by one across ticks. - Use
when/disabledWheninstead of Lua for show/hide and locking. They are evaluated in the page. - Validate with
rulesin the page, then again on the server. - Keep schemas modest: under about 300 nodes / 100 KB. The dev preview warns above that. Bigger, busier UIs (inventory-style grids, drag and drop, 3D previews) should stay custom builds.
- Long menus are paged. Menus render one page at a time (
pageSize, default 8).
What a call costs
| Call | Messages |
|---|---|
tm.ui.window(schema) | 1 (queued until the page has loaded, then flushed) |
win:set / win:setNode (any number in one tick) | 1 |
win:update(schema) | 1 |
win:close() | 1 |
Typing in a field, tabs, when, menu navigation | 0 |
A button press or a notify field change | 1 NUI callback (native, replied to immediately) |
Frames
Each resource that points ui_page at the windows page gets its own page instance, so the small bundle (about 150 KB, 54 KB gzipped) is loaded once per consumer. That is cheap for a handful of resources. It is a reason to keep ui for lightweight UIs, and not to make every resource in the server a consumer of ten windows.
Measuring
- Dev preview: payload size and node count are logged in the console when a schema is large.
- In game:
resmonfor the consumer resource around open / update; wrap a call inGetGameTimer()to time it.