Inside Look at Modern Web Browser Inside Look at Modern Web Browser Inside Look at Modern Web Browser Inside Look at Modern Web Browser Inside Look at Modern Web Browser

An overview of the

While the previous chapters covered the basic processes, threads, and synergies of browsers, focusing on how renderers handle page rendering, the final chapter dives into how browsers handle events on a page.

It’s fun to think about it from the perspective of the browser implementation.

The input goes into the synthesizer

This is the title of the first section. It may not seem obvious at first, but this sentence is the core of this article. To better understand this sentence, it is necessary to explain what input and synthesizer are:

  • Input: not only includes input box input, 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 section 3, the last step of rendering, which is raster drawing on the GPU, can be very efficient if decouple from the browser main thread.

So input into the synthesizer means that in the actual browser running environment, the synthesizer has to respond to the input, which may cause the synthesizer itself to render blocked, causing the page to stall.

“Non-fast” scroll area

As js code can bind event listeners, and there is a preventDefault() API to prevent the event’s native effects such as scrolling, browsers will mark all blocks that create this listener as “non-fast” scroll areas on a page.

Note that the onwheel listener is flagged as long as it is created, not as long as it is called preventDefault(), as the browser has no way of knowing when the business is called, so it has to be preventDefault.

Why is this region called a “non-fast”? Because when the event is triggered in this area, the synthesizer must communicate with the renderer to execute the JS event listener code and get user instructions, such as whether preventDefault() is called to prevent scrolling? If it is blocked, it will stop rolling, if it is not blocked, it will continue rolling, if the final result is not blocked, but the waiting time consumption is huge, in low performance devices such as mobile phones, the rolling delay is even 10 ~ 100ms.

However, this is not due to poor device performance, because scrolling happens on the synthesizer, and even a 500 yuan Android can scroll smoothly if it doesn’t communicate with the rendering process.

Note the event delegate

More interestingly, browsers support an API for event delegation, which delegates events to its parent to listen on.

This should be a very convenient API, but it can be a disaster for browser implementations:

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

If the browser parses the code above, it can only be described as speechless. This means that the entire page must be marked “non-fast” because the code delegates the entire Document! This causes scrolling to be very slow, because scrolling anywhere on the page takes place once between the synthesizer and the rendering process.

So the best thing to do is not to write about it. But another option is to tell the browser that you won’t have preventDefault(), because Chrome does app source statistics and finds that around 80% of event listeners don’t have preventDefault() and are simply doing something else, So the synthesizer should be able to run in parallel with the event processing of the renderer process, so that neither lag nor logic is lost. So a flag for 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});
Copy the code

It won’t be stalling, but preventDefault() will also fail.

Check whether the event can be cancelled

In the case of passive: true, the event is effectively uncancelable, so it is best to make a judgment in the code:

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

However, this simply prevents pointless preventDefault(), and does not prevent scrolling. In this case, it is best to prevent horizontal movement through CSS declarations, because this judgment does not occur in the renderer process, so it does not cause the synthesizer to communicate with the renderer process:

#area {
  touch-action: pan-x;
}
Copy the code

Events to merge

Since events can be triggered more frequently than the browser’s frame rate (120 per second), if the browser insists on responding to every event and every event must be responded to once in JS, it will cause a lot of event blocking, because at 60 FPS, you can only perform 60 event responses per second. So the backlog of events is inevitable.

To solve this problem, browsers combine multiple events into a single JS for events that can cause backlogs, such as scrolling events, leaving only the final state.

If you don’t want to lose the intermediate event, you can use getCoalescedEvents to retrieve the state of each step of the event from the coalescedevents 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.}});Copy the code

Intensive reading

As long as we realize that event listeners have to be run in the render process, and many of the high performance “renders” in modern browsers are actually done on the GPU in the compositing layer, what looks like a convenient event listener will definitely slow down the page flow.

React 17 Touch/Wheel Event Passiveness in React 17 React can directly monitor touch and Wheel events on elements, but actually the framework uses the method of delegation to monitor document (later on the app root node). As a result, the user has no way to determine whether the event is passive or not. If the framework defaults to passive, preventDefault() will fail, otherwise the performance is not preventDefault.

In terms of the conclusion, React still adopts passive mode for several affected events.

const Test = () = > (
  <div// No use, can't stop scrolling, because delegate defaultpassive
    onWheel={event= > event.preventDefault()}
  >
    ...
  </div>
)
Copy the code

While this was true and performance-friendly, it wasn’t a solution that everyone was happy with, so let’s take a look at how Dan thought and what solutions he came up with.

The React 16 event delegate is bound to the document node and the React 17 event delegate is bound to the root node of the App. According to chrome optimizer, the event delegate bound to the Document node is passive by default, while the event delegate bound to other nodes is not. So for React 17, if you do nothing but Change the binding node position, there will be a Break Change.

  1. The first solution is to stick to the spirit of Chrome performance optimization and delegate while still pasive processing. This is at least the same as React 16,preventDefault()Both are invalid, incorrect, but at least not BreakChange.
  2. The second option is to do nothing, which leads to default, rightpassiveIs bound to a non-document nodenon-passiveNot only does this have performance issues, but there is also a BreackChange in the API, although this approach is more “native”.
  3. Touch /wheel no longer uses delegates, which means browsers can have fewer “non-fast” areas, whilepreventDefault()It’s also working.

We chose the first option because we didn’t want inconsistent BreakChange behavior at the React API level for the time being.

React 18, however, is a time for BreakChange, and the jury is still out.

conclusion

Seeing things from the browser’s point of view gives you a god’s point of view, not a developer’s point of view, and you don’t feel like some weird optimization logic is a Hack anymore because you know how the browser understands and implements it behind the scenes.

However, we will also see some frustration with the implementation of strong binding, which inevitably causes headaches when implementing a front-end development framework. After all, as a developer who doesn’t know much about browser implementations, it’s natural to assume that the preventDefault() binding must prevent the default scrolling on the event, but why is it:

  • Browsers are divided into synthesis layer and rendering process. The high communication cost leads to the scrolling event monitoring, which will cause the scrolling lag.
  • To avoid communication, the browser defaults to turning on the Document bindingpassivePolicy to reduce the “non-fast” area.
  • Open thepassiveEvent listening forpreventDefault()Will fail because this layer is implemented in JS and not GPU.
  • React16 uses event proxies to place elementsonWheelProxy to the Document node instead of the current node.
  • Act17 moves the Document node binding down to the App root node, so the browser-optimizedpassiveFailed.
  • React uses the event delegate bound to the App root node by default to keep the API from BreakChangepassiveTo behave as if it were bound to document.

The bottom line is that React and the browser implementation are in dispute, resulting in a failure to prevent scrolling behavior, and this chain of consequences is clearly felt by developers. However, you can understand the React team’s pain. There is no way to describe passive behavior in the EXISTING API, so this is a problem that cannot be solved for now.

The discussion address is: close reading “Deep Understanding modern Browsers iv” · Issue #381 · dT-fe /weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.

Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)