What’s changing with scrolling on macOS
by Simon Fraser (Apple)
An update on scrolling.
Simon: Talking about scrolling changes. Working on specifically macOS. Hopefully none of this is from last year.
Simon: Talking about overflow scrolling sync and then a more recent (Safari only for now) thats about more asynchronous scrolling
Simon: Overflow scroll Async is scrolling overflow areas on the scrolling thread, which means that even if JS or other things are on the main thread, you still get some scrolling.
Simon: We have a scrolling tree, which is the tree of nodes that represents a scrollable area.
Simon: For the longest time you could set scrolling: touch and then you would get fast overflow scrolling with the side effect of creating a stacking context. We wan't to implement this on Mac without this quirk. This involved being able to determine what is under the cursor off of the main thread. --- Simon: An element that has rounded corners needs hit test to ignore those corners.
Simon: We build this region using a fake paint.
Simon: On Mac this is used for interative region and wheel event listeners.
Simon:On iOS used touchAction CSS property.
Simon: Third use is editabe regions.
Simon: We have good impl of event loop in Page.
Simon: We build these event regions once per frame.
Simon: Updated in render layer backing by a fake paint.
Simon: Instead of drawing, we build the even region in a dummy graphics context.
Simon: Event region is the union of the fake painting, taking care of rounded corners and other cases.
Simon: Better than finding event handlers and regions - this is more performant and more accurate since it matches painting.
Simon: We use fake regions for interactive region, touch action, wheel events, and editable content.
Simon: [Showing example] It's fine if we overestimate region, not good if we underestimate.
Simon: Blue overlay is showing the region that is interactive.
Simon: That's how we do hit testing off the main thread.
Simon: Additional compositing complexity on Mac.
Simon: CSS is crazy. We have containing block hierarchy, but for z-order tree to build CALayerTree.
Simon: We have to represent the clipping to do clipping through CA.
Simon: This isn't all the time, but it does need taken care of.
Simon: We have to represent overflow scroll clipping in the z-order.
Simon: It's possible for elements to sit ontop of the scroll bar, so the scroll bar has to be it's own layer on the Mac.
Simon: We end up with crazy clipping order because of this.
Simon: On the Mac you now always get async scrolling unless you have fixed background or wheel event listeners.
Simon: Scroll sync.
Simon: Mac had worse behavior with things that behaved worse like fake position, sticky position, fixed.
Simon: In old world, wheel events went to Event dispatcher thread.
Simon: Event dispatcher thread sends those to the scrolling thread.
Simon: [Showing diagram] This is three frames, but wheel events don't match rendering frequency in all cases.
Simon: Immediately adjusted CALayer positions and flush changes to screen.
Simon: Meanwhile main thread is running it's rendering update cycle.
Simon: Main thread has it's own notion of where layers are. Happens every frame.
Simon: What shows up on the screen was the last thing commited.
Simon: On the first frame, that's the render, on the second frame thats the scrolling.
Simon: There was a worse problem.
Simon: We only tell the main thread that things have changes asynced, so the main thread could have stale state mid-frame.
Simon: Guaranteed main thread would be stale re: visible scroll position.
Simon: Fixing this required...
Simon: Scrolling thread needs to know when rendering updates will be triggered.
Simon: Display refresh now notifies scrolling thread which then notifies main thread.
Simon: Main thread tells scrolling thread when it starts and finishes working.
Simon: Scrolling thread can avoid a commit this way.
Simon: The good case: Main thread responsive. Main thread synchronously fetches scrolling state from scrolling thread.
Simon: Scrolling thread then waits for main thread.
Simon: When main thread is done, it does a CACommit and unlocks the scrolling thread.
Simon: This can go bad...
Simon: Long request animation frame.
Simon: Scrolling thread will time out waiting for the main thread to ensure fast async scrolling if the main thread is behind.
Simon: In that case scrolling thread does trigger rendering.
Simon: Unresponsive main thread falls back to allowing the scrolling thread to commit frames.
Simon: [Demo]
Simon: This old version of Safari shows the bug.
Simon: This content is scrollable, and we have two boxes to move around, on that is position fixed, the other is fake fixed (on scroll it moves).
Simon: Main thread is responsive in this case.
Simon: Even so, with the fake fixed we don't keep the fake fixed element in one position while scrolling.
Simon: When fixed, the orange box is almost always in the right place (much better).
Simon: If we force random blocks on the main thread, we see it falls back and behaves as it did before.
Simon: Okay, that's scroll synchronization.
Simon: More features for scroll performance...
Simon: Wheel event handlers: There is an option to make event listeners passive. Acknowledge that preventDefault won't be called.
Simon: If your pointer is in a region with only passive listeners, we stay on the fast-path for scrolling.
Simon: Previously this wasn't the case.
Simon: Any region with event handlers followed the slow path before.
Simon: One note is that many sites used event handlers to track user behavior.
Simon: We had to add code to collect active and passive event handlers.
Simon: We track these regions all the way through to the event regions, letting us stay on the fast path more often.
Simon: This fixes Reddit.
Simon: We now hit the fast path for Reddit.
Simon: A few WIP fixes...
Simon: If the page registers wheel event handlers on the root, make them passive by default (like Blink has been doing).
Simon: We already do this for touch event handlers on iOS.
Simon: This makes Twitter faster.
Simon: Chrome also has a behavior where the first event in a gesture is not prevent-defaulted, none of the events in that sequence will be cancellable.
Simon: This matches Blink.
Simon: The spec is vague regarding this. Hoping now web-compat issues.
Questions & Comments
Nikolas: There is interest in this. I haven't had a look at it, but I liked this nice overview. Very nice.
Alexg: This is one of our goals for the next months. We have issues on mostly embedded devices with scrolling. We are working on it and very interested in any improvements. Awesome talk.