Skip to content

Per-tick locale memoization, error isolation in dateObserver, sync connectedCallback - #369

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

Per-tick locale memoization, error isolation in dateObserver, sync connectedCallback#369
mattcosta7 with Copilot wants to merge 2 commits into
copilot/optimize-relative-time-updatefrom
copilot/copilotoptimize-relative-time-update

Conversation

Copilot AI commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The prior PR batched connectedCallback into a microtask flush to reduce startup work, but the microtask still ran before paint — work was moved, not removed — while silently breaking the synchronous-render contract consumers rely on. This PR removes that batching entirely and ships two targeted improvements with no observable behavior change.

Changes

Reverted: batching infrastructure removed

  • Deleted pendingElements, pendingFlush, and flushPending()
  • connectedCallback() calls this.update() synchronously again (as on main)
  • disconnectedCallback() back to only dateObserver.unobserve(this)
  • Removed pendingElements.has(this) short-circuit from attributeChangedCallback

Per-tick locale/timeZone/hourCycle memoization

During a dateObserver.update() pass, each element's #lang/timeZone/hourCycle getters performed an independent closest() ancestor walk — O(N) walks for what is almost always the same value (inherited from <html>). This PR adds a cache that lives only for one synchronous pass:

// enabled before the element loop, cleared unconditionally in finally
localeCacheEnabled = true
try {
  for (const timeEl of this.elements) { timeEl.update() }
} finally {
  localeCacheEnabled = false
  localePassCache.clear()  // Map<Node, ResolvedLocale> keyed by parentNode
}

Cache key: parentNode. Siblings under the same parent share an identical ancestor chain for inherited attributes, so one walk per unique parent is sufficient. Elements with lang, time-zone, or hour-cycle set directly on themselves are excluded (their own attribute overrides the inherited value). intlLocale(lang).toString() is additionally memoized in a persistent localeNormCache since locale-string normalization is stable.

Error isolation in dateObserver.update()

One element throwing no longer aborts the rest of the pass. Errors are rethrown via setTimeout(() => { throw error }) to preserve global error visibility without breaking the batch.

attributeChangedCallback coalescing — no change needed

The existing #updating guard already coalesces multiple attribute changes within the same task into a single deferred render. No module-level queue reintroduced.

Tests

  • Synchronous render contract: shadowRoot.textContent and title are populated immediately after appendChild with no await
  • Tick with multiple elements under different lang ancestors renders each with correct, independent output (proves cache does not over-share across parents)
  • Ancestor time-zone change is reflected on the next update() call (cache does not persist across passes)
  • One element throwing during a tick does not prevent remaining elements from updating

Copilot AI changed the title [WIP] Remove deferred rendering in connectedCallback Per-tick locale memoization, error isolation in dateObserver, sync connectedCallback Jul 30, 2026
Copilot AI requested a review from mattcosta7 July 30, 2026 16:30
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