Scroll Timing API proposal - clarifications to some definitions#1233
Conversation
| | `framesProduced` | unsigned long | Number of frames actually rendered during the scroll | | ||
| | `checkerboardTime` | DOMHighResTimeStamp | Total duration (ms) that unpainted areas were visible during scroll | | ||
| | `duration` | DOMHighResTimeStamp | Total scroll duration from `startTime` until scrolling stops. A scroll interaction is considered complete when no scroll position changes have occurred for at least 150 milliseconds, or when a scroll end event is explicitly signaled (e.g., `touchend`, `scrollend`). Includes momentum/inertia phases. | | ||
| | `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)`. | |
There was a problem hiding this comment.
Looking at the definitions for framesExpected and framesProduced, it sounds like if I have a scroll that includes back-to-back identical frames, that would count against my perceived performance?
Some scenarios I'm thinking of (which might not all be possible, or might not all count):
- An animated scroll going 50px down, over a period of time larger than 50 frames (e.g. 1 second at 60Hz).
- Or if animated/smooth-scroll is not an option, then doing the same with a clicked-mouse-wheel to scroll down at a constant but very slow speed.
- A touch-based scroll where I stop for 100ms in the middle of the scroll and then resume moving.
- A keyboard-based scroll where each scroll is taking less than 100ms to complete, but my keypresses are coming every 140ms. So they get combined together based on the 150ms window, but have 40ms of non-scrolling time in between.
There was a problem hiding this comment.
Great question! You're right that the current design includes frames during stationary periods in framesExpected, which can result in lower smoothness scores for scenarios like the ones you mentioned.
The short answer: Yes, all four scenarios you described would count gap/pause frames against smoothness. This is intentional—the design provides raw measurements of "rendering opportunities vs. actual updates" rather than trying to filter out intentional low-velocity periods.
Addressing your scenarios:
-
50px animated scroll over 1 second (60 frames): ~50 frames produced, 83% smoothness
- For sub-pixel-per-frame animations, implementations SHOULD still count a frame as "produced" if the browser attempted to update scroll position, even if pixel rounding results in identical rendered positions. So this might score closer to 100% in practice.
-
Touch pause for 100ms mid-scroll: Those ~6 frames during the pause are included in
framesExpected- Correct, this reflects that the rendering pipeline had opportunities to update, but input velocity was zero
-
Keyboard with 40ms gaps: Frames during gaps count toward
framesExpected- Yes, because the 150ms threshold combines these into one entry, treating it as continuous scrolling intent
Why this design?
The alternative—excluding "stationary" frames from framesExpected—would require defining velocity thresholds and make it harder to distinguish "couldn't render" from "no motion." The current approach gives developers the raw data to make their own interpretations based on velocity and duration:
- High velocity + low smoothness = performance issue
- Low velocity + low smoothness = likely intentional
I've added clarification to address this:
- Detailed explanation in DESIGN_NOTES.md (Frame Counting and Stationary Periods section)
- Short summary in explainer.md (Understanding Frame Counts and Smoothness section)
Does this design make sense for your use case, or do you think we should reconsider the frame counting approach?
There was a problem hiding this comment.
My understanding is that during stationary periods, when no frame is expected, the Chromium compositor is not called. In fact, multiple optimizations are in place to avoid this extra cost. Did you have an opportunity to test stationary periods with your prototype implementation?
There was a problem hiding this comment.
The explanation makes sense and thanks for the write-up. I'm not familiar with what developer expectations would be in this respect, but this certainly sounds reasonable enough to get in the explainer and receive feedback.
I'll leave this comment unresolved for now for the sake of @ogerchikov's question
There was a problem hiding this comment.
My understanding is that during stationary periods, when no frame is expected, the Chromium compositor is not called. In fact, multiple optimizations are in place to avoid this extra cost. Did you have an opportunity to test stationary periods with your prototype implementation?
This was indeed a challenge during when prototyping it. The way I addressed it was to emit the entry only after the next frame that a movement is detected or if there is a scroll end signal (e.g. user lift finger). But I assume this is an implementation choice and shouldn't be included as part of the API spec, right?
|
|
||
| #### Direction Change Segmentation | ||
|
|
||
| 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. |
There was a problem hiding this comment.
I'm not familiar with what values get exposed, but does this mean an inertia-based overscroll would result in 2 scroll entries? (This is mostly me being curious)
There was a problem hiding this comment.
Good question! It's a single entry, but it ends when the scroll hits the boundary - the bounce-back effect isn't included.
Here's the thinking:
When you fling scroll down with momentum and hit the bottom, the entry ends right when you reach that boundary. The visual bounce that happens after (the rubber-band effect on iOS, for example) isn't captured because it's a platform UI thing that developers can't really control or optimize for.
Since the API is meant to help developers measure and improve their scroll performance, including the bounce-back would just add noise without giving them anything actionable to work with.
So what actually gets measured:
- Scrolling up to the boundary
durationstops when you hit the boundary (doesn't include the bounce animation)deltaX/deltaYreflect the actual scroll distance to the boundaryframesExpected/framesProducedonly count frames during the actual scrolling
One edge case worth mentioning: If you're already at the boundary and try to scroll further (frustrated scrolling), you still get an entry with deltaX/deltaY = 0, which can be useful for detecting that behavior. But again, no bounce measurement.
Updated the Edge Cases section in DESIGN_NOTES.md to clarify this.
Does this answer your question (or I misunderstood something)?
There was a problem hiding this comment.
Looking into it further I agree that the bounce back shouldn't fire an additional scroll event.
I am curious about the "frustrated scrolling" edge case though. I don't see where you call this out in the explainer (though I may have just missed it). If you would like this behavior included, can you add a section somewhere outlining it?
| | `framesProduced` | unsigned long | Number of frames actually rendered during the scroll | | ||
| | `checkerboardTime` | DOMHighResTimeStamp | Total duration (ms) that unpainted areas were visible during scroll | | ||
| | `duration` | DOMHighResTimeStamp | Total scroll duration from `startTime` until scrolling stops. A scroll interaction is considered complete when no scroll position changes have occurred for at least 150 milliseconds, or when a scroll end event is explicitly signaled (e.g., `touchend`, `scrollend`). Includes momentum/inertia phases. | | ||
| | `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)`. | |
There was a problem hiding this comment.
The explanation makes sense and thanks for the write-up. I'm not familiar with what developer expectations would be in this respect, but this certainly sounds reasonable enough to get in the explainer and receive feedback.
I'll leave this comment unresolved for now for the sake of @ogerchikov's question
…le for combined entries in the Scroll Timing API proposal
|
|
||
| #### Direction Change Segmentation | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Looking into it further I agree that the bounce back shouldn't fire an additional scroll event.
I am curious about the "frustrated scrolling" edge case though. I don't see where you call this out in the explainer (though I may have just missed it). If you would like this behavior included, can you add a section somewhere outlining it?
|
|
||
| A scroll interaction is considered **active** while the user is continuously interacting with a scroll gesture (e.g., finger on screen during touch scrolling, dragging a scrollbar thumb, holding a scroll key). | ||
| This means: | ||
| - **Very slow scrolls** (less than ~1 pixel per frame) will show lower smoothness scores even if rendering is perfect |
There was a problem hiding this comment.
To align with the recommendation for browsers to count subpixel changes in their framesProduced, what about tweaking this to say that very slow scrolls could cause a drop in smoothness, but ideally still won't?
Maybe something as small as:
| - **Very slow scrolls** (less than ~1 pixel per frame) will show lower smoothness scores even if rendering is perfect | |
| - **Very slow scrolls** (less than ~1 pixel per frame) MAY show lower smoothness scores even if rendering is perfect, depending on browser implementation |
|
|
||
| A scroll interaction is considered **complete** when: | ||
| 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 | ||
| 2. A scroll end event is explicitly signaled (e.g., `touchend` for touch scrolling, `scrollend` event) |
There was a problem hiding this comment.
I made a play space to look at the scroll and scrollend existing behaviors:
https://codepen.io/mhochk/pen/LEZmBgr
Toying around, it looks like scrollend almost matches all of your desired end times already:
- It does combine multiple keystrokes that happen quickly in succession, firing
scrollendonly once (though likely it's not a 150ms gap, but a requires the scroll not be "complete") - It fires after the bounce of an overscroll
- It fires upon releasing the scrollbar, a finger being used for scrolling, or center-clicking to exit auto-scroll mode.
- *It isn't firing at all if I excessively hold down the page up/down key and then release. This feels like a bug, so I'm looking into filing one.
Would it be reasonable to simplify the definition of an entry period to work off the existing scrollend definition/behavior? Something akin to:
A scroll entry starts when a request is made to change the scroll position.
A scroll entry ends when:
- A scroll end event is explicitly signaled (i.e. the `scrollend` event is fired) OR
- The scroll direction changes OR
- Retroactively if the scroll position does not change for at least **150 milliseconds** (approximately 9 frames at 60Hz)
There was a problem hiding this comment.
FWIW on the bug I mentioned (in case you play around and encounter it as well):
Normal behavior:
- Hold down page down until it starts to auto repeat
- Release before reaching the end of the scroll region
scrollendcorrectly fires
Bug Repro:
- Hold down page down until it starts to auto repeat
- Wait until you have scrolled the entire scroll region down
- Release the key
scrollenddoes not fire
I've confirmed this doesn't repro in Firefox or Safari, so will get a proper bug filed to track this.
There was a problem hiding this comment.
Hi!
This PR has the required approval and is ready to merge.
Since I don’t have permissions to approve/merge, could someone with write access please merge it?
@mwjacksonmsft @ogerchikov @mhochk
Thanks!
This pull request improves the clarity and completeness of the
PerformanceScrollTimingexplainer documentation. The main changes provide more precise definitions for key metrics, introduce detailed rules for when scroll timing entries are emitted, and update acknowledgements.Key documentation improvements:
PerformanceScrollTiminginterface, including more precise descriptions forduration,framesExpected,framesProduced, andcheckerboardTime. These now specify calculation methods, default behaviors, and implementation guidance.PerformanceScrollTimingentries are created. This includes rules for scroll end detection, direction change segmentation, and input-type-specific entry boundaries.Acknowledgements update: