# Timers, Observers and Cleanup

> lifecycle.timeout, interval, frame, loop, observeVisibility, observeMutations, addCleanup, cleanup, stopped, and the section watchdog.

Canonical URL: https://77865786950d:3005/scripting/timers-and-observers

Timers and observers created through `lifecycle` are cleared when the section
stops. Repeating work pauses while the tab is hidden or the section is off
screen (more than 200 px outside the viewport).

## lifecycle.timeout

```ts
lifecycle.timeout(callback: () => void, delay = 0): number
```

Runs once after `delay` ms unless the section has stopped. Cleared on cleanup.
Returns the timer id.

## lifecycle.interval

```ts
lifecycle.interval(callback: () => void, delay = 0): number
```

Repeats every `delay` ms, with a minimum of 16 ms. Ticks are skipped while the
tab is hidden or the section is off screen. Cleared on cleanup.

## lifecycle.frame

```ts
lifecycle.frame(callback: FrameRequestCallback): number
```

One animation frame. Skipped while the tab is hidden or the section is off
screen. Cancelled on cleanup.

## lifecycle.loop

```ts
lifecycle.loop(callback: FrameRequestCallback, options?: { fps?: number }): () => void
```

A frame loop throttled to `fps` (clamped to 1–60, default 60). It pauses while
hidden or off screen and returns a function that stops it.

A section can run at most **2** loops at once; starting a third throws
`Section animation loop limit (2) exceeded`.

```js
const stop = lifecycle.loop((t) => {
  dial.style.rotate = `${(t / 20) % 360}deg`;
}, { fps: 30 });
```

## lifecycle.observeVisibility

```ts
lifecycle.observeVisibility(
  targets: Element | Iterable<Element>,
  callback: IntersectionObserverCallback,
  options?: IntersectionObserverInit,
): IntersectionObserver
```

An `IntersectionObserver` on elements inside the section. Targets outside
`root` or inside a nested island are skipped silently. Disconnected on
cleanup.

```js
lifecycle.observeVisibility(lifecycle.queryAll(".stat"), (entries) => {
  for (const e of entries) if (e.isIntersecting) e.target.classList.add("in");
}, { threshold: 0.4 });
```

## lifecycle.observeMutations

```ts
lifecycle.observeMutations(
  target: Node,
  callback: MutationCallback,
  options: MutationObserverInit = { childList: true, subtree: true },
): MutationObserver
```

A `MutationObserver` scoped to the section. Records from inside nested islands
are filtered out.

- A target outside `root` or inside an island throws
  `Section observers cannot target outside the section or inside an island`.
- More than 100 callbacks in one second disconnects the observer and stops the
  section with `observer-callback-storm`.

The injected `MutationObserver` argument behaves the same way. The compiler
rejects `new MutationObserver(` as `unmanaged_mutation_observer`; use this
method.

## lifecycle.addCleanup

```ts
lifecycle.addCleanup(cleanup: (() => void) | void): void
```

Registers a function to run when the section stops. If the section has
already stopped, it runs immediately. Returning a function from the script
does the same thing.

## lifecycle.cleanup

```ts
lifecycle.cleanup(reason?: string): void
```

Stops the section: runs every cleanup in reverse order, removes listeners,
timers and observers, and aborts pending `lifecycle.fetch` calls. With a
`reason`, it is written to `data-lx-animation-stopped` on `root`. Calling it
again does nothing.

The runtime also calls it when `root` leaves the document.

## lifecycle.stopped

`boolean`. `true` once the section has been cleaned up. Check it after an
`await` before touching the DOM.

```js
const data = await (await lifecycle.fetch(url)).json();
if (lifecycle.stopped) return;
render(data);
```

## Watchdog

The runtime stops a section, reveals its content and logs the reason to the
console when any of these happen:

| Reason | Trigger |
|---|---|
| `callback-budget` | Three callbacks over 50 ms each (a fast callback removes one strike) |
| `event-loop-stall` | The main thread is blocked for more than 2 seconds |
| `mutation-storm` | More than 1,000 mutations in one second inside the section |
| `dom-growth` | More than 1,000 elements added, or more than 200 added and over 25% growth |
| `observer-callback-storm` | A managed mutation observer fires more than 100 times in one second |
| `runtime-error` | The script throws while starting |

The callback budget covers listeners from `on`, timers, frames, loops,
observers, `onCart` and anything wrapped with
[`guard`](/scripting/dom-and-events#lifecycleguard). The stop reason is
visible on `root` as `data-lx-animation-stopped`.
