Skip to content

feat: per-element deadline scheduling in dateObserver - #370

Draft
mattcosta7 with Copilot wants to merge 2 commits into
mainfrom
copilot/optimize-date-observer
Draft

feat: per-element deadline scheduling in dateObserver#370
mattcosta7 with Copilot wants to merge 2 commits into
mainfrom
copilot/optimize-date-observer

Conversation

Copilot AI commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

A single second-precision <relative-time> element drives the entire page to a 1 s tick. Every tick calls update() on every observed element — so 300 elements showing "3 months ago" each run a full elapsedTime() + #resolveFormat() + Intl.format() pass every second, producing identical output each time.

Approach

Keep the single shared timer but give each element its own next-due timestamp via a WeakMap<RelativeTimeElement, number>. Each tick skips elements whose deadline hasn't passed; after updating an element its deadline is reset to now + getUnitFactor(el). The next timer interval is the minimum remaining time across all observed elements.

Key details

  • WeakMap not Map — deadlines are only looked up by key (we iterate this.elements), so no iteration over the map itself is needed and WeakMap gives better GC semantics.
  • observe() during a ticktimeEl.update() calls observe(this) at the end of every update. The existing if (this.updating) return guard prevents clobbering the deadline the tick loop is about to set immediately after timeEl.update() returns. Attribute changes arriving outside a tick (the normal attributeChangedCallback → microtask → update() path) always re-enter observe() with updating = false, so they correctly recompute the deadline.
  • Per-element error isolation — wraps each timeEl.update() call in try/catch, rethrowing async, so one throwing element cannot abort the tick or skip the reschedule.
for (const timeEl of this.elements) {
  let due = this.deadlines.get(timeEl) ?? 0
  if (due <= now) {
    try { timeEl.update() } catch (error) { setTimeout(() => { throw error }) }
    due = now + getUnitFactor(timeEl)
    this.deadlines.set(timeEl, due)
  }
  nearest = Math.min(nearest, due - now)
}

Benchmark

1 second-precision element + 300 month-old elements, 10 s simulated time:

update() calls
Before 3,311
After 10

Tests added

  • Slow elements are not re-formatted on fast-cadence ticks
  • Slow elements still update when their own deadline passes
  • First render is synchronous on connectedCallback
  • datetime change resets the deadline to the fast cadence
  • Error in one element does not prevent others from updating

Add a WeakMap<RelativeTimeElement, number> tracking each element's
next-update deadline so slow-changing elements are skipped on fast
ticks driven by a single second-precision element.

- dateObserver.deadlines: WeakMap seeds per element in observe(),
  cleared in unobserve(), checked per tick in update()
- update() only calls timeEl.update() when its deadline has passed,
  then recomputes the deadline as now + getUnitFactor(el)
- Per-element try/catch in update() so one throwing element cannot
  abort the tick or skip the rescheduler
- nearest is computed across ALL elements (not just updated ones) so
  the timer fires at the earliest remaining deadline
- 5 new tests covering: fast/slow cadence isolation, slow element
  eventual update, synchronous first render, datetime-change deadline
  reset, and error isolation

Benchmark: 1 second-precision element + 300 month-old elements over
10s: 3311 update() calls before → 10 after (300× reduction).
Copilot AI changed the title [WIP] Optimize dateObserver for individual update deadlines feat: per-element deadline scheduling in dateObserver Jul 30, 2026
Copilot AI requested a review from mattcosta7 July 30, 2026 16:36
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