Skip to content

perf: skip redundant render-root and title writes on no-op ticks#363

Merged
mattcosta7 merged 3 commits into
mainfrom
perf/skip-noop-render-writes
Jul 15, 2026
Merged

perf: skip redundant render-root and title writes on no-op ticks#363
mattcosta7 merged 3 commits into
mainfrom
perf/skip-noop-render-writes

Conversation

@mattcosta7

@mattcosta7 mattcosta7 commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

The shared dateObserver singleton drives every registered <relative-time> on a single timer, calling update() on each element per tick. Today update():

  • Unconditionally rebuilds the [part="root"] span via replaceChildren(span) in #updateRenderRootContent, and
  • Re-sets the title attribute,

even when the computed text/title are identical to what is already rendered. This is common on periodic ticks (e.g. an item that still reads "3mo", or any element whose display hasn't crossed a unit boundary since the last tick). Each no-op write still invalidates style for the element, so pages with many <relative-time> elements pay avoidable style-invalidation churn every tick.

This is the "skip the DOM write when nothing changed" optimization — no public API change, no behavior change.

Changes

  • #updateRenderRootContent now reuses the existing [part="root"] span and mutates it in place rather than rebuilding it. The span is only recreated when it is missing or malformed (no part="root" span, or extra children). textContent and aria-hidden are each written only when their value actually changes, so unchanged ticks perform no DOM writes while aria-hidden toggles still take effect.
  • update() skips setAttribute('title', …) when the new title equals the current one.

Tests

Added tests to test/relative-time.js:

  • render-root node is reused when a subsequent update() produces the same text.
  • render-root node is reused and its text is updated in place when the displayed text changes (node identity is retained across text changes).
  • a no-op update() emits no shadow-root mutations (verified with a MutationObserver), including an aria-hidden="true" case.
  • an unchanged formatted title is not written again on a no-op update().

Full suite passes in Chromium.

Notes / scope

This is the low-risk, in-library slice of a broader perf discussion. It intentionally does not add offscreen pausing (an IntersectionObserver in dateObserver) — that's a larger, behavior-changing follow-up worth profiling first, since content-visibility: auto in consuming apps already skips layout/paint for offscreen ticks.

The shared dateObserver ticks every registered <relative-time> on a single timer, calling update() on each. update() unconditionally rebuilt the [part=root] span via replaceChildren and re-set the title attribute even when the computed text/title were identical to what was already rendered (common on periodic ticks, e.g. an item that still reads "3mo").

Guard #updateRenderRootContent to reuse the existing span when the content and aria-hidden state are unchanged, and skip setAttribute('title') when the title is unchanged. This avoids needless DOM churn and style invalidation on every no-op tick.
Reuse the existing part="root" span across ticks and update textContent/aria-hidden in place, skipping the write when unchanged. Avoids node allocation + replaceChildren on every change and keeps a stable part node. Also types #renderRoot as Node & ParentNode to drop the Element cast.
@mattcosta7 mattcosta7 self-assigned this Jul 15, 2026
@mattcosta7
mattcosta7 requested a review from jonrohan July 15, 2026 00:43
@mattcosta7
mattcosta7 marked this pull request as ready for review July 15, 2026 00:43
@mattcosta7
mattcosta7 requested a review from a team as a code owner July 15, 2026 00:43
Copilot AI review requested due to automatic review settings July 15, 2026 00:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes periodic <relative-time> updates to reduce redundant DOM writes.

Changes:

  • Reuses the render-root span and conditionally updates its content.
  • Skips unchanged title assignments.
  • Adds render-root reuse tests.
Show a summary per file
File Description
src/relative-time-element.ts Implements no-op write optimizations.
test/relative-time.js Tests render-root node reuse.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 4
  • Review effort level: Medium

Comment thread src/relative-time-element.ts
Comment thread test/relative-time.js
Comment on lines +121 to +125
// A subsequent update that produces the same text must not replace the node.
el.update()
await Promise.resolve()
const rootAfter = el.shadowRoot.querySelector('[part="root"]')
assert.equal(rootAfter, root, 'render root node should be reused when text is unchanged')
if (!this.#customTitle) {
newTitle = this.#getFormattedTitle(date) || ''
if (newTitle && !this.noTitle) this.setAttribute('title', newTitle)
if (newTitle && !this.noTitle && newTitle !== oldTitle) this.setAttribute('title', newTitle)
Comment thread test/relative-time.js
el.setAttribute('datetime', new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString())
await Promise.resolve()
const rootAfter = el.shadowRoot.querySelector('[part="root"]')
assert.equal(rootAfter, root, 'render root node should be reused across a text change')
- Skip same-value aria-hidden setAttribute/removeAttribute on the render span
- Add MutationObserver tests asserting no shadow-root writes on no-op ticks
  (including aria-hidden), and that an unchanged title is not rewritten
@mattcosta7
mattcosta7 requested a review from Copilot July 15, 2026 14:55
@mattcosta7
mattcosta7 merged commit 7936031 into main Jul 15, 2026
6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Low

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.

3 participants