diff --git a/performanceMarkPaintTime/explainer.md b/performanceMarkPaintTime/explainer.md new file mode 100644 index 00000000..0865d50a --- /dev/null +++ b/performanceMarkPaintTime/explainer.md @@ -0,0 +1,163 @@ +# performance.markPaintTime() Explainer + +Authors: [Wangsong Jin](https://github.com/JosephJin0815) - Engineer at Microsoft Edge + +## Status of this Document + +This document is a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions. + +* This document status: **Active** +* Expected venue: [W3C Web Incubator Community Group](https://wicg.io/) +* Current version: this document + +## Table of Contents + +- [Introduction](#introduction) +- [Goals](#goals) +- [Non-goals](#non-goals) +- [The Problem](#the-problem) +- [Proposed API](#proposed-api) +- [Spec Draft](#spec-draft) +- [Entry Properties](#entry-properties) +- [Key Design Decisions](#key-design-decisions) +- [Alternatives Considered](#alternatives-considered) +- [Security and Privacy Considerations](#security-and-privacy-considerations) + +## Introduction + +Web developers need to measure when their visual updates actually render — not just the browser-detected milestones like First Paint or Largest Contentful Paint, but any update they care about: a component mount, a state transition, a style change. + +The platform already captures paint and presentation timestamps for key moments via PaintTimingMixin, but only for entries the browser selects automatically. `performance.markPaintTime()` extends this capability to let developers capture the same `paintTime` and `presentationTime` for any visual update, on demand. + +## Goals + - Give developers on-demand access to `paintTime` and `presentationTime` for any visual update. + - Deliver timestamps through `PerformanceObserver`, consistent with modern performance APIs. + +## Non-goals + - **Replacing existing paint timing entries.** FP, FCP, LCP, Event Timing, and LoAF continue to serve their existing purposes. + - **Forcing a rendering update.** `markPaintTime()` does not cause a rendering opportunity — it tags the next one that naturally occurs. + +## The Problem + +Without an on-demand API, developers resort to workarounds like double-rAF or rAF+setTimeout to approximate when the rendering update completes, but these workarounds are unreliable (see [Nolan Lawson's post](https://nolanlawson.com/2015/09/29/the-difference-between-throttling-and-debouncing/)). Furthermore, no workaround can provide `presentationTime` — the actual time when pixels appear on screen. For example, a developer measuring when a virtual DOM (vdom) change actually lands on the real DOM and renders: + +### Double requestAnimationFrame + +```javascript +// React component measuring vdom → DOM rendering +function MyComponent() { + useLayoutEffect(() => { + // useLayoutEffect fires after React's DOM commit but before the browser's + // rendering update. Resort to double-rAF to approximate paint timing. + requestAnimationFrame(() => { + requestAnimationFrame(() => { + performance.mark('component-rendered'); + }); + }); + }); +} +``` + +This is a widely-used approach to approximate when a vdom change actually lands on the DOM. However, we cannot be guaranteed that we are looking at the frame that corresponds to the change 100% of the time. This gets worse when observers (e.g., ResizeObserver, IntersectionObserver) are present — their callbacks add work between frames, making the second rAF even less likely to land on the expected frame. + +### requestAnimationFrame + setTimeout + +```javascript +// Alternative approach used with React useLayoutEffect +function MyComponent() { + useLayoutEffect(() => { + requestAnimationFrame(() => { + setTimeout(() => { + performance.mark('component-rendered'); + }, 0); + }); + }); +} +``` + +This is more accurate but less precise because now we are well past the frame in the next task. The overshoot is non-deterministic due to other queued tasks. + +Both approaches presuppose React via `useLayoutEffect` to measure "on DOM and interactive." Both workarounds exist because there is no API to get paint timing for arbitrary visual updates. + +### With markPaintTime: + +```javascript +function MyComponent() { + useLayoutEffect(() => { + performance.markPaintTime("component-rendered"); + }); +} + +// Observe the result +const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + // paintTime — captured during the rendering opportunity, after style+layout + // presentationTime — the time when pixels were actually shown on display + console.log(`Paint time: ${entry.paintTime}ms`); + console.log(`Presentation time: ${entry.presentationTime}ms`); + console.log(`Rendering latency: ${entry.paintTime - entry.startTime}ms`); + } +}); +observer.observe({ type: "mark-paint-time" }); +``` + +Benefits: +- **Accurate time**: No idle time gap, no task queue delay. +- **Main-thread rendering cost**: `paintTime - startTime` captures the time from the call to the paint phase of the rendering update. +- **End-to-end visual latency**: `presentationTime - startTime` captures the full latency until pixels are actually shown on the display. + +## Proposed API + +### Spec Draft + +We extend the existing Paint Timing spec by adding `markPaintTime()` to the Performance interface. The returned entry includes `PaintTimingMixin`, reusing the same `paintTime` and `presentationTime` attributes that FP/FCP/LCP already define: + +```webidl +// Extends Paint Timing spec — https://w3c.github.io/paint-timing/ +partial interface Performance { + undefined markPaintTime(DOMString markName); +}; + +[Exposed=Window] +interface PerformancePaintTimeMark : PerformanceEntry { + [Default] object toJSON(); +}; +PerformancePaintTimeMark includes PaintTimingMixin; + +// PaintTimingMixin already defined in Paint Timing spec: +// interface mixin PaintTimingMixin { +// readonly attribute DOMHighResTimeStamp paintTime; +// readonly attribute DOMHighResTimeStamp? presentationTime; +// }; +``` + + ### Entry Properties + + | Attribute | Type | Description | + |-----------|------|-------------| + | `entryType` | DOMString | Always `"mark-paint-time"` (inherited from PerformanceEntry) | + | `name` | DOMString | The mark name passed to `markPaintTime()` (inherited from PerformanceEntry) | + | `startTime` | DOMHighResTimeStamp | `performance.now()` at the time `markPaintTime()` was called | + | `duration` | DOMHighResTimeStamp | Always `0` | + | `paintTime` | DOMHighResTimeStamp | The rendering update end time, captured at [step 21 ("mark paint timing")](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model) of the event loop processing model. Same as FP/FCP/LCP `paintTime`. | + | `presentationTime` | DOMHighResTimeStamp? | Implementation-defined presentation time when the composited frame is actually presented to the display. Same semantics as FP/FCP/LCP `presentationTime`. | + +## Key Design Decisions + +- **Reuses PaintTimingMixin**: No new timestamp concepts — `paintTime` and `presentationTime` are the same timestamps that FP/FCP/LCP already expose. Developers who understand paint timing milestones already understand this API. +- **On-demand**: Unlike FP/FCP/LCP which fire automatically for browser-detected milestones, `markPaintTime()` is triggered by the developer for any visual update at any time. +- **PerformanceObserver-based**: Consistent with modern performance APIs (LoAF, FCP, LCP). + +## Alternatives Considered + +### requestPostAnimationFrame (rPAF) + +`requestPostAnimationFrame` by design fires immediately after the rendering update completes. Calling `performance.now()` inside the callback could approximate `paintTime`, but: + +- Cannot provide `presentationTime` — rPAF fires on the main thread, before compositor/GPU work. +- The proposal's original author has concluded that a post-animation callback is not actually useful for its intended purpose (optimizing rendering latency), and the proposal is not being pursued. + +## Security and Privacy Considerations + +- `paintTime` and `presentationTime` are subject to the same cross-origin coarsening as existing paint timing entries. +- Timestamps are coarsened to mitigate timing side-channel attacks, consistent with `performance.now()` resolution restrictions.