Skip to content

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'), so SendNUIMessage, RegisterNUICallback and SetNuiFocus are 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. :set and :setNode calls in the same tick are merged into one message.

Rules of thumb ​

  1. Keep state in the page. Don't use notify = true unless Lua really needs each change. Where it does, changes are debounced (150 ms), so a slider drag sends one event.
  2. Batch updates. Several win:set(...) calls in one tick cost one message. Prefer win:set({ a = 1, b = 2 }) or win:update(schema) over patching fields one by one across ticks.
  3. Use when / disabledWhen instead of Lua for show/hide and locking. They are evaluated in the page.
  4. Validate with rules in the page, then again on the server.
  5. 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.
  6. Long menus are paged. Menus render one page at a time (pageSize, default 8).

What a call costs ​

CallMessages
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 navigation0
A button press or a notify field change1 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: resmon for the consumer resource around open / update; wrap a call in GetGameTimer() to time it.