# Container Timing: measuring when components actually appear

Core Web Vitals are great at telling you how a page performs. But they do not always tell you when a specific part of that page became available to users.

That is where the `containertiming` attribute comes in. Although it is experimental at the time of writing, I can see this becoming a serious candidate for stable browser support.

The best part: it is as easy as adding an attribute to your server-side or client-side rendered HTML. The next step is digesting the entries and determining what to collect.

And that's the perfect role for a Real User Monitoring (RUM) tool. So yes, we jumped on this performance bandwagon, as the API unlocks a new layer of real-user visibility.

## What is Container Timing?

We'll get to how RUMvision is participating. But let's start with the basics: what even is Container Timing?

### Container Timing history

Although `elementtiming` is around for a while, the people at Bloomberg were missing an equivalent mechanism for larger HTML components and page sections. Coincidentally, [Jason and Paul Williams discussed this](https://www.youtube.com/watch?v=y1MCLZm8yAY&t=2640s) right after [our talk at performance.now() conference](https://www.rumvision.com/md/blog/performance-now-2024/).

### What Container Timing does

Container Timing lets you mark a part of a page and measure when visible content inside it is painted. You do this by adding a `containertiming="identifier"` attribute to a container, such as a hero section, product widget or cookie notice.

In other words, Container Timing is opt-in. The browser only reports entries for elements that were marked with `containertiming`. A RUM or any other script can observe existing entries, but it cannot reliably discover and measure every component after the fact.

Providing a value as `identifier` is up to you. Good identifiers are stable and human-readable, such as `hero`, `product-variants`, `checkout-summary` or `consent-banner`. Prevent using product- or even more dynamic ID or user-specific values as that would make collected data very noisy and harder to analyze.

As content inside that container appears, the browser emits performance entries with your chosen identifier. If more content is added or revealed later, additional entries can be emitted as well, as illustrated by this diagram:

![containertiming-entries.jpg](https://www.rumvision.com/file/upload/img/containertiming-entries.jpg)[Image by Igalia](https://blogs.igalia.com/dape/2026/03/26/the-implementation-of-container-timing-aggregating-paints-in-blink/)

This makes Container Timing useful for measuring components that render progressively, especially on modern JavaScript-heavy websites.

### Container vs Element Timing

That's also how it's different from `elementtiming`. Element Timing measures the paint timing of specific supported elements, such as images or text-containing elements. But it is also a one-shot signal: once that element has been painted and reported, you do not keep receiving updates for later changes.

Container Timing on the other hand lets you mark a broader container and keep observing paint activity inside that container. If more visible content appears later, for example via JavaScript, new entries can be emitted with updated information.

### Ignoring nodes

The latter brings us to a new challenge: you might want to ignore changes caused by certain nodes. For example, buttons within your cookie notice might not be as important. Instead of marking all other elements, you can add `containertiming` to the parent element, while ignoring those buttons.

This can be achieved by adding the `containertiming-ignore` attribute to those buttons.

```

  
    lorem ipsum
    Reject
  

```

This this can be useful for placeholders, skeleton loaders or UI controls that are not part of the final content you want to measure.

### Ignored paints

Not all paints result in Container Timing entries. The API is based on contentful paints, not on every visual repaint. Consider a sudden color change via JavaScript. That may repaint existing pixels, but that alone won't increase the painted region. Container Timing is [not intended to report every future repaint](https://developer.chrome.com/blog/container-timing-origin-trial#what_updates_count_for_container_timing).

### Above vs below the fold

If none of the painted region is visible within the viewport, you should not expect entries to be reported for what the user sees later.

When a below-the-fold component was already rendered, scrolling to it later does not necessarily create a new paint entry. Container Timing is therefore not an equivalent or replacement for the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).

## Origin Trial

[Bloomberg and Igalia collaborated](https://blogs.igalia.com/dape/2026/04/14/container-timing-moving-to-origin-trial/) on getting it implemented in the Chromium browser, with the challenge of doing so without slowing down the browser.

> I dive into the technical details of how I implemented the new Container Timing #webperf API in #Blink / #Chromium
> 
>  [José Dapena Paz on Bluesky](https://bsky.app/profile/jdapena.bsky.social/post/3mhy5vh7khc2l)

Developers could already test the Container Timing API. But it could not yet be used in production across real users. That's where this [Origin Trial](https://www.rumvision.com/md/blog/a-step-in-to-the-future-of-the-web-what-are-origin-trials/) comes in, allowing vendors like RUMvision to participate.

### Testing phase

At time of writing, Container Timing is in testing phase a.k.a. Origin Trial. At RUMvision, we decided to implement it as this API could unlock a new layer of real-user visibility into component-level performance, which could be of interest for our user base.

[Chrome for Developers](https://developer.chrome.com/blog/container-timing-origin-trial "Container Timing origin trial | Blog - Chrome for Developers") published the Container Timing Origin Trial announcement and explains how the API extends existing paint timing capabilities from individual elements to larger blocks of content.

The Origin Trial for Container Timing runs until Chrome version 153.

### Token versus flag

You can either enable “Experimental Web Platform Features” via chrome://flags, or [register an origin trial token for Container Timing](https://developer.chrome.com/origintrials/#/view_trial/3312960475884421121).

When enabling the flag, running a `PerformanceObserver` will only work in your browser. When using a token, you'll be able to track entries across real users on supported Chromium browsers, as it is a Chrome Origin Trial after all.

### Implementing the token

[This codepen by web.dev](https://codepen.io/web-dot-dev/pen/ogYvzgQ) illustrates how a token can be embedded if you're a third party:

```

  // For Container Timing API origin trial, you're best off placing this in your 
  // Get your token at https://developer.chrome.com/origintrials/#/view_trial/3312960475884421121
  const otMeta = document.createElement("meta");
  otMeta.httpEquiv = "origin-trial";
  otMeta.content = "your-token";
  document.head.append(otMeta);

```

Or so we thought. Because like many other origin trials we participate in, we did exactly this -but with our own token- to embed the token before our JS uses the API methods or Performance Observers.

This has worked for all of our other participations and makes adoption easier for our RUM customers. And in some cases, like with [fetchLater API](https://www.rumvision.com/md/blog/introducing-fetchlater-api-a-new-way-to-collect-core-web-vitals/), we just do it for all websites as it makes our lives easier.

But with Container Timing, that did not work. After asking around, a Web Performance Developer Advocate on the Google Chrome team explained why.

### Token timing matters

Typically, an Origin Trial token needs to be processed before the JavaScript feature is used. But aside from the JavaScript part, the `containertiming` attribute itself is also part of the origin trial.

In practice, the token needs to be available before the browser encounters the first `containertiming` attribute. And that's where our implementation failed, as site owners should not, and in our case are not, embedding RUM scripts render-blocking in the head.

That part of our plan failed, and we informed a participating client to register their own token. As soon as they did, we saw more datapoints appearing aside from our very own visits.

## Why component timing matters

Most performance metrics look at the page (or [navigation in case of SPAs](https://www.rumvision.com/md/blog/soft-navigations-better-core-web-vitals-attribution-for-spas/)) as a whole. That is useful, but modern websites are no longer just “a document that loads”.

A product page can contain:

- a product image
- product variants and prices
- reviews
- recommendations
- a checkout widget
- a cookie notice
- a third-party chat widget
 
Some of these areas are server-rendered. Others, like cookie notices, chat widgets, reviews, recommendations or even variant pricing, can be client-side rendered. Some are delayed by API calls. Others are hidden until a user clicks a button.

From the perspective of a site owner, the important question is often not:

When was the page loaded?

But rather:

When was this specific part of the page available to the user?

That's the gap Container Timing is trying to fill.

### LCP is not always specific enough

Largest Contentful Paint tells you when the largest content element in the viewport was rendered. That is valuable, but:

1. unlike Lighthouse and other lab data results, RUM data shows that even on the same page and device type, [LCP will not be the same for every visitor](https://www.rumvision.com/md/use-cases/be-wary-a-cookie-banner-may-cause-your-lcp-to-vary/#there-is-not "Visitors load pages in different conditions, which can result in multiple LCP elements").
2. this can be caused by different viewport sizes, but LCP also stops being measured after user interaction. So even if the component you care about is usually the LCP element, it will not be reported as LCP when it appears after the user already clicked or scrolled.
3. and often, the largest element simply is not the component you care about.
 
For example:

- a cookie notice can block access to the page
- a variant selector can block product decisions
- a delivery date widget can influence conversion
- a search results grid can be the main user experience
- a login form can decide whether the user can continue
 
None of these components are guaranteed to become the LCP element.

### Custom JavaScript timers are not the same

Tracking component readiness with custom JavaScript can be useful, but it usually measures when JavaScript thinks something has happened.

Container Timing is different. It is about when the browser painted pixels. That makes it closer to what the user could actually see.

## Container Timing use cases

### What should you mark?

Start with components that are both visible to users and important to the business. In my opinion, good candidates are components that influence conversion, block progress or explain user frustration when they appear late.

- hero sections
- product variant selectors
- checkout summaries
- search results
- cookie notices
- third-party widgets
 
### Client-side rendering / SPAs

Container Timing is not limited to server-rendered HTML. In fact, client-side rendered sections are one of the more interesting use cases:

```

```

Many developers will recognize this HTML boilerplate which serves the empty container where a JavaScript application, such as a React, Vue, or other single-page app, is rendered into the page.

Do note that I would not blindly mark a full application root in production, as this could produce too many entries and mix unrelated component updates. However, it does illustrate that the attribute can exist in server-rendered HTML, even when the actual contents are rendered client-side.

The `containertiming` attribute is not limited to server-rendered HTML. It can also be added client-side, for example by cookie notices that are loaded through a third-party JavaScript file. The important condition is that the attribute exists before the relevant paint happens.

One thing to watch out for is skeleton UI. If a skeleton loader is inside the marked container, that placeholder can contribute to Container Timing as well. Depending on what you want to measure, you may need to place the attribute around the final content, ignore placeholder nodes, or adjust your collection strategy.

### Cookie notices

Cookie notices are a surprisingly good candidate for Container Timing, as they are often loaded by third-party CMPs and render their DOM client-side.

Moreover, they produce a visually blocking or distracting experience that can't consistently be measured with just the LCP metric.

Aside from OneTrust, where I asked about introducing an `elementtiming` attribute, and maybe other CMPs, not every CMP uses images or headings in their cookie notice HTML. That makes `containertiming` the better candidate for measuring the full notice.

#### Marking a cookie notice

A CMP or website could add `containertiming="consent-banner"` to their consent banner.

That would make the container eligible for measurement by websites and RUM providers whenever the API is available. A first [CMP vendor already embraced this.](https://improve.consent.studio/nl/p/add-containertiming-to-both-v1-and-v2-consent-banners)

> adding some attributes for measurement purposes is something we'd happily take the lead in. :)
> 
>  Thierry Maasdam [via LinkedIn](https://www.linkedin.com/feed/update/urn:li:activity:7460973032346435584?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7460973032346435584%2C7461101434055593985%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287461101434055593985%2Curn%3Ali%3Aactivity%3A7460973032346435584%29)

Websites and third parties do need to add this attribute from the start, as it must be present before the relevant paint happens. Otherwise, the PerformanceObserver won't emit entries for that missed paint.

### Broader platform and vendor adoption

The biggest impact may come when (web and framework) platforms and (third party) vendors start adding `containertiming` themselves.

Imagine if the following shipped with meaningful names out of the box:

```
...
...
...
```

That would make component-level measurement much easier for site owners. And with standardized names, RUM providers are able to nicely group collected data.

However, there is no universal answer as to [when to stop tracking](https://github.com/WICG/container-timing/issues/13). A cookie notice, product variant selector and checkout summary may need different collection strategies.

## How RUMvision tracks Container Timing

At RUMvision, we added support for tracking configured Container Timing names and exposing those timings in our RUM data.

Container Timing starts with a simple `containertiming` attribute. Next step is to consume the data, which is done via a PerformanceObserver:

```
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.identifier, entry.startTime);
  }
}).observe({
  type: 'container',
  buffered: true
});
```

Using `buffered:true` helps developers and RUM scripts receive entries that were already created before the observer started. But it only works when the element was already marked with `containertiming` at the time it painted.

This allows developers, analytics tools and RUM providers to consume and report component timing data.

The browser API gives us the raw entries. The challenge is to decide which entries should be used, when to stop observing and how to normalize the value before sending it. For example, a first entry might represent “something appeared”. A later entry might better represent “the component became visually complete”.

### A small configuration layer

For our RUM use case, we introduced an additional, easy-to-implement `data-*` attribute:

```

  ...

```

The native `containertiming="hero"` attribute tells the browser what to observe, and by what `identifier`.

Our RUMvision implementation reads `data-container-timing-until` from the entry’s root element, merges it with defaults, and supports configuration such as:

- `entries` as entry count (i.e. `1` to stop tracking at the first entry)
- `interactions` (stop at a total interaction count)
- event-specific interaction limits, with support for `keydown`, `mousedown`, `touchstart`, `click` and `scrollend`
- `lcp`, which can be set to `true` to stop tracking as soon as LCP happened i.e. aligning behaviour with LCP
- `subtract` to specify if we should subtract the reported value with another value. For example to subtract FCP from the Container Timing's entry to get the actual duration from FCP to rendering.
- `value` to specify which numeric data of the entry object you want to report.
 
### Choosing the reported value

Container Timing entries can expose multiple timing fields:

```
{
  "name": "container-paints",
  "entryType": "container",
  "startTime": 644,
  "duration": 0,
  "navigationId": 4833,
  "paintTime": 610.5,
  "presentationTime": 644,
  "intersectionRect": {
    "x": 316,
    "y": 372,
    "width": 80,
    "height": 338,
    "top": 372,
    "right": 396,
    "bottom": 710,
    "left": 316
  },
  "size": 8680,
  "identifier": "list-variants",
  "firstRenderTime": 644
}
```

Our implementation falls back to `presentationTime` when available, otherwise `startTime`, while also allowing a configured field through `value`:

```
data-container-timing-until='{"value":"paintTime"}'
```

### Subtracting FCP

The absolute render time of a component can be useful, but it can also be misleading. If an A/B testing anti-flicker snippet hides the whole body for four seconds, a CMP might also report a render time around four seconds. That does not necessarily mean the CMP was the bottleneck.

That is why we support subtracting another metric, such as FCP. For consultants and site owners, the gap between FCP and a component timing can be more actionable:

- how much additional delay happened after the page started showing content?
- and how much closer to FCP can we get that component to render?
 
## Closing words

In short: `containertiming` may be just an attribute, but it opens the door to a much richer way of understanding real-user performance.

As this is still an experimental API, collected data should be treated as early field data rather than a long-term stable metric just yet. Names, fields and edge-case behaviour may still change while the specification evolves.

When it comes to the API as a whole though, [Mozilla's standards position for Container Timing is positive](https://github.com/mozilla/standards-positions/issues/1155), so once shaped up, we might soon see adoption beyond Chromium.