Skip to content

Batch initial connectedCallback renders into a single microtask flush#365

Draft
mattcosta7 with Copilot wants to merge 2 commits into
mainfrom
copilot/optimize-relative-time-update
Draft

Batch initial connectedCallback renders into a single microtask flush#365
mattcosta7 with Copilot wants to merge 2 commits into
mainfrom
copilot/optimize-relative-time-update

Conversation

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

For N <relative-time> elements inserted together, connectedCallback was calling update() synchronously per element — N ancestor-walking locale/timeZone/hourCycle resolutions, N Intl format passes, N shadow-DOM writes — all blocking layout at startup.

Changes

src/relative-time-element.ts

  • New batch infrastructure: module-level pendingElements: Set<RelativeTimeElement> and async flushPending() that snapshots + clears the set before iterating (safe against mid-flush connect/disconnect side-effects), then calls update() on each element after a single await Promise.resolve().
  • connectedCallback: enqueues into the batch instead of calling update() synchronously. If #updating is already set (an attributeChangedCallback microtask is already in-flight for this element), skips enqueuing — that microtask handles the first render.
  • disconnectedCallback: adds pendingElements.delete(this) so elements removed before the flush are never updated.
  • attributeChangedCallback: short-circuits with return if pendingElements.has(this) — defers to the pending batch flush rather than scheduling a redundant second microtask.
// Before
connectedCallback(): void {
  this.update()  // synchronous — blocks for every element at insert time
}

// After
connectedCallback(): void {
  if (this.#updating) return  // attributeChangedCallback already scheduled a microtask
  pendingElements.add(this)
  if (!pendingFlush) {
    pendingFlush = true
    flushPending()  // one microtask for the whole batch
  }
}

The microtask resolves before paint, so rendered text, title, dispatched events, and the accessibility tree are identical to the previous synchronous behavior.

test/relative-time.js

Four new tests in a connectedCallback microtask batching suite: single-element deferred render, multi-element batch, disconnect-before-flush safety, and attribute-change-after-connect coalescing.

Move the initial update() call off the synchronous insertion critical path
by coalescing the first render of newly-connected <relative-time> elements
into a single microtask-flushed batch.

- Add module-level pendingElements set and pendingFlush flag
- Add async flushPending() that snapshots and clears the set before
  iterating, then calls update() on each element
- connectedCallback enqueues the element and triggers one flush per batch
  instead of calling update() synchronously; if #updating is already set
  (attributeChangedCallback fired before connection) skip enqueuing
- disconnectedCallback removes the element from pendingElements so
  detached elements are never updated
- attributeChangedCallback skips scheduling a separate microtask when the
  element is already in pendingElements (the batch flush handles it)
- Add 4 new tests for: single-element microtask render, multi-element
  batch, disconnect-before-flush safety, attribute-change coalescing
Copilot AI changed the title [WIP] Optimize update call for relative time elements Batch initial connectedCallback renders into a single microtask flush Jul 20, 2026
Copilot AI requested a review from mattcosta7 July 20, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants