Theme Framework How the LeaseLeads theme frontend works

Scheduling

The Performance.js primitives that keep boot off the critical path — defer DOM writes, chunk work, and hand off to idle time.

On this page 4

Boot touches a lot of the page at once: dozens of components make themselves, each one reading and writing the DOM. Do that in one synchronous pass and you hand the browser a single long task that blocks paint and input. Performance.js is the theme's answer — three primitives that spread work across frames so no phase of boot ever monopolises the main thread.

Everything here is built on standard scheduling APIs (scheduler.yield(), requestIdleCallback, the load event), polyfilled at the top of the file so you can rely on them everywhere:

js
import 'scheduler-polyfill'
import 'requestidlecallback-polyfill'

manipulateDOM — defer a write until the document is ready

Use it whenever you mutate layout during boot. It runs your callback once the document has finished loading (immediately if it already has) and resolves with the result, so a write that would otherwise race the parser or thrash layout lands at a safe point. You hand it a callback and await the result:

js
const width = await manipulateDOM(() => element.getBoundingClientRect().width)

FadesIn and AutoInitializes both wrap their DOM-touching work in it, so a component that fades in or self-inits never writes before the page is ready:

js
// utilities/Traits/FadesIn.js
manipulateDOM(() => {
  // toggle the fade state class
})

batchProcess — chunk a fan-out and yield between batches

This is the workhorse. Hand it an array and a callback; it processes the array in batches of ten and calls scheduler.yield() before each batch, handing control back to the browser so a large collection never becomes one long task. It resolves once every callback result — promises included — has settled:

js
const instances = await batchProcess(elements, el => this.make(el))

It drives every lifecycle fan-out in the theme. Instantiable.boot chunks element construction through it, and the asset Processor chunks the loaders it drains through it — so instantiating a hundred elements or firing a hundred loaders yields ten times instead of blocking once:

js
// utilities/Instantiable/Instantiable.js
await batchProcess(Array.from(elements), element => this.make(element))

// utilities/AssetLoader/Processor.js
return await batchProcess([...this.#assetLoaders], async assetLoader => { /* … */ })
Note

The callback is invoked synchronously within a batch and its return value collected; the yield happens between batches, not between items. Ten items run back-to-back, then the browser gets a turn. That's the granularity — pick work where a batch of ten is a reasonable slice.

backgroundTask — run when the browser is idle

For work that isn't on any critical path — a measurement, a non-urgent state sync — hand it to idle time. backgroundTask wraps requestIdleCallback with a 2-second timeout so it still runs on a busy page, and resolves with the result:

js
const metrics = await backgroundTask(() => measureLayout())

GlobalResizeObserver schedules every consumer callback through it, so resize reactions never pile onto the frame that triggered them, and ScreenHeight uses it to defer its measurement pass off the critical path.

Tip

Reach for manipulateDOM when when matters (after load), backgroundTask when urgency is low (idle), and batchProcess when volume is the risk (a fan-out you don't want to run in one task).

  • The lifecycle — the boot/init phases whose fan-outs run through batchProcess.
  • Global observers — Resize callbacks are scheduled with backgroundTask.
  • Instantiable — where element construction is chunked.