Web scrolling: ABC book

Original author: Nolan Lawson
  • Transfer
Posted by Nolan Lawson, Microsoft Edge Project Manager

Scrolling is one of the oldest interactions on the web. Long before the advent of pull-to-refresh methods and endless download lists, a modest scroll bar solved the original problem of scaling on the web: how to interact with content that extends beyond the available viewing area?

Today, scrolling is still the most fundamental interaction on the Web, and perhaps the most misunderstood. For example, do you know the difference between the following scenarios?

  • User scrolls the page with two fingers on the touchpad.
  • User scrolls with one finger on the touchscreen.
  • User scrolls the mouse wheel.
  • The user clicks on the scroll bar and drags it down and up
  • The user presses the up, down, PageUp, PageDown and spacebar arrows on the keyboard

If you ask the average Internet user (or even the average web developer!), They can tell you that these actions are equivalent. The truth is much more interesting.

As it turns out, these five input methods have very different characteristics, especially in terms of performance and cross-browser compatibility. Some of them (like scrolling through the touchscreen) will probably be smooth even on the page using heavy JavaScript, while from others (like scrolling from the keyboard) the same page will lag and become unresponsive. Moreover, some types of scrolling can be slowed down by DOM event handlers, while others cannot. What's going on here?

To answer this question and understand how to implement the most smooth scrolling for your site, let's step back to understand and understand how browsers deal with multithreading and input.

Multithreaded web


Conceptually, the web is a single-threaded environment. JavaScript blocks the DOM, and the DOM blocks JavaScript because both fight for the same thread, often called the "main thread" or "UI thread."

For example, if you add this (terrible) JavaScript snippet to the page, you will immediately notice a performance degradation:

setInterval(() => {
  var start = Date.now();
  while (Date.now() - start < 500) {/* wheeeee! */}
}, 1000);

While this JavaScript is spinning in an endless loop, the buttons do not work, form elements do not respond, and even animated GIFs slow down - in all senses and respects, the page freezes. You can study the effect in action in a simple demo .



Moreover, if you try to scroll the page with the up and down keys on your keyboard, the page will predictably get stuck until JavaScript stops executing. All of this is clear evidence of our view of the web as a single-threaded environment.

There is a funny anomaly: if you try scrolling through the touchscreen, the page scrolls up and down perfectly, although JavaScript blocks everything else on the page. The same applies to scrolling from the touchpad, mouse wheel, and scrolling after the page is captured with a click-and-drag cursor (depending on the browser).

Somehow, some scrolling actions can change the state of the page, while everything else - buttons, data entry fields, GIFs - completely freezes. How can we combine this with our single-threaded web theory?



History of two threads


As it turns out, in general, the thesis “browsers are single-threaded” is true, but there are important exceptions. Scrolling, in all its diversity, is one such exception.

Over the years, browser developers have realized that unloading auxiliary work into background threads can provide significant benefits for smooth operation and sensitivity. Scrolling is so important for key browser experience that this task was quickly chosen for such optimization. Nowadays, all the main browser engines (Blink, EdgeHTML, Gecko, WebKit) support scrolling outside the main thread of execution to one degree or another (Firefox was the last to join the club, from the version of Firefox 46 ).

With background scrolling, even a cluttered page will scroll smoothly, because all scrolling is performed in a separate thread. Only if you try to interact with the page through some extraneous mechanism that is not related to scrolling - press a key, enter data in the input field, click on the link - then the facade is reset and the essence of the salon trick fully reveals itself. (Considering how well it works, this is a great trick!)

True, asynchronous scrolling has a common side effect called the checkerboard effect.(checkerboarding). It first appeared on Safari for iOS in the form of gray and white cells, as if from a chessboard. In most modern browsers, the effect appears as a blank space on the screen if you scroll faster than the browser can render the page. This is not ideal, but it is an acceptable compromise compared to a blocked, twitching or non-responding scroll.



Unfortunately, it is not always easy to transfer scrolling to the background thread of execution. Browsers can only do this if the operating system allows for simultaneous input, and this can vary from device to device. In particular, keyboard input is not as optimized as mouse or touch device input, which ultimately leads to more significant lags when typing from the keyboard in all browsers.

A little story will be instructive here. When operating systems such as Windows and macOS first came out, they allowed only one thread of execution, and few foresaw the need to provide for simultaneous input. Only when multi-core machines appeared did operating systems begin to embed parallelism in their architecture.

Just as the rudimentary organs of animals make their evolutionary history understandable, the single-threaded origin of operating systems manifests itself if you look at the ways of scrolling on the web. Only if the operating system allows parallel input — from a mouse, keyboard, or other device — can browsers effectively optimize scrolling so that it is not affected by the lengthy execution of JavaScript that clutters the main thread of execution.

However, in the Microsoft Edge development team, we are making strides to guarantee smooth and responsive scrolling, regardless of its method. In EdgeHTML 14 (which was included with the Windows 10 Anniversary Update), we support background scrolling for the following methods:

  • One finger touchscreen
  • Two fingers, touchpad
  • Mouse wheel
  • Scroll bar

If you compare Edge with other desktop browsers, you will notice that only it supports asynchronous scrolling using the scroll bar, that is, holding and moving the scroll bar with the mouse, clicking on the scroll bar or arrows. (In fact, without the announcement, we introduced this feature back in the Anniversary Update!)

According to the test results in Windows 10 (14393, Surface Book) and macOS Sierra (10.12, MacBook Air) we got the following results:

Two finger touchpadTouchMouse wheelScroll barKeyboard
Edge 14 (Windows)there isthere isthere isthere isNot
Chrome 56 (Windows)there isthere isthere isNotNot
Firefox 51 (Windows)NotNotNotNotNot
Chrome 56 (MacOS)there isN / athere isNotNot
Firefox 51 (macOS)there isN / athere isNotNot
Safari 10.1 (MacOS)there isN / athere isNotNot

As this table demonstrates * , scroll behavior can dramatically change from browser to browser, and even from one OS to another. If you test one scrolling method in only one browser, you will get very incomplete results of the performance of your site, compared with how users actually work with it!

In general, it should be clear that scrolling has a special place on the web and browsers work very hard to make it fast and receptive. However, there are subtle ways that a web developer can inadvertently disable browser-based optimizations. Let's look at how web developers can influence browser scrolling in a good and bad way.



How listening processes interfere with scrolling


Background scrolling gives a tangible increase in efficiency - scrolling and JavaScript are completely separate, allowing them to work in parallel without interfering with each other.

But everyone who designed the web pages a bit knows how to make the connection between JavaScript and scrolling:

window.addEventListener(“wheel”, function (e) {
  e.preventDefault(); // oh no you don’t!
});

When we add the wheel listening process , which calls event.preventDefault(), it will 100% block scrolling for both the mouse wheel and the touchpad. And obviously, if scrolling is blocked, then background scrolling is also blocked.

Less obvious is the effect of such an example:

window.addEventListener(“wheel”, function (e) {
  console.log(‘wheel!’);
  // innocent listener, not calling preventDefault()
});

You might naively think that if a function does not call preventDefault(), then it cannot block scrolling at all or, in the worst case, block it only for the duration of the function itself. However, the truth is that even an empty listening process completely blocks scrolling until all the JavaScript processes on this page are finished , which you can check in this demo .

Listening to the mouse wheel does not interact with our large blocking JavaScript operation, but they have a common event loop, so the background thread must wait for the longer JavaScript operation to complete before it receives a response from the event listener.

Why should he wait? Well, JavaScript is a dynamic programming language, and the browser cannot know for sure that it preventDefault()will never be called. Even if it’s obvious to the developer that the function is simply recording console.log(), browser developers prefer not to leave a chance. In fact, even empty function() {}will cause the same effect.

Please note that this applies not only to the mouse wheel: on touch devices, scrolling can also be blocked by touchstart or touchmove listening processes .

You have to be careful when adding listener events to the page because they affect performance!

There are several scrolling javascript APIs, however they do not block scrolling. The scroll event , although this is somewhat illogical, cannot block scrolling, because it fires after scrolling, and therefore is not canceled. Also, the new Pointer Events API , introduced in IE and Microsoft Edge, and recently introduced in Chrome and Firefox, was originally designed to avoid inadvertently blocking scrolling.

Even when we really need to listen to wheel or touchstart eventsThere are certain tricks to how web developers can guarantee that scroll operation works at maximum quality. Let's look at some of these tricks.



Global and local listening processes


In the previous example, we saw a case of a global listening process (that is, attached to a window or document ). But what about listening processes for individual scroll elements?

In other words, imagine a page for which scrolling works, but the page has a separate area with its own independent scrolling. Does the browser block scrolling for the entire page if you add a listening process only in this area?

document.getElementById(‘scrollableDiv’)
.addEventListener(“wheel”, function (e) {
  // In theory, I can only block scrolling on the div itself!
});

If you check on a simple demo page , you will notice that Microsoft Edge and Safari will leave smooth scrolling for the whole document if the listening process for scrolling is in a div with independent scrolling.

Here is a table of browsers and their behavior:

Two finger touchpadTouchMouse wheelClick-and-dragKeyboard
Desktop Edge 14 (Windows)there isthere isthere isthere isNot
Desktop Chrome 56 (Windows)Notthere isNotNotNot
Desktop Firefox 51 (Windows)NotNotNotNotNot
Desktop Chrome 56 (MacOS)NotN / aNotNotNot
Desktop Firefox 51 (MacOS)there isN / athere isNotNot
Safari 10.1 (MacOS)there isN / athere isNotNot

The results show * that there are optimizations available for web developers to benefit from these browser features. Instead of using wheel / touch listening processes for the entire document, it is preferable to add listening processes to a specific subsection of the document so that the scroll remains smooth for all other parts of the page. In other words, instead of delegating the wheel / touchstart listening processes to the highest level possible, it is best to isolate them for the element where it is needed.

Unfortunately, not all JavaScript frameworks allow this practice - in particular, React tends to add a global listening process to the entire document, even if it should only apply to part of the page. However there isAn open ticket specifically for this problem, and the guys from React said they would be happy to accept the pull request. (Respect for React guys who reacted so quickly to our proposal )



Passive listening process


Avoiding wheel / touchstart global listening processes is good practice, but sometimes it’s just not possible, depending on the effect you are trying to achieve. And in some ways it looks silly that just listening to events makes the browser stop the whole world, simply because there is a hypothetical probability of a call PreventDefault(), and it is waiting for it.

Fortunately, a new feature has begun to appear in browsers when web developers can explicitly mark the listening process as “passive” and therefore avoid waiting:

window.addEventListener(“wheel”, function (e) {
  // Not calling preventDefault()!
}, { passive: true } // I pinkie-swear I won't call preventDefault()
);

With this approach, the browser will handle scrolling as if the wheel listening process were completely absent. This feature is already available in the latest versions of Chrome, Firefox, and Safari, and should be available soon in a future release of Microsoft Edge . (Note that you need to use feature detection to support browsers that do not recognize passive listening processes).

For some events (including touchstart and touchmove ), Chrome from version 56 decided to intervene and made them passive by default . Keep in mind this slight difference between browsers when adding listening processes!



Conclusion


As we have seen, scrolling on the web is a fantastically complex process, and all browsers are at different stages of improving their performance. But overall, we can provide some clear advice for web developers.

First, it’s best not to add the wheel or touch listener processes to the global document or window objects , but instead add them to smaller elements with individual scrolling. Developers should also use passive listening processes, where possible, using feature detection to avoid compatibility issues. Using Pointer Events (there is a polyfill there ) and listening events scrollIs also a surefire way to avoid inadvertently scrolling locks.

Hopefully this article has provided some useful tips for web developers and allowed a quick glimpse of what browsers have under the hood. Without a doubt, as browsers evolve and the web grows, the scroll mechanics will become even more complex and sophisticated.

Our Microsoft Edge team will continue to innovate in this area to provide seamless scrolling for more sites and users. Let's say this for a modest scrollbar - the oldest and most controversial interaction on the web!

* The results were obtained on the latest version of each browser in February 2017. Since then, Firefox 52 has updated support for scrolling, and now matches the behavior of Edge 14 in all tests, except for scrolling with the scroll bar. We hope that other browsers will also make improvements in the implementation of scrolling and make the web faster and more susceptible!

Also popular now: