Diagnose a slow interaction
Find what runs on each frame of a stutter, which script line made it, and what to write instead.
A scroll, a keystroke or a streamed reply that stutters: find what runs on each of its frames and which script line made it, then fix that line.
-
Run a release build with devtools:
cargo run --release -p zgx-run -- --inspect app/(a debug build’s times are several times a release build’s, so compare like with like). An app already running with devtools needs no restart. -
Do the interaction a few times, through devtools so it is the same each time:
zgx inspect scroll '#feed' 400(orclick,type,key), or by hand. -
Read what ran:
zgx inspect frames 5. Each frame gives its time, the states written, where the time went by phase, and its runs, the costliest first:frame 212 (19.80 ms): state.luau::rows, 3 unnamed → 41 runs, 9840 µs phases: update 10120 µs (52310K instr), layout 6400 µs (30120K instr), text 1900 µs, paint 820 µs, present 560 µs 6120 µs @58 binding (column components/row.luau:14) ← state.luau::rows 2210 µs app.luau:88 effect ← state.luau::rows … -
The costliest phase says where to look:
updateandscriptare the runs below it;layout,text,paintandencodeare the interface’s size and drawing (see the table). Map each costly run to its source line (components/row.luau:14,app.luau:88), then look at what that line reads and does.zgx inspect node @58shows the node, andzgx inspect lintlists what the linter already knows is costly. -
Change it, save (the app reloads), and repeat steps 2–3 until the frame fits: 8.3 ms at 120 Hz, 16.7 ms at 60.
What the runs tell you:
| You see | It usually is | Do instead |
|---|---|---|
many binding runs of a list’s rows on every write | rows reading the whole list (list:get()[i]) in their functions | ui.list { data = state, key, row = function(item, i, row) … }: a row reads its own item |
a binding run costing milliseconds, on a node holding thousands of children | ui.each over a long list: every row built and laid out | ui.list, which builds the rows in view |
the same effect line in every frame, with no write you made | an effect writing state that it (or another) reads, or ui.every faster than it needs | write only when the value changes; a slower timer; ui.tween/ui.spring for motion |
a binding run for a whole long text or list after a small change | items:update(function(l) … end) or set of a rebuilt table | push, set_key, append, patch: they write the path that changed |
few or cheap runs, yet layout, text or paint is most of the frame | the cost is the interface’s size or drawing, not the script | a box laid out from its content (no w/h), a long text re-measured, a blur or shadow redrawn; zgx inspect tree --depth 2 shows which boxes are large or deep enough to cost |
| a slow frame with no writes and no runs | a scroll or an animation laying out or drawing a lot | the same as above: the phases say which |