You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Scroll Timing API proposal - clarifications to some definitions (#1233)
* Updates to clarify some of the definitions used in scroll timing API
* Enhance scroll timing proposal with detailed frame counting and smoothness metrics
* Refine entry emission rules for scroll sources in the Scroll Timing API proposal
* Add detailed examples for 150ms timeout mechanics in Scroll Timing API proposal
* Clarify boundary conditions and overscroll effects in scroll performance metrics
* Clarify duration calculation and entry emission rules in Scroll Timing API proposal
* Clarify programmatic scroll entry emission rules and provide an example for combined entries in the Scroll Timing API proposal
---------
Co-authored-by: Noam Helfman <noamh@microsoft.com>
Copy file name to clipboardExpand all lines: PerformanceScrollTiming/DESIGN_NOTES.md
+50-5Lines changed: 50 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,6 +70,50 @@ Even if scroll starts quickly, dropped frames during scrolling create visible ja
70
70
- Image decoding on the main thread
71
71
- Garbage collection pauses
72
72
73
+
### Frame Counting and Stationary Periods
74
+
75
+
The frame counting mechanism (`framesExpected` vs `framesProduced`) is designed to measure **rendering performance during active scrolling**, not to penalize intentionally slow or paused scroll gestures.
76
+
77
+
**Core principle:**
78
+
During an active scroll gesture, the browser is expected to produce visual updates that reflect scroll position changes. `framesExpected` represents the number of opportunities (display refresh cycles) to update the display during the scroll duration, while `framesProduced` counts how many of those opportunities actually resulted in a position change being rendered.
79
+
80
+
**Handling of stationary periods:**
81
+
82
+
1.**Very slow scrolls**: For animated or smooth scrolls where scroll velocity results in sub-pixel movement per frame, implementations SHOULD still count a frame as "produced" if the browser attempted to update the scroll position, even if pixel-level rounding results in identical rendered positions across consecutive frames. The intent is to measure rendering pipeline performance, not scroll speed.
83
+
84
+
2.**Mid-gesture pauses**: When a user pauses during a continuous touch gesture (finger down but not moving), the 150ms scroll-end detection threshold determines when the entry completes. Brief pauses under 150ms remain part of the same entry:
85
+
- Frames during the pause are included in `framesExpected` (the scroll is still "active")
86
+
- Frames during the pause typically won't increment `framesProduced` (no position change)
87
+
- This reflects that the rendering pipeline had opportunities to update but the input velocity was zero
88
+
89
+
3.**Discrete scroll events with gaps**: For input sources like keyboard or wheel scrolling, multiple discrete events within 150ms are combined into a single entry:
90
+
- Example: Key held for 100ms, released, then pressed again after 40ms → combined into one entry
91
+
- Frames during the 40ms gap count toward `framesExpected`
92
+
- This reflects the continuous nature of the user's scrolling intent, even if input delivery is discrete
93
+
94
+
**Interpreting smoothness scores:**
95
+
96
+
The smoothness score (`framesProduced / framesExpected`) measures **motion continuity** during the scroll interaction. A low smoothness score can indicate:
97
+
-**Dropped frames**: The browser couldn't render updates fast enough (performance issue)
98
+
-**Very slow scroll velocity**: Intentionally slow scrolling where position changes less frequently than the refresh rate (not a performance issue)
99
+
-**Pauses and gaps**: User paused mid-gesture or discrete input events had temporal gaps (not a performance issue)
100
+
101
+
Developers should consider scroll velocity and duration when interpreting smoothness:
- Low velocity + low smoothness = may be intentional slow scroll
104
+
- Short duration + low smoothness but high `framesProduced` = likely fine (few expected frames due to brief interaction)
105
+
106
+
**Rationale for current design:**
107
+
108
+
Alternative designs considered:
109
+
1.**Only count "active motion" frames in `framesExpected`**: This would require detecting scroll velocity and excluding frames with zero velocity. However, this is complex to specify (what threshold defines "active"?), makes the metric harder to reason about, and obscures the difference between "couldn't render" and "no motion."
110
+
111
+
2.**Separate `duration` into active vs. total time**: This adds API surface complexity and still requires defining "active" scrolling.
112
+
113
+
The current design provides raw measurements that allow developers to calculate derived metrics based on their specific needs. The trade-off is that smoothness scores must be interpreted in context with velocity and duration.
114
+
115
+
**Open question**: Should we provide additional metrics to help distinguish rendering performance issues from intentional low-velocity scrolling? See [OPEN_QUESTIONS.md](OPEN_QUESTIONS.md#smoothness-scoring-options) for related discussion on smoothness calculation methods.
116
+
73
117
## Scroll Checkerboarding
74
118
Scroll checkerboarding occurs when content is not ready to be displayed as it scrolls into the viewport, resulting in blank or placeholder areas.
75
119
@@ -160,8 +204,8 @@ Scroll interactions can be interrupted or cancelled mid-stream. This section def
160
204
-`duration` includes the snap animation time
161
205
162
206
5.**Boundary collision**: Scroll reaches container bounds and cannot continue.
163
-
- Entry ends naturally when scrolling stops at the boundary
164
-
- Overscroll/bounce effects (on supported platforms) are included in `duration`
207
+
- Entry ends when scrolling reaches the boundary (last actual scroll position change)
208
+
- Overscroll/bounce effects (platform visual effects) are NOT included in measurements - they are not developer-controllable and don't reflect scroll performance
165
209
166
210
**Entry emission timing:**
167
211
Entries are emitted after the scroll interaction fully completes (including momentum, snap, and settle phases). Interrupted scrolls emit entries at the interruption point with metrics reflecting the partial interaction.
@@ -183,9 +227,10 @@ This section documents expected behavior for boundary conditions and unusual sce
183
227
184
228
**Overscroll and bounce effects:**
185
229
- On platforms with overscroll (iOS rubber-banding, Android overscroll glow):
186
-
-`deltaX`/`deltaY` reflect the actual scroll position change, not the visual overscroll
187
-
-`duration` includes the bounce-back animation time
188
-
- Overscroll does not count as checkerboarding
230
+
- The scroll entry ends when the scrollable boundary is reached
231
+
-`deltaX`/`deltaY` reflect the actual scroll distance to the boundary, not the visual overscroll effect
232
+
-`duration` ends when the scroll reaches the boundary, NOT including the bounce-back animation time
233
+
- Overscroll bounce-back is a platform visual effect that developers cannot control or optimize, so it is not measured
189
234
190
235
**Scroll-linked animations:**
191
236
- If the page uses `scroll-timeline` or JavaScript scroll-linked animations:
|`name`| DOMString | Always `"scroll"` (inherited from PerformanceEntry) |
110
111
|`startTime`| DOMHighResTimeStamp | Timestamp of the first input event that initiated the scroll or code invocation timestamp for programmatic scroll|
111
112
|`firstFrameTime`| DOMHighResTimeStamp | Timestamp when the first visual frame reflecting the scroll was presented |
112
-
|`duration`| DOMHighResTimeStamp | Total scroll duration from `startTime`until scrolling stops (includes momentum/inertia)|
113
-
|`framesExpected`| unsigned long | Number of frames that should have rendered at the target refresh rate |
114
-
|`framesProduced`| unsigned long |Number of frames actually rendered during the scroll |
115
-
|`checkerboardTime`| DOMHighResTimeStamp |Total duration (ms) that unpainted areas were visible during scroll|
113
+
|`duration`| DOMHighResTimeStamp | Total scroll duration from `startTime`to the last scroll position change. Entry emission is triggered when: (1) no scroll position changes have occurred for 150ms (inactivity detection), or (2) a scroll end event is explicitly signaled (e.g., `touchend`, `scrollend`). The 150ms timeout is for detecting when scrolling has stopped, but is not included in the duration. Includes momentum/inertia phases.|
114
+
|`framesExpected`| unsigned long | Number of frames that would be rendered at the display's refresh rate during the scroll duration. Implementations SHOULD use the actual display refresh rate when available, and MAY fall back to 60Hz as a default. Calculated as `ceil(duration / vsync_interval)`.|
115
+
|`framesProduced`| unsigned long |Count of distinct visual updates presented to the display that reflected scroll position changes. A frame is considered "produced" when it is presented and contains a different scroll position than the previous frame.|
116
+
|`checkerboardTime`| DOMHighResTimeStamp |Duration (ms) during which any visible area of the scrolled content was not fully painted. Implementations MAY return 0 if unpainted region visibility is not tracked.|
116
117
|`deltaX`| long | Horizontal scroll delta in pixels (positive = right, negative = left) |
117
118
|`deltaY`| long | Vertical scroll delta in pixels (positive = down, negative = up) |
-**Scroll velocity**: `totalDistance / duration * 1000` — scroll speed in pixels per second
127
128
129
+
### Understanding Frame Counts and Smoothness
130
+
131
+
**Frame counting during stationary periods:**
132
+
133
+
The `framesExpected` metric counts all frame opportunities during the scroll `duration`, including periods where scroll velocity is zero (pauses, gaps between discrete events). The `framesProduced` metric only counts frames where the scroll position actually changed.
134
+
135
+
This means:
136
+
-**Very slow scrolls** (less than ~1 pixel per frame) will show lower smoothness scores even if rendering is perfect
137
+
-**Pauses mid-gesture** (touch finger stationary, but not lifted) will reduce smoothness scores
138
+
-**Discrete scrolls with gaps** (keyboard presses with temporal gaps, combined due to 150ms threshold) include gap frames in expected count
139
+
140
+
**Why this design?**
141
+
142
+
This approach provides raw, unfiltered measurements of rendering opportunities vs. actual updates. Developers can combine smoothness with velocity and duration to distinguish:
143
+
-**Performance issues**: High velocity + low smoothness = dropped frames
Alternative designs that exclude "stationary" frames from `framesExpected` would require arbitrary velocity thresholds and obscure the distinction between "couldn't render" and "no motion."
147
+
148
+
For advanced use cases, developers can implement custom logic to filter or segment scroll entries based on velocity characteristics before calculating aggregate smoothness metrics.
149
+
150
+
For detailed discussion of frame counting behavior, see the "Frame Counting and Stationary Periods" section in [DESIGN_NOTES.md](DESIGN_NOTES.md#frame-counting-and-stationary-periods).
151
+
152
+
### Entry Emission Rules
153
+
154
+
This section defines when `PerformanceScrollTiming` entries are emitted.
155
+
156
+
#### Entry Granularity by Scroll Source
157
+
158
+
Entry emission rules vary by input type to match natural interaction boundaries:
159
+
160
+
| Scroll Source | Entry Boundary |
161
+
|--------------|----------------|
162
+
|`"touch"`| One entry per continuous gesture (`touchstart` → `touchend`), split on direction changes |
163
+
|`"wheel"`| One entry per scroll interaction; consecutive wheel events are combined into a single entry if they occur within 150ms of each other |
164
+
|`"keyboard"`| One entry per key repeat sequence (from `keydown` until key release + 150ms inactivity) |
165
+
|`"programmatic"`| One entry per scroll interaction; consecutive programmatic scroll calls (e.g., `scrollTo()`, `scrollBy()`) on the same scrollable element are combined into a single entry if they occur within 150ms of each other |
166
+
|`"other"`| One entry per scroll interaction, ending after 150ms of inactivity |
167
+
168
+
#### Scroll End Detection
169
+
170
+
A scroll interaction is considered **active** while the user is continuously interacting with a scroll gesture. What constitutes "active interaction" depends on the input type (see table above):
171
+
-**Touch**: Finger is touching and moving on the screen
172
+
-**Wheel**: Mouse wheel events are being received
173
+
-**Keyboard**: Scroll key is held down
174
+
-**Other**: Scrollbar is being dragged, or other input method is engaged
175
+
176
+
A scroll interaction is considered **complete** when:
177
+
1. The user is no longer actively interacting AND no scroll position changes have occurred for at least **150 milliseconds** (approximately 9 frames at 60Hz), OR
178
+
2. A scroll end event is explicitly signaled (e.g., `touchend` for touch scrolling, `scrollend` event)
179
+
180
+
This timeout allows momentum/inertia scrolling to be included in the same entry as the initiating gesture.
181
+
182
+
**150ms timeout mechanics:**
183
+
184
+
The 150ms inactivity timer starts from the **last scroll position change**, not from the initial input event. This timer determines when to emit the entry, but the **`duration` always ends at the last position change**, not 150ms later. The 150ms is purely a detection mechanism - it's the waiting period to confirm scrolling has stopped, but doesn't represent actual scroll activity.
185
+
186
+
**Example - Wheel scrolling:**
187
+
- Wheel event at `0ms` - this event timestamp becomes `startTime` (`0ms`)
188
+
- First visual scroll position change occurs shortly after (e.g., `8ms` = `firstFrameTime`, may be later if jank)
189
+
- Scroll position continues changing until `80ms` (last position change)
190
+
- Inactivity timer starts at `80ms`
191
+
- If another wheel event arrives before `230ms` (`80ms + 150ms`):
192
+
- Events are combined into one entry
193
+
- Timer resets based on when position changes from the new event stop
194
+
- If no event arrives by `230ms`: Entry emits with `duration = 80ms` (time to last position change, not including the 150ms wait)
195
+
196
+
**Example - Keyboard scrolling:**
197
+
- Key press at `0ms` - this event timestamp becomes `startTime` (`0ms`)
198
+
- First visual scroll position change occurs shortly after (e.g., `10ms` = `firstFrameTime`, may be later if jank)
199
+
- Key held until `100ms`, position changes until `120ms`
200
+
- Key released at `100ms`
201
+
- Inactivity timer starts at `120ms` (last position change)
202
+
- If another key press before `270ms` (`120ms + 150ms`): Combined into same entry
203
+
- Otherwise entry emits with `duration = 120ms` (time to last position change)
204
+
205
+
**Example - Scrollbar dragging:**
206
+
- User starts dragging scrollbar thumb at `0ms` - this event timestamp becomes `startTime` (`0ms`)
207
+
- First visual scroll position change occurs shortly after (e.g., `8ms` = `firstFrameTime`, may be later if jank)
208
+
- Continues dragging until `200ms`, scroll position changes continuously
209
+
- User releases thumb at `200ms` (last position change)
210
+
- Inactivity timer starts at `200ms`
211
+
- Entry emits at `350ms` with `duration = 200ms` (time to last position change)
212
+
213
+
**Example - Autoscroll (middle-click):**
214
+
- User middle-clicks to enter autoscroll mode (no entry yet)
215
+
- User moves cursor away from anchor point - cursor movement event timestamp becomes `startTime` (`0ms`)
216
+
- First visual scroll position change occurs shortly after (e.g., `10ms` = `firstFrameTime`, may be later if jank)
217
+
- Autoscroll continues based on cursor position, position changes continuously
218
+
- User clicks again to stop autoscroll at `500ms` (last position change)
219
+
- Inactivity timer starts at `500ms`
220
+
- Entry emits at `650ms` with `duration = 500ms` (time to last position change)
221
+
- Note: If user middle-clicks but never moves cursor away (no cursor movement event), no entry is emitted
- User touches screen (`touchstart`) - no entry yet
225
+
- User begins dragging finger at `0ms` - this touch gesture timestamp becomes `startTime` (`0ms`)
226
+
- First visual scroll position change occurs at `16ms` (`firstFrameTime` - may be later if jank)
227
+
- User continues dragging until `200ms`, then pauses (finger still on screen, but not moving)
228
+
- Inactivity timer starts at `200ms` (last position change during the pause)
229
+
- If user resumes dragging before `350ms` (`200ms + 150ms`): continues as same entry, timer resets
230
+
- If no movement by `350ms`: entry emits with `duration = 200ms` (time to last position change), even though finger is still on screen
231
+
- Note: `startTime` is the touch gesture event timestamp, not when viewport visually moves (this allows measuring scroll start latency)
232
+
233
+
**Example - Touch scrolling (lift with momentum):**
234
+
- User touches screen and begins dragging - touch gesture timestamp at `0ms` becomes `startTime`
235
+
- First visual scroll position change occurs shortly after (e.g., `16ms` = `firstFrameTime`)
236
+
- User continues dragging until lifting finger at `300ms` (`touchend`)
237
+
-**If no momentum:** Entry emits immediately with `duration = 300ms` (or whenever last position change occurred)
238
+
-**If momentum occurs:** Momentum scrolling continues until naturally stopping at `800ms` (last position change), entry emits with `duration = 800ms` (no 150ms timer needed - `touchend` or momentum stopping signals the end)
239
+
- Note: If user touches screen but never moves finger (no scroll gesture occurs), no entry is emitted
240
+
241
+
**Example - Programmatic scrolling (gamepad joystick mapped to scrollBy):**
242
+
- User tilts gamepad joystick down, triggering `scrollBy(0, 50)` at `0ms` - this API call timestamp becomes `startTime` (`0ms`)
243
+
- First visual scroll position change occurs shortly after (e.g., `10ms` = `firstFrameTime`, may be later if jank)
244
+
- Joystick remains tilted, triggering additional `scrollBy(0, 50)` calls every 100ms at `100ms`, `200ms`, `300ms`
245
+
- Each call causes position changes, last position change at `350ms`
246
+
- User releases joystick at `300ms`
247
+
- Inactivity timer starts at `350ms` (last position change)
248
+
- If another `scrollBy()` call on the same element before `500ms` (`350ms + 150ms`): Combined into same entry
249
+
- Otherwise entry emits at `500ms` with `duration = 350ms` (time to last position change)
250
+
- Note: Consecutive programmatic scrolls are only grouped if they target the same scrollable element
251
+
252
+
#### Direction Change Segmentation
253
+
254
+
A new scroll timing entry MUST be emitted when the scroll direction reverses (i.e., `deltaX` or `deltaY` changes sign during the scroll). This means a single scroll gesture can produce multiple entries if the user reverses direction mid-scroll.
255
+
256
+
**Rationale**: Direction reversals represent distinct scroll interactions from a performance measurement perspective, as they may trigger different rendering paths and affect smoothness calculations.
257
+
128
258
### Example Usage with PerformanceObserver
129
259
130
260
```javascript
@@ -281,4 +411,4 @@ See [polyfill.js](polyfill.js) for the full implementation.
281
411
**Note:** This polyfill uses heuristics-based approximations due to the lack of relevant native APIs required for accurate scroll performance measurement. It is intended for demonstration and prototyping purposes only. Metrics like checkerboarding detection and precise frame timing cannot be accurately measured without browser-level instrumentation. A native implementation would have access to compositor data, rendering pipeline information, and other internal metrics not exposed to JavaScript.
282
412
283
413
### Acknowledgements
284
-
Many thanks for valuable feedback and advice from: Alex Russel, Mike Jackson, Olga Gerchikov, Andy Luhr, Pninit Goldman, Roee Barnea for guidance and contributions.
414
+
Many thanks for valuable feedback and advice from: Alex Russel, Mike Jackson, Olga Gerchikov, Andy Luhr, Hoch Hochkeppel, Pninit Goldman, Roee Barnea for guidance and contributions.
0 commit comments