Performance
Most apps built on the framework run comfortably without performance work. This page covers the few cases that need attention and the levers the framework gives you when they come up.
Layout coalescing
Setters call an internal scheduleLayout() rather than running doLayout() synchronously. The queue flushes once per animation frame; multiple changes within the same frame coalesce into one layout pass. Components whose ancestor is also scheduled get pruned because the ancestor's pass will recurse into them.
In practice this means you can call setPreferredSize on hundreds of components in a tight loop and pay for one layout pass, not hundreds.
pauseLayout / resumeLayout
For bulk mutations that span multiple frames or need explicit grouping:
panel.pauseLayout();
for (const item of largeArray) {
panel.addComponent(buildItem(item));
}
panel.resumeLayout(); // single doLayout pass at the endpauseLayout() blocks the rAF queue from running on this component (and its subtree). resumeLayout() runs a synchronous doLayout() and re-enables scheduling.
Use this when:
- You're mutating dozens or hundreds of components in one logical operation.
- You want to guarantee a single layout pass without relying on rAF coalescing.
Virtual scrolling
Table and Tree both render only the rows visible in the viewport plus a small buffer. The mechanics:
- A fixed pool of table
Row(or tree row) components. - Scrolling is JS-owned via a
VirtualScroller: the rows live inside a container whosetranslate3dtransform exposes the requested viewport, and two customScrollbaroverlays drivescrollX/scrollY. Wheel, touch (with 2D fling momentum), and keyboard navigation all funnel through the samesetScrollY/setScrollXentry points. - Pool slots are rebound to new data via
setData()only when their data index changes — DOM nodes stay in place and only their bound data shifts.
This gives constant memory and constant frame time regardless of dataset size. A 100,000-row table has the same performance characteristics as a 100-row table for the rows currently on screen.
See the Virtualized lists recipe for an end-to-end example.
Compositor-layer hints
Elements that animate via translate3d (table rows during scroll, the header during horizontal scroll, windows during drag) are promoted to their own compositor layer the first time the browser sees the transform actually change. That first frame pays a layer-creation cost the next frames don't — visible as a brief "settle" tick at the start of motion.
Component.setWillChange pre-creates the layer by writing will-change: transform (or any other CSS will-change value). The framework calls it automatically:
- Window drag — set on
mousedown, cleared onmouseup. The first dragged frame is layer-ready. - Virtual table / tree rows — set when a row joins the pool, cleared when it leaves. Pool size is bounded by the visible window plus buffer, so the hint count stays well under the browser threshold.
- Table header — set once for the Table's lifetime, since the header is always the scroll-mirror target.
Custom code that drives its own continuous motion can use the same setter:
panel.setWillChange("transform"); // before motion starts
// ... setTranslate calls ...
panel.setWillChange(null); // when motion ends, to release the layerThe hint costs GPU memory and is ignored by browsers past a per-page threshold (~50–100 elements), so set it only over the active-motion lifetime and clear it promptly. For permanent scroll targets (one or two per page) it can be left set for the component's lifetime.
Web Worker for sort and filter
AbstractStore automatically offloads sort and filter operations to a Web Worker once the dataset crosses 1,000 rows:
store.sort('value', 'desc'); // worker handles it for >1k rows
store.filterBy(r => r.get('value') > 500); // worker handles it for >1k rowsYou don't configure anything — the worker is created lazily on first use. Below the threshold the round-trip overhead exceeds the work, so operations run synchronously in-process.
Filter functions are serialised
Custom filter predicates passed to filterBy must be pure functions with no captured non-serialisable state. They are sent to the worker via structured clone. For richer filter logic, use FilterDescriptor — a serialisable AST that the framework's filter evaluator runs identically on both sides of the worker boundary.
Disposing Text components
Text and its subclasses (Label, Header, Legend) subscribe to ThemeManager.onThemeChange on construction so they re-measure on every theme change.
Custom components that create Text instances dynamically and remove them must call text.dispose() to detach the listener:
class StatusBar extends Component {
private message: Text = Text('');
constructor() {
super('div');
this.addComponent(this.message);
}
protected destructor() {
this.message.dispose(); // detach theme listener
super.destructor();
}
}Built-in components attached and removed through the normal addComponent / removeComponent flow have this handled for you. The leak only appears when you create a Text outside that flow.
Avoiding layout thrash
A few patterns can defeat the rAF coalescing and force multiple layout passes per frame:
- Reading
getSize()between sets. EverygetSizecall forces a flush so the read is up-to-date. If you write, read, write again, you've caused two layout passes. Batch your sets, then read once at the end. - Mutating during a layout callback. Adding or removing components from inside
doLayout(or a layout-triggered listener) re-enters the layout pass. The framework handles this safely, but the immediate call you triggered won't see the new children — they land on the next frame. - Missing
pauseLayoutfor large bulk operations. rAF coalescing helps, but for thousands of changes you'll also pay queue-management overhead.pauseLayoutskips that.
Deferring expensive panel construction
When a Tab layout has more than a handful of panels, building all of them up-front delays first paint for content the user may never visit. Tab.addLazyTab registers a tab button immediately and runs the panel factory only on first activation:
layout.addLazyTab(() => new HeavyPanel(), 'Heavy');Subsequent activations reuse the cached instance, so scroll position and form state are preserved. See Tab » Lazy panel construction for details.
The same yield-and-fade lifecycle is available for floating windows whose content is expensive to build via Window.setContentFactory: the window opens immediately with a spinner in its content area, the factory runs after a two-rAF yield, and the built tree fades in over the spinner.
const win = Window('Heavy');
win.setSize({ width: 800, height: 600 });
win.setContentFactory(() => new HeavyContent());
win.show();setContentFactory accepts an optional second argument that fires after the built component has been attached, laid out, and faded in — use it for work that must happen against a rendered subtree, such as kicking off an async data load whose loading spinner is rendered by the content itself:
win.setContentFactory(
() => TablePanel(store),
() => void store.load()
);
win.show();Running store.load() before the callback would emit loadingchanged: true before TablePanel had subscribed (and before the table had a size for its overlay spinner to mount against). The onReady callback is the supported hook for any "after content is on screen" side effect.
Both code paths share Animation.materialize, which composes the spinner mount, two-rAF yield, factory invocation, and cross-fade in one call.
CSS rule generation cost
Each component creates one CSS rule for itself the first time it renders. Construction itself is JS-only — no stylesheet inserts, no forced layout, no document.body probes — so building a detached subtree before attaching it to the live document is cheap and predictable. The rule materialises during the first render() pass (typically driven by Component.addComponent or Window.show). Button (:active), ToggleButton (.selected), and similar pseudo-state classes add a second rule each on top of that. For a typical app with hundreds of components this is fine. For lists rendering thousands of items, prefer the virtual-scrolling components which reuse a fixed pool of rules.
If you find yourself building a custom virtual list, look at how the table Body is implemented — it's the canonical reference. The scroll plumbing is reusable on its own via VirtualScroller.
See also
- Virtualized lists recipe
- Component lifecycle —
pauseLayout/resumeLayoutAPI - Layout system — how
doLayoutactually runs - API: AbstractStore — store-level worker offload