background

Long pages are very common in front-end development. For example, in the e-commerce home page in the figure below, the floor data comes from the configuration of operation personnel in the background. The number of floors is not fixed, and each floor may depend on more page turning data. In this case, if we render the entire page at once, we can imagine that our page straight out efficiency (FMP, FID) would be affected.

For a better user experience, we need to consider rendering the next screen of components as the user scrolls to the next screen.

Design ideas

Suppose the page expects to render n components, each of which triggers requests to other interfaces. In designing such a long page, we will face the following two main problems:

  • How to determine when to render the next screen component?
  • How do you keep components from making repeated requests for data during repeated updates?

Figure 1

First, the timing of rendering the next screen

  1. The initial definition

For the home page example, we saved the story data source in the homeInfo variable, and the actual rendered data in compList. In addition, we need a loading component that is always at the bottom of the floor component.

constHomeInfo = [... floor data];const [compList, setCompList] = useState([]); // Render component data
const bottomDomRef = useRef<HTMLDivElement>(null);
Copy the code
// Floor components
<div>
   {compList.map((homeItem, index) = > (
     <div className="home-floor" key={index}>RenderHomeConfig (homeItem) {renderHomeConfig(homeItem)}</div>
   ))}
</div>

// loading DOM
<div ref={bottomDomRef} className='bottom-loading'>
  <Icon name="loading" />
</div>

// completed DOM
<div className="bottom-completed">
   <p>To the bottom</p>
</div>
Copy the code
  1. Loading Whether the Loading component is in the view

As shown in Figure 1, when the loading component’s position is rolled into the view, and if there are unrendered components, it is time to render the next screen.

To determine whether a component in the view has two ways, one is called call Element. GetBoundingClientRect () method for the boundary of the loading Element information, judge, another kind is called Intersection computes the Observer API.

Method 1: getBoundingClientRect

We need to know the height of the window and the height of the Loading component.

Element.clientHeight The height inside an Element, including the inner margin, but excluding horizontal scroll bars, borders, and margins.

Element.scrollHeight A measure of the height of an Element’s content, including content that is not visible in the view due to overflow.

Element. GetBoundingClientRect () method returns the Element size and its position relative to the viewport.

const scrollRenderHandler = ():void= > {
    constrect = bottomDomRef.current? .getBoundingClientRect();// Top is the position of the loading component
    const top = rect ? rect.top : 0;
    / / the window
    const clientHeight = document.documentElement.clientHeight
                        || document.body.clientHeight;
    if(Top < clientHeight && component not finished rendering) {// Continue rendering
    }
}

 useEffect(() = > {
    document.addEventListener('scroll', scrollRenderHandler);
    return() :void= > {
      document.removeEventListener('scroll', scrollRenderHandler);
    };
  }, [scrollRenderHandler]);
Copy the code
Method 2: Intersection Observer

Use the react-intersection-observer API to determine whether the loading element is in the view.

// Use object destructing, so you don't need to remember the exact order
const { ref, inView, entry } = useInView(options);
// Or array destructing, making it easy to customize the field names
const [ref, inView, entry] = useInView(options);
Copy the code
import { useInView } from 'react-intersection-observer';

const [bottomDomRef, inView] = useInView({
   threshold: 0});const scrollRenderHandler = ():void= > {
    if(inView && component not finished rendering) {// Continue rendering}}Copy the code
  1. Whether the component is rendered

Suppose a screen shows 3 components, similar to the common paging logic of pageSize = 3, we can split n components into 3 groups, render each group in turn, and use compGroups to save the split groups. The groupIdx pointer is also used to point to the next group sequence to render.

export const splitGroups = (homeList: any[], pageSize: number): any[] => {
  const groupsTemp = [];
  for (let i = 0; i < homeList.length; i += pageSize) {
    groupsTemp.push(homeList.slice(i, i + pageSize));
  }
  return groupsTemp;
};

const compGroups = useMemo(() = > splitGroups(homeInfo, 3), [homeInfo]);
const groupCount = compGroups.length;
const [groupIdx, setGroupIdx] = useState(0);
Copy the code

When groupIdx is smaller than groupCount, update compList and groupIdx.

if (top < clientHeight && groupIdx < compGroups.length) {
    setCompList(compList.concat(compGroups[groupIdx]));
    setGroupIdx(groupIdx + 1);

 }
Copy the code
  1. Monitor rolling optimization

The scrollRenderHandler function is frequently triggered during scrolling, resulting in poor page performance. Throttling is required and the scrollRenderHandler function is cached with useCallback to improve performance.

const [scrollRenderHandler] = useDebounce((): void= > {
    if (inView && groupIdx < groupCount) {
      setCompList(compList.concat(compGroups[groupIdx]));
      setGroupIdx(groupIdx + 1); }},300,
  [compGroups, compList, groupIdx, inView],
 );

 useEffect(() = > {
    document.addEventListener('scroll', scrollRenderHandler);
    return() :void= > {
      document.removeEventListener('scroll', scrollRenderHandler);
    };
 }, [scrollRenderHandler]);

export default function useDebounce<T extends(. args: any[]) = >any> (func: T, delay: number, deps: DependencyList = [],) :T, () = >void] {
  const timer = useRef<number>();
  const cancel = useCallback(() = > {
    if (timer.current) {
      clearTimeout(timer.current); }} []);const run = useCallback((. args) = > {
    cancel();
    timer.current = window.setTimeout(() = >{ func(... args); }, delay); }, deps);return [run as T, cancel];
}
Copy the code

Second, do not initiate data requests repeatedly

  1. The problem analysis

At this point, with the screen scrolling, we have basically completed the requirements for dynamic rendering of components. But there’s another problem: as you scroll, the same data interface is requested multiple times.As shown above, the same floor interface is requested twice. This means that we repeatedly updated the compList data during window scrolling, resulting in the re-rendering of the floor components, while the data requests for each floor component are placed inside the component, which is related to the unique identification UUID of that floor, thus resulting in repeated requests for the data interface.

  1. React.memo

React Top-Level API – React

From the above points, we can avoid the problem of repeated requests as long as the component does not repeat the rendering.

Before introducing React. Memo, we can use PureComponent to perform shallow comparisons to props. We can also use shouldComponentUpdate to perform specific comparisons to reduce component rendering times.

ShouldComponentUpdate (nextProps, nextState) shouldComponentUpdate(nextProps, nextState) In function components, we can use react. memo, which is very simple, as shown below. If areEqual is not passed, a shallow comparison is performed on props. If passed, you need to return the specific comparison result true, false.

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /* return true if passing nextProps to render would return the same result as passing prevProps to render, otherwise return false */
}
export default React.memo(MyComponent, areEqual);
Copy the code

Therefore, we just need to wrap the components in memo in the corresponding floor components and compare their unique identifier UUids.

The code is as follows:

import React, { memo } from 'react'; type GoodsRecommedProps = { ... Other props, goodsQuery: {uuid: '... '}}const GoodsRecommed: React.FC<GoodsRecommedProps> = (props) = >{... }const isEqual = (prevProps: GoodsRecommedProps, nextProps: GoodsRecommedProps): boolean= > {
  if(prevProps.goodsQuery.uuid ! == nextProps.goodsQuery.uuid) {return false;
  }
  return true;
};

export default memo(GoodsRecommed, isEqual);
Copy the code

Finally, there are no duplicate data requests.

conclusion

  • React. Memo is used for performance optimization of component units.
  • UseCallback is used to cache functions based on a callback that relies on caching the first parameter.

  • UseMemo is mostly used for fine-grained performance optimization of a part of a component based on the return value of the first parameter that depends on the cache.

Writing a normal long page is easy if it’s just about completion, but there’s a lot more you can do if you want to optimize.

The resources

  • React Top-Level API – React
  • Element. GetBoundingClientRect () – | MDN Web API interface reference

  • IntersectionObserver API Usage Tutorial – Ruan Yifeng’s weblog

  • React-intersection-Observer

  • UseCallback, useMemo analysis & differences

  • thebuilder/react-intersection-observer

  • React How to render lists with large amounts of data?


Welcome to “Byte front-end ByteFE” resume delivery email “[email protected]