1 the introduction

BI platform is a very important platform-level product of Ali Data Center team. To ensure a good experience of report editing and browsing, performance optimization is essential.

Current BI tool is generally report forms, to know the report form is not just the chart component, filter condition associated with these components and the complexity of relationship any filter condition can lead to changes in their related items to fetch and rendering component, is a large amount of data and statements, a form component loading millions of magnitude of the data, Rendering on demand is a must in order to maintain a normal display with such a large amount of data.

Here not ListView infinite scroll, on-demand rendering model because the layout of the report have free streaming layout, the layout of magnet and three sets of layout, each layout style difference is very big, can’t use a fixed formula to calculate the component is visible, so we choose to initialize components full volume rendering, prevent components within the first screen rendering. Since the data has not been obtained under the initial conditions, full rendering will not cause performance problems, which is the premise of the establishment of this scheme.

So TODAY I will introduce how to use DOM to determine whether components are visible in the canvas of this technical solution, from the point of view of architecture design and code abstraction, step by step decomposition, not only hope you can easily understand how to achieve this technical solution, but also hope you can master the tricks of this, learn to draw infertile examples.

2. Intensive reading

Using the React framework as an example, the thinking path for rendering on demand is as follows:

Get component active state -> block rerendering of non-active components.

Here I choose to start with the results, thinking first about how to block component rendering, and then working out how to write the function that determines whether the component is visible.

Block component rerendering

We need a RenderWhenActive component that supports an active parameter. This layer is transparent when Active is true and blocks all renderings when active is false.

To be more specific, the effect looks like this:

  1. In inActive, any changes to props will not cause the component to render.
  2. When switching from inActive to Active, the props previously applied to the component takes effect immediately.
  3. If the switch to Active does not change, the re-render should not be triggered.
  4. Switching from Active to inActive should not trigger rendering and immediately blocks subsequent rerenders.

Function Component can’t do this right now. We still need to use the Class Component shouldComponentUpdate to do this, because the Class Component blocks rendering. The latest props is stored, and the Function Component has no internal state at all and can’t do this yet.

We can easily do this by writing a RenderWhenActive component:

class RenderWhenActive extends React.Component {
  public shouldComponentUpdate(nextProps) {
    return nextProps.active;
  }

 public render() {  return this.props.children  } } Copy the code

Gets the active status of components

Before we think further, let’s not get down to the details of how to tell if a component is displayed, but how to call it, assuming that we already have such a function.

We obviously need a custom Hook: useActive determines whether the component is active and passes the value returned by active to RenderWhenActive components:

const ComponentLoader = ({ children }) = > {
  const active = useActive();

  return <RenderWhenActive active={active}>{children}</RenderWhenActive>;
};
Copy the code

In this way, any component rendered by the rendering engine with ComponentLoader is capable of rendering on demand.

Implement useActive

Now that the component and Hook side flow is fully connected, we can focus on how to implement the useActive Hook.

Using the Hooks API, you can use the useEffect to determine if the component is Active after rendering and use the useState to store this state:

export function useActive(domId: string) {
  // All elements default to unActive
  const [active, setActive] = React.useState(false);

  React.useEffect((a)= > {
 const visibleObserve = new VisibleObserve(domId, "rootId", setActive);   visibleObserve.observe();   return (a)= > visibleObserve.unobserve();  }, [domId]);   return active; } Copy the code

The active state of all components is false when initialized, however this state in shouldComponentUpdate does not block the first rendering, so the component’s DOM node initialization is still rendered.

The custom Class VisibleObserve is registered in the useEffect phase. It is used to listen to whether the component DOM node is visible in its parent node rootId and is thrown by a third callback when the status changes. Here setActive is the third parameter. You can change the active status of the current component in time.

VisibleObserve this function has the observe and unobserve apis to start and cancel the listen, respectively. Using the useEffect destruction function to perform a return callback, the listening and destruction mechanism is complete.

The next step is to implement the core VisibleObserve function that listens for components to be visible.

Preparations to listen for components to be visible

Before you implement VisibleObserve, how many ways can you implement it? You probably have a lot of weird scenarios in mind. Yes, there are many ways to determine whether a component is visible in a container, and even if the best solution is found in terms of functionality, there is no perfect solution from a compatibility point of view, so this is a function with many implementation possibilities, and different solutions for different browser versions is the best strategy.

One way to handle this is to make an abstract class that all the actual methods inherit and implement, so that we have multiple sets of “different implementations of the same API” that can be switched between different scenarios.

Use the abstract create an abstract class AVisibleObserve, realize two public constructors and affirms the important function to observe and unobserve:

/ * ** An abstract class that listens for elements to be visible* /
abstract class AVisibleObserve {
  / * ** Listen for the DOM ID of the element* /  protected targetDomId: string;   / * ** Visible range root node DOM ID* /  protected rootDomId: string;   / * ** Active change callback* /  protected onActiveChange: (active? : boolean) = > void;   constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { this.targetDomId = targetDomId;  this.rootDomId = rootDomId;  this.onActiveChange = onActiveChange;  }   / * ** Start listening* /  abstract observe(): void;   / * ** Cancel listening* /  abstract unobserve(): void; } Copy the code

So we can implement multiple solutions. After a bit of thinking, we only need two solutions. One is a stupid method of polling detection implemented by setInterval, and the other is a new method implemented by advanced API IntersectionObserver of the browser. Since the latter has compatibility requirements, the former is implemented as a bottom-all solution.

Therefore, we can define two sets of corresponding methods:

class IntersectionVisibleObserve extends AVisibleObserve {
  constructor(/ * * /) {    super(targetDomId, rootDomId, onActiveChange);
  }

 observe() {  // balabala..  }   unobserve() {  // balabala..  } }  class SetIntervalVisibleObserve extends AVisibleObserve {  constructor(/ * * /) { super(targetDomId, rootDomId, onActiveChange);  }   observe() {  // balabala..  }   unobserve() {  // balabala..  } } Copy the code

Finally, make a general class as a call entry:

/ * *Listen to whether the element is visible to the general class* /
export class VisibleObserve extends AVisibleObserve {
  / * ** Actual VisibleObserve class* /  private actualVisibleObserve: AVisibleObserve = null;   constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { super(targetDomId, rootDomId, onActiveChange);   // Select different Observe solutions according to browser API compatibility  if ('IntersectionObserver' in window) {  // The latest IntersectionObserve scheme  this.actualVisibleObserve = new IntersectionVisibleObserve(targetDomId, rootDomId, onActiveChange);  } else {  // Compatible SetInterval scheme  this.actualVisibleObserve = new SetIntervalVisibleObserve(targetDomId, rootDomId, onActiveChange);  }  }   observe() {  this.actualVisibleObserve.observe();  }   unobserve() {  this.actualVisibleObserve.unobserve();  } } Copy the code

In the constructor, we can determine whether the current browser supports IntersectionObserver. However, the instance created in any scheme inherits from AVisibleObserve, so we can store it with the uniform actualVisibleObserve member variable.

The observe and unobserve phases can override the implementation of a concrete class, Direct call this. ActualVisibleObserve. Observe () and enclosing actualVisibleObserve. Unobserve () the two apis.

The idea here is that the parent class cares about the interface layer API, and the subclass cares about how the API is implemented based on that interface.

Let’s take a look at how low specs (compatible) and high specs (native) are implemented.

Listening component visible – Compatible version

In compatible versions, you need to define an additional member variable called interval to store the SetInterval reference, which is unobserved when clearInterval is used.

The judging visible function is abstracted into the judgeActive function. The core idea is to judge whether there is an inclusion relation between two rectangles (container and component to be judged). If the inclusion is true, it represents visible; if the inclusion is not true, it is not visible.

Here is the full implementation function:

class SetIntervalVisibleObserve extends AVisibleObserve {
  / * ** the Interval reference* /
  private interval: number;
  / * ** Check whether the interval is visible* /  private checkInterval = 1000;   constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { super(targetDomId, rootDomId, onActiveChange);  }   / * ** Determines whether the element is visible* /  private judgeActive() {  // Get rect of root component  const rootComponentDom = document.getElementById(this.rootDomId);  if(! rootComponentDom) { return;  }  // Root component rect  const rootComponentRect = rootComponentDom.getBoundingClientRect();  // Get the current component recT  const componentDom = document.getElementById(this.targetDomId);  if(! componentDom) { return;  }  // The current component rect  const componentRect = componentDom.getBoundingClientRect();   // Determine whether the current component is visible to the root component  // Sum of the lengths  const sumOfWidth =  Math.abs(rootComponentRect.left - rootComponentRect.right) + Math.abs(componentRect.left - componentRect.right);  // Add the width  const sumOfHeight =  Math.abs(rootComponentRect.bottom - rootComponentRect.top) + Math.abs(componentRect.bottom - componentRect.top);   // Sum of lengths + double spacing (cross spacing is negative)  const sumOfWidthWithGap = Math.abs(  rootComponentRect.left + rootComponentRect.right - componentRect.left - componentRect.right,  );  // Total width + double spacing (cross spacing is negative)  const sumOfHeightWithGap = Math.abs(  rootComponentRect.bottom + rootComponentRect.top - componentRect.bottom - componentRect.top,  );  if (sumOfWidthWithGap <= sumOfWidth && sumOfHeightWithGap <= sumOfHeight) {  / / in the interior  this.onActiveChange(true);  } else {  / / in the external  this.onActiveChange(false);  }  }   observe() {  // Check if the element is visible once when listening  this.judgeActive();   this.interval = setInterval(this.judgeActive, this.checkInterval);  }   unobserve() {  clearInterval(this.interval);  } } Copy the code

Based on the container rootDomId and component targetDomId, we can get the corresponding DOM instance and call getBoundingClientRect to get the position and width of the corresponding rectangle.

The algorithm idea is as follows:

Make the container root and the component Component.

  1. Calculates the sum of root and Component lengthssumOfWidthSum of and widthsumOfHeight.
  2. Calculate the sum of root and Component lengths + double the spacingsumOfWidthWithGapPlus the sum of the widths plus twice the spacingsumOfHeightWithGap.
  3. sumOfWidthWithGap - sumOfWidthIs the transverse gap distance,sumOfHeightWithGap - sumOfHeightThe difference of is the transverse gap distance, and both values are negative in the interior.

The key is that, from a horizontal perspective, the following formula can be understood as the sum of the widths + twice the width spacing:

// Sum of lengths + double spacing (cross spacing is negative)
const sumOfWidthWithGap = Math.abs(
  rootComponentRect.left +
    rootComponentRect.right -
    componentRect.left -
 componentRect.right ); Copy the code

While sumOfWidth is the sum of the widths, the difference between them is the double spacing value, and a positive number means there is no intersection horizontally. When the intersection of the vertical and horizontal is negative, it is either crossed or contained internally.

Whether the listening component is visible – native version

It would be easier if the browser supports IntersectionObserver. Here is the complete code:

class IntersectionVisibleObserve extends AVisibleObserve {
  / * ** IntersectionObserver instance* /
  private intersectionObserver: IntersectionObserver;
  constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { super(targetDomId, rootDomId, onActiveChange);   this.intersectionObserver = new IntersectionObserver(  changes= > {  if (changes[0].intersectionRatio > 0) {  onActiveChange(true);  } else {  onActiveChange(false);   // Because virtual DOM updates lead to actual DOM updates, it is also triggered here. If dom is lost, listen again  if (!document.body.contains(changes[0].target)) {  this.intersectionObserver.unobserve(changes[0].target);  this.intersectionObserver.observe(document.getElementById(this.targetDomId));  }  }  },  {  root: document.getElementById(rootDomId),  },  );  }   observe() {  if (document.getElementById(this.targetDomId)) {  this.intersectionObserver.observe(document.getElementById(this.targetDomId));  }  }   unobserve() {  this.intersectionObserver.disconnect();  } } Copy the code

IntersectionRatio > 0 can be used to determine whether the element appears in the parent container. If intersectionRatio === 1, it indicates that the component appears in the container completely. In this case, it is required that all parts appear active.

One thing to note is that this judgment is different from the SetInterval, virtual DOM DOM instance may be updated, due to the React to IntersectionObserver. Observe after listening DOM element is destroyed, causing subsequent failure monitoring, So add the following code when the element is hidden:

// Because virtual DOM updates lead to actual DOM updates, it is also triggered here. If dom is lost, listen again
if (!document.body.contains(changes[0].target)) {
  this.intersectionObserver.unobserve(changes[0].target);
  this.intersectionObserver.observe(document.getElementById(this.targetDomId));
}
Copy the code
  1. When an element is judged to be out of the viewable area, it also contains elements that are destroyed.
  2. So bybody.containsDetermines if the element is destroyed, and if so, listens again for a new DOM instance.

3 summary

To sum up, the logic of rendering on demand is not applicable only to rendering engines, but it can be quite intrusive to add to code written directly for ProCode scenes.

Perhaps on-demand rendering in the visual area could be done inside a front-end development framework, not a standard framework feature, but not entirely a business feature either.

This time around, what would be a better design if handwritten React code could render on demand?

React Render on Demand · Issue #254 · 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.

Pay attention to the front end of intensive reading wechat public account

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