Container Timing emits no entry for a display:contents container

#568304686 Filed 2026-10-02 Blink>PerformanceAPIs P3 / S3 Regression in r1684899

Run it

Requires the Container Timing API

The API is experimental, so start the browser with:

chrome --enable-blink-features=ContainerTiming https://static.januschka.com/i-568304686/

Without the flag the table below reports the API as unavailable.

CheckExpectedActualVerdict

text inside the display:contents container

text inside the display:block container

What happens

Both containers carry containertiming and hold one paragraph of text. The only difference is the container's own display value.

<div containertiming="contents-container" style="display: contents">
  <p>text inside the display:contents container</p>
</div>

<div containertiming="block-container" style="display: block">
  <p>text inside the display:block container</p>
</div>
Expected

Two container entries: contents-container and block-container.

Actual (tip of tree, before the fix)

One entry: block-container. The display: contents container is never reported.

Measured on this page

Chromium 146 (legacy attribution): both entries reported.

Tip of tree, unpatched: contents-container missing.

Tip of tree, patched: both entries reported.

Root cause

Attribution moved from a DOM node flag to the pre-paint tree walk in r1684899 (CL 8098545, which removed the legacy implementation). ContainerTimingPaintAttributionTracker::UpdateOnPrePaint() is called once per LayoutObject, and a display: contents element generates none. Its containertiming and containertimingignore attributes were therefore never read, so painted descendants resolved to the next laid-out root - or to nothing.

The legacy implementation walked parentElement() at paint time, which steps through boxless elements, which is why the entry was emitted before the switch.

Fix

While visiting a layout object, the tracker now also applies the container timing attributes of the boxless DOM ancestors between that object's node and its nearest laid-out ancestor, outermost first, so nested roots still chain. Adding or removing the attribute on a display: contents element dirties the nearest laid-out ancestor instead of its own (absent) layout object.

third_party/blink/renderer/core/paint/timing/
    container_timing_paint_attribution_tracker.cc
  UpdateUnlaidOutAncestorRoots()  // new
  UpdateContainerRoot()           // extracted, shared with the walk
third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
  PrePaintTreeWalk::UpdateContainerTimingContext()
third_party/blink/renderer/core/html/html_element.cc
  HTMLElement::MarkContainerTimingChangedForPrePaint()

Web platform tests: external/wpt/container-timing/tentative/containertiming-display-contents-with-child-img.html and containertiming-with-display-contents-child-ignore-and-child-img.html.

Links

↗ Issue 568304686 ↗ Reporter's repro