StringTune/Docs

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:

  • frameDOM
  • styleTxn

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(...) or onMouseMoveMeasure(...)
  • 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

  • MutationObserver
  • ResizeObserver
  • 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.