Recomposition

Per-node recomposition counts — the agent-facing performance signal for unnecessary recomposition. Source for heat maps and “this node recomposed N times for one click” reviews.

At a glance

   
Kind compose/recomposition
Schema version 2
Modules :data-recomposition-core (published) · :data-recomposition-connector
Render mode instrumented
Cost medium
Token usage ~1 000–1 500 tok per query (compose/recomposition ~3–5 KB). See token usage.
Transport inline
Platforms Android · Desktop · shared

What it answers

  • How many times did each composable recompose during this render?
  • Which subtree is the hot spot — a leaf re-running, or a parent re-running and dragging its children with it?
  • After a click / scroll / value change, what is the delta in recomposition counts compared to the steady-state snapshot?

Two modes:

  • Snapshot — a single instrumented render reports cumulative counts.
  • Click delta — drive the input, then diff before / after counts to attribute work to the interaction.

What it does NOT answer

  • It does not measure wall-clock time spent in composition — pair with render/trace.
  • It does not detect the cause (unstable lambda, missing remember, key churn) — it points at the location; reading the source is on the reviewer.
  • It runs the renderer in instrumented mode, so absolute counts are not directly comparable to a non-instrumented production build.

Use cases

  • Catch a Modifier factory that captures a new lambda each composition and forces the whole row to recompose.
  • Verify a derivedStateOf / remember change actually trimmed the recomposition count it was supposed to.
  • Build a heat-map overlay for a “performance review” PR.

Payload shape

RecompositionPayload, RecompositionNode in :data-recomposition-core.

// compose/recomposition (schemaVersion 2)
{
  "mode": "delta",                  // or "snapshot"
  "sinceFrameStreamId": "f-7a1c",   // populated in delta mode
  "inputSeq": 2,                    // monotonic per flushed delta
  "nodes": [
    // reason: PARAMETER_CHANGE | STATE_READ | BOTH | UNKNOWN
    { "nodeId": "1a2b3c4d", "count": 1, "reason": "STATE_READ" },
    { "nodeId": "5e6f7a8b", "count": 1, "reason": "PARAMETER_CHANGE" }
  ]
}

Each node’s reason is attributed from the Compose runtime’s onScopeInvalidated(scope, value) signal compared against the recompose count: a scope invalidated by a snapshot write it subscribed to reads STATE_READ, one that re-ran only because its caller did reads PARAMETER_CHANGE, and a mix reads BOTH. This is the “why did this recompose?” signal — a parent reading state and forwarding it as parameters shows up as one STATE_READ dragging a fan of PARAMETER_CHANGE children.

Nodes also carry nullable bounds ({x, y, width, height}) and source-marker fields (sourceFile / sourceLine / sourceColumn / functionName) for the heat-map and source-overlay surfaces. These are present on the v2 wire but not yet populated — the post-layout bounds join and slot-table reflection that fill them are deferred increments (see issue #1605).

Enabling

Producer requires instrumented render mode and runs when its extension is publicly enabled. From an MCP client:

{ "method": "tools/call", "params": { "name": "subscribe_preview_data",
  "arguments": { "uri": "compose-preview://<id>/_module/com.example.Foo",
                 "kind": "compose/recomposition" } } }

For PR-review style “did this PR add unnecessary recomposition?” guidance, see compose-preview-review/references/agent-audits.md.

Companion products

  • Layout inspectorlayout/inspector to map nodeId to source.
  • Render tracerender/trace, render/composeAiTrace for the wall-clock side of the same render.

Apache 2.0 licensed. Source on GitHub.

This site uses Just the Docs, a documentation theme for Jekyll.