# Find out why a component re-rendered

> Which Lit components re-rendered, how often, how long for, and which reactive properties changed to cause it.

Answer “which component re-renders too much” and “why did this one update” from one recording. Only the Lit Lifecycle layer needs to be on.

## Record one

1. Open the **Timeline** tab and press **▶ Record**.
2. Interact with the page, or just wait if something updates on its own.
3. Switch to **Updates**.

Recording is shared state, so it does not matter which tab you start it from. Stopping keeps the recording. Starting again clears it, because the page re-zeroes its timeline clock each time.

## Read the component table

The upper pane holds one row per component **tag**, slowest first.

![The Updates tab table: one row per component with update counts and total time.](/_astro/devtools-updates-table.light.DFf-lUXN_Z2jJJlK.webp)![The Updates tab table: one row per component with update counts and total time.](/_astro/devtools-updates-table.dark.BpicBXif_Z1Qr53p.webp)

| Column          | Meaning                                                                                                                                                                                                                                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Component · *n* | The tag, with `×n` when more than one instance of it updated. The header counts the components listed.                                                                                                                                                                                                                                          |
| *(unlabelled)*  | The reactive properties that changed, most frequent first, then a badge for each thing to look at: `n skipped` for ticks `shouldUpdate` vetoed, `n re-rendered` for updates the component scheduled itself, ≡ and a count for equal-value reassignments, ⚠ and a count for updates that threw. A badge only appears when its count is not zero. |
| Updates         | How many update ticks were recorded, as `n×`.                                                                                                                                                                                                                                                                                                   |
| Total           | Summed time in `performUpdate` across those ticks, with a bar scaled to the largest total in the table.                                                                                                                                                                                                                                         |
| Slowest         | The slowest single update.                                                                                                                                                                                                                                                                                                                      |

Rows group by tag rather than by instance on purpose. A list of 200 rows that each updated twice is one component’s problem, not two hundred.

Time comes from `performUpdate` alone. `willUpdate`, `update` and `updated` all run inside it, so adding the phases together would count the same milliseconds several times over. A tick whose end was not recorded, because it is still running or older than the retained window, counts toward **Updates** but adds nothing to **Total**.

## Read one component’s updates

Pick a component and the lower pane lists each of its update ticks: **when** it started and **how long** it took, **what changed**, **what caused it**, and the instance id, which links through to the [Components tab](/guides/devtools/components/). Above the list, the source link opens the component’s file in your editor. While a single instance has updated, a **Rendered at** link beside it opens the template tag that created that instance; with several instances there is no one place to name, so it is left out.

![hmr-counter selected in the Updates tab: its source and Rendered at links above a list of its updates, each with its time, duration and changed keys.](/_astro/devtools-updates-detail.light.DeVb5FhL_2cX6iG.webp)![hmr-counter selected in the Updates tab: its source and Rendered at links above a list of its updates, each with its time, duration and changed keys.](/_astro/devtools-updates-detail.dark.BEekkwsR_ZwhNbA.webp)

The changed-property keys are the actual answer to “why did this re-render”. They come from Lit’s own `changedProperties` map, so they are what the element reacted to, not an inference.

A row marked *skipped* means the component’s `shouldUpdate` returned false: the properties listed changed, but nothing re-rendered. Skipped ticks are counted in the row’s `skipped` badge and not under **Updates** or **Total**, so those columns keep meaning renders. `lit_update-summary` returns `skipped` per component and `skipped: true` per cycle.

A row marked *re-rendered* was scheduled by the component’s own previous update, with nothing else asking for it: the component rendered twice for one change. Setting a reactive property in `updated()` (or `firstUpdated()`) does this, and Lit’s dev build warns about it as `change-in-update`. Compute the value in `willUpdate()` instead, which runs before the render and joins the update already in progress. The component table counts these under `n re-rendered`, the Components tab adds the count to the element’s Warnings, and `lit_update-summary` returns `selfCaused` per component and `selfCaused: true` per cycle.

A row marked *threw in update* means that phase threw. The error’s name is shown, and its message is in the tooltip. The component table counts such updates under ⚠, and `lit_update-summary` returns `errors` per component and `error` per cycle, so an agent sees them too.

Failures that happen later are tied to the update that started them. A row marked *rejected in updated* means an `async updated()` (or `willUpdate`, `firstUpdated`) returned a promise that rejected with nothing handling it. A row marked *task userTask failed* means that `@lit/task` ended in its error state. An error thrown from a timer or an event listener the component set up has no such link, so it shows only in the browser console.

## See the old and new values

Turn on the **Changed values** layer in the Timeline tab before you record. Each update then lists `prev → next` for every property that changed, under its row. The previews use the same limits as the [Components tab](/guides/devtools/components/): two levels deep, eight items, 120 characters.

A badge reads *new reference, same value* when the property was assigned a different object, array or date with equal contents. That is the usual cause of a pointless re-render: an inline `[]` or `{}` in a template, or a list that is re-mapped on every change. *Same reference* means the property was not reassigned, for example a `requestUpdate('items')` after mutating in place. The component table marks a row with ≡ and a count when any of its updates reassigned an equal value, and the tooltip names the properties.

The layer is off by default because it serializes values on every update. At most 16 properties are recorded per update. Only the `update` phase is read, so a component that overrides `update()` without calling `super` reports nothing. Equality is checked on plain objects, arrays and dates, to six levels and 200 values. Anything else, such as a `Map` or a class instance, counts as different, and so does a value that exceeds the budget.

The same data is in `lit_update-summary`: `changedDetail` on each cycle and `redundantChanges` on each component.

In the playground fixture below, clicking the provider’s button bumps the marked `counter`. Both the provider and its consumer show a tick, and both name `counter` among the changed properties.

```ts
import {LitElement, css, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
import {consume, provide} from '@lit/context';
import {counterContext} from './context-def.js';

/**
 * Context API surface: a provider whose `@provide` value is also `@state`
 * (bumping it pushes to subscribed consumers), and a consumer nested in
 * its shadow DOM. The consumer sits in its own template literal so
 * provider-header edits never rebuild the consumer element; the
 * ContextProvider/ContextConsumer controllers live on the (untouched)
 * instances and must keep working after either class is hot-patched.
 */
@customElement('hmr-ctx-provider')
export class HmrCtxProvider extends LitElement {
  static override styles = css`
    .badge {
      font-size: 0.8em;
      color: var(--muted, #666);
    }
  `;

  @provide({context: counterContext})
  @state()
  private counter = 0;

  private renders = 0;

  private renderHeader() {
    return html`<h2>Context: HELLO</h2>`;
  }

  override render() {
    return html`
      ${this.renderHeader()}
      <button id="provide-increment" @click=${this.increment}>
        Provided: ${this.counter}
      </button>
      <hmr-ctx-consumer></hmr-ctx-consumer>
      <span class="badge" id="badge">renders: 0</span>
    `;
  }

  override updated() {
    this.renders++;
    this.setAttribute('data-renders', String(this.renders));
    const badge = this.renderRoot.querySelector('#badge');
    if (badge !== null) {
      badge.textContent = `renders: ${this.renders}`;
    }
  }

  private increment() {
    this.counter++;
  }
}

@customElement('hmr-ctx-consumer')
export class HmrCtxConsumer extends LitElement {
  static override styles = css`
    #consumed {
      font-weight: 600;
    }
  `;

  @consume({context: counterContext, subscribe: true})
  @state()
  private counter?: number;

  private renders = 0;

  override render() {
    return html`<p id="consumed">Consumed: ${this.counter}</p>`;
  }

  override updated() {
    this.renders++;
    this.setAttribute('data-renders', String(this.renders));
  }
}
```

An update with no cause

The cause is recorded when a handler of a mouse, keyboard or custom event, another component’s update, or a `@lit/task` settling calls `requestUpdate`, and when you press **Request update**, edit a property or override one in the Components tab. It reads “after click (412, 88)”, “after my-parent update”, “after userTask task”, “after Request update in DevTools”, “after edit in DevTools” or “after override in DevTools”. Only when the causing event is no longer in the buffer does attribution fall back to a Mouse or Keyboard event within the 200ms before the update. An update with no cause is a timer, a signal, or a stray `requestUpdate()`. That is itself the useful finding, and better than blaming the last click that happened to be nearby.

A component that overrides `willUpdate` or `updated` without calling `super` shadows the base implementation and does not report that phase. The tick still appears, because `performUpdate` is never overridden, but its changed keys can be missing.

## Link to a view

The tab is addressable like the others. The element id selects the component row that instance belongs to, so the same id works whether you came from the Components tree or from an exported snapshot.

```text
#tab=updates&component=7
```

## Ask an agent instead

The same derivation is available over MCP as `lit_update-summary`, which a [coding agent](/guides/devtools/agents/) should prefer over `lit_recent-events` for anything shaped like “why did this re-render”.

## Related

[Limitations](/reference/limitations/)What the plugin cannot patch, when a reload explains the reset counts.

[Inspect live components](/guides/devtools/components/)The tab an instance id links through to.
