| icon | lucide/gauge |
|---|
Note
The docs GIF is captured at a fixed 18 FPS for deterministic encoding. The
runnable examples/performance demo reports the live Love2D FPS on your machine.
Glyph is intended for debugger panels and game UI, so performance matters.
- Keep layout explicit and cheap.
- Avoid CSS-like selectors, global queries, string parsing, or cascading ancestry scans.
- Keep static UI static.
- Mount only what needs to be visible for large lists.
- Prefer primitives that compose over specialized components.
Theme font specs are loaded lazily and cached by source, rounded resolved size,
hinting, and filter. Wrapped-text measurement (font:getWidth) is cached per
(font, text) so re-laying out the same labels each frame does not re-measure
them. Prefer named fonts and textStyle presets over creating Love2D fonts
inside component functions. Avoid continuously animating fontSize: every
distinct rounded size intentionally retains a separate Love Font.
SYSL-backed rich text is opt-in. Use plain ui.text for hot-path labels, and
reserve ui.richText for copy that actually needs rich formatting, images,
effects, or dialogue-style behavior.
Use ui.memo(component, deps) to reuse a subtree when dependencies are unchanged.
local rows = ui.memo(function()
return buildRows(data)
end, { dataVersion })Use ui.static(node) for stable labels, icons, or repeated rows that do not need rebuild/layout churn.
local label = ui.static(ui.text("Ready"))Static and memoized nodes reuse cached geometry while their incoming layout constraints are unchanged. A window resize or parent-size change automatically recomputes their geometry, so responsive percent sizes remain accurate.
Resolved styles also use a bounded per-runtime cache. When that cache is full, Glyph retains its existing warm entries and resolves additional paths without caching them, avoiding both unbounded memory growth and whole-cache rebuilds.
A render rebuilds the tree only when it is dirty — a useState setter ran,
input changed focus/hover, or an animation is in flight — then lays out and
draws. On an idle frame with an unchanged viewport, the existing tree and its
completed geometry are reused while custom and built-in drawing still run. (See
Architecture.)
The example runner keeps the root component function stable between draws, so an idle example exercises this clean-root path. Clean-root reuse skips component build, layout, and layout-callback traversal; it never skips drawing. A dirty tree or changed viewport rebuilds and republishes geometry before drawing.
Glyph does not yet do fine-grained diffing or automatic memoization: a dirty
render rebuilds and re-allocates the affected tree. For large or rapidly-changing
UIs the tools above — ui.memo, ui.static, stable keys, and a mounted
visible window — are how you keep that bounded. Finer-grained reactivity is a
possible future direction, not something you need to design around today.
Load Love2D image assets once in app setup, then pass the image object to
ui.image. Glyph caches the derived fit/alignment plan for each image node, but
it does not own filesystem loading or asset lifetime.
local icon = love.graphics.newImage("assets/icon.png")
local iconNode = ui.static(ui.image({ source = icon, width = 32, height = 32 }))For repeated inventory cells or portraits, combine reused image objects with
ui.memo or ui.static. Use custom draw only when a single image draw is not
enough for the effect.
Glyph caches translations that are safe to reuse. Plain keys are cached until
ui.i18n.invalidate() or ui.i18n.setLocale(locale).
ui.textKey("menu.play")Parameterized translations are translated fresh unless you provide a stable cache key:
ui.textKey("messages", {
textParams = { count = count },
textCacheKey = "messages:" .. tostring(count),
})For memoized translated subtrees, include ui.i18n.version() in the deps so
locale changes rebuild the cached nodes:
local panel = ui.memo(buildPanel, { ui.i18n.version(), dataVersion })For large log/table views:
- Keep the dataset outside the UI tree.
- Use
ui.virtualListfor fixed-height rows so only the visible window is mounted. - Reuse stable row components where possible.
- Give each row a stable
keyso its identity — focus, hover, a mid-edit input cursor, and cached style — follows the data when rows are inserted, removed, or reordered, instead of snapping to whichever sibling now sits at that index. - Use plain
scrollViewfor small lists or variable-height content. - Show coarse live counters such as FPS, render time, layout passes, and mounted row counts so performance examples explain their budget at a glance.
See examples/performance.
The performance example presents these readings in one flat instrument register
beside its 10,000-event ledger. It reports the previous completed frame as
IDLE / REUSE or DIRTY / BUILD. ROOT BUILDS counts component executions,
LAYOUT PASSES counts root layout events rather than visited nodes, so it reads
zero on a clean frame. LAST TOTAL measures the whole Glyph render call:
clean frames contain drawing, while dirty or resized frames also include build,
layout, and callback publication.
Scene layers keep isolated hook scopes and cached roots. Use layers for overlays and modals instead of rebuilding unrelated UI inside the main tree.
Custom draw runs every render. Avoid hot-path allocation when possible:
- Reuse tables for repeated geometry when practical.
- Avoid building huge arrays every frame.
- Keep shader/state changes localized.
- Mark decorative overlays
interactive = falseto keep hit testing clean.
