Custom Modules
Performance Patterns
Author custom modules in the same read/write pipeline as the runtime so scroll and cursor work stay stable under load.
Performance Patterns
Custom modules should follow the same performance model as the built-ins:
- reads in measure phases
- writes in mutate phases
- no unnecessary rebuilds
- no repeated string work in hot loops
Read and write separation
StringTune exposes two important authoring primitives:
frameDOMstyleTxn
The runtime already uses them in the main update loop:
- scroll and pointer reads are queued into measure phases
- style writes are committed inside a mutate phase
For module code, the simplest rule is:
- do DOM reads in
onScrollMeasure(...)oronMouseMoveMeasure(...) - do DOM writes in
onMutate(...)
Prefer onMutate(...) for output
If a module writes CSS variables or inline styles on every frame, do it in onMutate(...).
Good pattern:
override onMutate(): void {
for (const object of this.objects) {
const value = object.getProperty<number>('next-value');
this.applyVarToElement(object, '--my-value', value);
}
}
Less stable pattern:
override onFrame(): void {
object.htmlElement.style.setProperty('--my-value', String(value));
}
The second version mixes computation and DOM writes into the hot loop.
Prefer cached object state over repeated parsing
Do not re-read attributes or re-parse easing functions on every frame.
Parse once in initializeObject(...), then store the result on the object:
object.setProperty('easing', parsedEasing);
After that, read it from object state.
Use cssProperties only when it helps
If your module owns a CSS custom property and wants typed registration through CSS.registerProperty(...), define it on cssProperties:
this.cssProperties = [
{ name: '--my-progress', syntax: '<number>', initialValue: '0', inherits: true },
];
ModuleManager registers those properties automatically when the module is registered.
Do this only for variables your module actually owns.
Rebuild permissions
Each module has rebuild permissions on permissions.desktop and permissions.mobile.
Built-ins use this to skip unnecessary object recalculation on mobile or on certain resize types.
Example:
this.permissions.mobile.rebuild.height = false;
this.permissions.mobile.rebuild.width = false;
this.permissions.mobile.rebuild.scrollHeight = false;
Use this only when you are certain the module does not depend on those changes.
Object-local cleanup
Leaks in custom modules usually come from:
MutationObserverResizeObserver- global
events.on(...) - object-local
object.events.on(...)
Always pair them with cleanup in:
onObjectDisconnected(...)onUnsubscribe(...)destroy()
DOMBatcher
DOMBatcher is exported, but it is an advanced primitive.
Most custom modules do not need their own batcher because the runtime already batches:
- object initialization
- measure work
- mutate work
- style transactions
Reach for DOMBatcher only if you are building a module that creates or initializes many nodes in one burst and you have measured that the default path is not enough.
Practical rules
- keep
onFrame(...)mostly pure - treat
onMutate(...)as your write lane - cache event names and parsed values
- reduce rebuild permissions only when you fully understand the dependency
- clean up every observer and subscription you create
That is how custom modules stay compatible with the runtime instead of fighting it.