4

Inside look at modern web browser is a series of articles that introduces the principle of browser implementation. There are 4 articles in total. This intensive reading introduces the fourth article.

Overview

The first few chapters introduced the basic processes and threads of the browser, as well as the relationship between them, and focused on how the rendering process handles page drawing. Then the last chapter also goes deep into how the browser handles events in the page. .

The whole article is very interesting to think about from the perspective of browser implementation.

Input into the synthesizer

This is the title of the first section. At first glance, you may not understand what is being said, but this sentence is the core knowledge point of this article. In order to better understand this sentence, we must first explain what input and synthesizer are:

  • Input: Not only includes the input of the input box, in fact, all user operations are input in the eyes of the browser, such as scrolling, clicking, mouse movement, and so on.
  • Synthesizer: As mentioned in the third section, the last step of rendering, this step is to perform rasterization drawing on the GPU. If it is decoupled from the main thread of the browser, the efficiency will be very high.

So the input into the synthesizer means that in the environment where the browser is actually running, the synthesizer has to respond to the input, which may cause the rendering of the synthesizer itself to be blocked and cause the page to freeze.

"non-fast" scrolling area

Since the js code can be bound to event listeners, and there is a preventDefault() API in the event listeners that can prevent the original effects of the event such as scrolling, so in a page, the browser will mark all the blocks that create this listener as "non -fast" scroll area.

Note that as long as the onwheel event listener is created, it will be marked, not when preventDefault() is called, because the browser cannot know when the service is called, so it can only be used across the board.

Why is this area called "non-fast"? Because when an event is triggered in this area, the compositor must communicate with the rendering process so that the rendering process executes the js event monitoring code and obtains user instructions, such as whether preventDefault() is called to prevent scrolling? If it is blocked, the scrolling will be terminated. If it is not blocked, the scrolling will continue. If the final result is not blocked, the waiting time consumption is huge. On low-performance devices such as mobile phones, the scrolling delay can even be 10-100ms.

However, this is not caused by poor device performance, because scrolling occurs in the synthesizer. If it can not communicate with the rendering process, then even a 500 yuan Android machine can scroll smoothly.

Attention event delegation

What's more interesting is that the browser supports an event delegation API, which can delegate events to its parent node and monitor it together.

This is a very convenient API, but it may be a disaster for browser implementation:

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault();
  }
});

If the browser parses the above code, it can only be described silently. Because this means that all pages must be marked "non-fast", because the code entrusts the entire document! This can cause scrolling to be very slow, because there is a communication between the compositor and the rendering process when scrolling anywhere on the page.

So the best way is not to write this kind of monitoring. But another solution is to tell the browser that you don’t preventDefault() . This is because Chrome found through statistics of the application source code that about 80% of the event monitoring does not have preventDefault() , but just doing other things, so the synthesizer should be able to interact with The event processing of the rendering process is carried out in parallel, so that neither stalls nor logic is lost. passive: true is added to indicate that the current event can be processed in parallel:

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault()
  }
 }, {passive: true});

This will not preventDefault() , but 061bfeabcac405 will also fail.

Check if the event can be cancelled

In passive: true , the event actually becomes irrevocable, so we'd better make a layer of judgment in the code:

document.body.addEventListener('touchstart', event => {
  if (event.cancelable && event.target === area) {
    event.preventDefault()
  }
 }, {passive: true});

However, this only prevents the execution of preventDefault() , which is meaningless, and does not prevent scrolling. In this case, the best way is to prevent lateral movement through css declarations, because this judgment will not occur in the rendering process, so it will not cause communication between the compositor and the rendering process:

#area {
  touch-action: pan-x;
}

Event merge

Since the event trigger frequency may be higher than the browser frame rate (120 times per second), if the browser insists on responding to each event, and an event must be responded to once in js, it will cause a large number of events to be blocked. Because when the FPS is 60, only 60 event responses can be performed in one second, so the event backlog is unavoidable.

In order to solve this problem, the browser merges multiple events into one js in response to events that may cause a backlog, such as scrolling events, and only retains the final state.

If you don’t want to lose the intermediate process of the event, you can use getCoalescedEvents to retrieve the status of each step of the event from the merged event:

window.addEventListener('pointermove', event => {
  const events = event.getCoalescedEvents();
  for (let event of events) {
    const x = event.pageX;
    const y = event.pageY;
    // draw a line using x and y coordinates.
  }
});

intensive reading

As long as we realize that event monitoring must run in the rendering process, and many high-performance "rendering" of modern browsers are actually done in the compositing layer using GPU, the seemingly convenient event monitoring will definitely slow down the smoothness of the page.

But this matter was discussed once in React 17 Touch/Wheel Event Passiveness in React 17 (In fact, this issue is still under discussion in the 18 161bfeabcacdb0 React 18 not passive wheel / touch event listeners support ) , Because React can directly monitor the Touch and Wheel events on the element, but in fact the framework uses a delegated method to monitor the document (later in the root node of the app) uniformly, which results in the user having no way of determining whether the event is passive , if the framework defaults passive will cause preventDefault() fail, otherwise the performance will not be optimized.

As far as the conclusion is concerned, React still adopts the passive touchstart touchmove wheel , namely:

const Test = () => (
  <div
    // 没有用的,无法阻止滚动,因为委托处默认 passive
    onWheel={event => event.preventDefault()}
  >
    ...
  </div>
)

Although the conclusion is so and performance-friendly, it is not a solution that satisfies everyone. Let's take a look at how Dan thought at the time and what solutions he gave.

First of all, the background is that the React 16 event delegate is bound to the document, and the React 17 event delegate is bound to the App root node. According to the optimization of chrome, the event delegate bound to the document defaults to passive , while other nodes will not , So for React 17, if you do nothing and only change the position of the binding node, there will be a Break Change.

  1. The first solution is to adhere to the spirit of Chrome performance optimization, and still pasive processing when commissioned. This processing is at least the same as React 16, preventDefault() is invalid, although incorrect, but at least not BreakChange.
  2. The second solution is to do nothing, which causes the original default passive to be bound to a non-document node instead of non-passive . This not only has performance problems, but also the API will have BreakChange, although this approach is more "native".
  3. Touch/wheel no longer uses delegation, which means that the browser can have less "non-fast" area, and preventDefault() can also take effect.

In the end, I chose the first solution because I don’t want the inconsistent BreakChange at the React API level for the time being.

However, React 18 is an opportunity for BreakChange, and there is no further conclusion yet.

Summarize

Looking at the problem from the browser perspective will give you a God's perspective rather than a developer's perspective. You will no longer think that some weird optimization logic is a Hack, because you understand how the browser is understood and implemented behind it.

However, we will also see some helplessness with the implementation of strong binding, which caused inevitable troubles in the implementation of the front-end development framework. After all, as a developer who doesn’t understand browser implementation, he naturally thinks that when preventDefault() bound to a scroll event, it can definitely prevent the default scrolling behavior, but why is it because:

  • The browser is divided into a composition layer and a rendering process. The high communication cost causes scrolling event monitoring to cause scrolling freezes.
  • In order to avoid communication, the browser defaults to document binding to enable the passive strategy to reduce the "non-fast" area.
  • The event listener preventDefault() passive is turned on will fail, because this layer is implemented in js instead of GPU.
  • React16 uses event proxy to proxy the element onWheel to the document node instead of the current node.
  • React17 moved the document node binding down to the App root node, so the browser optimized passive failed.
  • React To keep the API BreakChange not happen, so the event App root bound delegate default make up passive , making it appear to bind in the same document.

In short, the dispute between React and the browser implementation led to the failure of the scrolling behavior prevention, and this chain of results was transmitted to the developer, and there was obvious perception. But after understanding the reasons behind, you should be able to understand the pain of the React team, because there is really no way to describe passive the existing API, so this is a temporarily unsolvable problem.

The discussion address is: "Intensive Understanding of Modern Browser IV" · Issue #381 · dt-fe/weekly

If you want to participate in the discussion, please click here , a new theme every week, weekend or Monday. Front-end intensive reading-to help you filter reliable content.

Follow front-end intensive reading WeChat public

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Freely reprinted-non-commercial-non-derivative-keep the signature ( Creative Commons 3.0 License )

黄子毅
7k 声望9.5k 粉丝