⚠️ this article for nuggets community first contract article, not authorized to forbid reprint

Introduction to the

This is the second article in the mall Practices series on optimizing long list renderings for mall projects to improve the efficiency of rendering and optimize display speed.

When we use the e-commerce platform, when we open the home page, we keep sliding down and there will be a steady stream of recommended content to show us. With more and more page browsing and more and more data, this kind of scene is commonly referred to as long list rendering.

In mall projects, the pages of long list rendering are closely related to users, such as order list, coupon list, shopping cart, etc., which are frequently viewed in our daily life. Therefore, the performance efficiency of long list rendering is proportional to the user experience.

When it comes to performance optimization and development of long list pages, most of us run into the following two problems:

  • Too much data, the first display time is too long, the interface returns too much data, and the page data is difficult to process.
  • Too many DOM elements, page rendering is slow, operation is not smooth, browser performance pressure.

How can these problems be solved? I recommend using a paging load + virtual list scenario.

Why use a paging + virtual list scenario?

For your convenience, I’ve put together a mind map with detailed scenarios, problems, and available solutions. Available solutions include page load, slice load, virtual list, and page + virtual list. So why did I choose the pagination + virtual list scenario?

First of all, we will make a review of the problems that can be solved and cannot be solved by each solution. The specific advantages and disadvantages are as follows:

  • Page loading: Solves the data overload problem by paging data to reduce the amount of data and DOM that the first page loads. Is the implementation method used in most applications today. As more page data is viewed and more DOM is created, there are still some problems.

  • Shard loading: The same as paging loading, but the user’s bottom behavior to get the latest data node in the beginning of the slicing loading, display the page data first before loading other data. Page blocking and performance issues can occur.

  • Virtual list: the driver is handed over to the data, and the data DOM in the interval content is directly rendered through the interval, which solves the problem of too many elements in the page list. There is no hook with the data loading.

After listing three common approaches, we found that a single solution could not meet our demands. Therefore, I chose to handle the data loading in paging mode while leaving the rendering of the page to the virtual list to render. By combining the two different focuses of the program, to meet our initial appeal.

Implement virtual lists

Now that we’ve settled on a solution, let’s take a look at what a virtual list is 🥳.

In the diagram below, we divide the overall list into scrolling Windows and visual Windows. The left side is the real list, all list items are real DOM elements, while the virtual list can be seen from the diagram. Only list items that appear in the visual window are real DOM elements, while elements that do not appear in the visual window are virtual data, not loaded on the page.

Unlike the real list, the virtual list scrolling is offset by transform or marginTop, and the list itself only shows DOM elements in the window area.

Next, let’s implement a basic virtual list from 0 to 1.

Basic layout

As shown in the following structure diagram, let’s first analyze the basic page composition:

  • The first layer ofThe container layer, select a fixed height, which is what we call the visualization window
  • The 2nd isContent of the layer, the height is usually opened here to allow the container to formscroll.
  • The third layer forChild content layerIs inside the content layer, which is the list item in the list.
  • .

After analysis, I implemented the code in the structure diagram using JSX, which is the following simple structure:

Page layout code

<div>
  <div>. List Item Element</div>
</div>;

.App {
    font-family: sans-serif;
    text-align: center;
}

.showElement {
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px solid #000;
    margin-bottom: 8px;
    border-radius: 4px;
}
Copy the code

Build a simple page and then render the corresponding list items using currentViewList.

Initialization page

Once we have the basic structure of the page, let’s refine the layout and configuration to achieve a realistic rendering of a list of thousands of pieces of data.

I started by defining some configurations, including the container height, the list item height, the number of preloaded offsets, and other fixed things I needed.

  • Container height: The height of the current virtual list
  • Item height: Height of a list item
  • Preloading offset: can be done up and down the window when the preloading need to show a few additional preparatory content

Page properties

/ * *@name Page container height */

const SCROLL_VIEW_HEIGHT: number = 500;

/ * *@name List item height */

const ITEM_HEIGHT: number = 50;

/ * *@name Number of preloads */

const PRE_LOAD_COUNT: number = SCROLL_VIEW_HEIGHT / ITEM_HEIGHT;
Copy the code

Next, create a useRef to store the elements and get the window height and offset attributes.

/** container Ref */

const containerRef = useRef<HTMLDivElement | null> (null);
Copy the code

Then, create a data source and generate 3000 pieces of random data for display processing.

const [sourceData, setSourceData] = useState<number[] > ([]);/** * Create a list to display data */
const createListData = () = > {
  const initnalList: number[] = Array.from(Array(4000).keys());
  setSourceData(initnalList);
};

useEffect(() = >{ createListData(); } []);Copy the code

Finally, bind the height for the corresponding container. Set the height of the outermost div tag to SCROLL_VIEW_HEIGHT and the height of the list div to sourceData.length * ITEM_HEIGHT.

Gets the overall height of the list

/** * scrollView */
 const scrollViewHeight = useMemo(() = > {
  return sourceData.length * ITEM_HEIGHT;
}, [sourceData]);
Copy the code

Binding page View

<div
  ref={containerRef}
  style={{
    height: SCROLL_VIEW_HEIGHT.overflow: "auto".}}onScroll={onContainerScroll}
>
  <div
    style={{
      width: "100% ",height: scrollViewHeight - scrollViewOffset.marginTop: scrollViewOffset,}} >
    {sourceData.map((e) => (
      <div
        style={{
          height: ITEM_HEIGHT,}}className="showElement"
        key={e}
      >
        Current Position: {e}
      </div>
    ))}
  </div>
</div>;
Copy the code

When the data is initialized, our list page is preliminarily complete, so take a look at the effect.

Content to intercept

For virtual lists, there is no need to fully render the data on the page. So, this is where we start to do data interception.

First, as shown in the figure below, we use showRange to control the number of elements displayed on the page. Slice is used to intercept sourceData, and the return value is the list of data that we display on the page. Instead of directly traversing souceData in the above code, I’ll replace it with our new list of data. As follows:

{currentViewList.map((e) = > (
  <div
    style={{
      height: ITEM_HEIGHT
    }}
    className="showElement"
    key={e.data}
  >
    Current Position: {e.data}
  </div>
))}
Copy the code

The currentViewList used above is a useMemo return value that changes as showRange and sourceData are updated.

/** * Current scrollView display list */
 const currentViewList = useMemo(() = > {
  return sourceData.slice(showRange.start, showRange.end).map((el, index) = > ({
    data: el,
    index,
  }));
}, [showRange, sourceData]);
Copy the code

Scroll to calculate

Now that we have a basic prototype of a virtual list, we need to listen for window scroll events to calculate the start and end offsets in showRange, and adjust the corresponding scroll bar progress to achieve a real list effect.

First, I bind the onScroll event to the scrollContainer, which is the onContainerScroll function method below.

/** * onScroll event callback *@param Event {UIEvent<HTMLDivElement>} ScrollView scrolling parameter */
 const onContainerScroll = (event: UIEvent<HTMLDivElement>) = > {
  event.preventDefault();
  calculateRange();
};
Copy the code

The main thing to do in the event is to calculate the current start and end positions in the showRange and update the page view data. Let’s see how it works.

First, by containerRef. Current. ScrollTop elements can know the height of the scroll bar at the top of the list of hidden inside, and then use Math. Take down the whole floor method, in order to get the current offset the number of elements, By subtracting the amount of context preloaded at the beginning, PRE_LOAD_COUNT, you can derive the location where the interception begins.

Second, by containerRef. Current. ClientHeight can get the height of the rolling window, Then through containerRef. Current. ClientHeight/ITEM_HEIGHT this formula can be concluded that the current container window can accommodate a few list items.

When I have calculated the start position of the interception window from the number of previously scrolled elements under the current scrollbar position, I can calculate the end position of the current interception window by the formula of start position + number of containers displayed + number of preloads. After updating the new positional subscript with the setShowPageRange method, as I slide the window up and down, the displayed data will be rendered on the page as different data cuts based on the showRange.

/** * Calculate the element range */
 const calculateRange = () = > {
  const element = containerRef.current;
  if (element) {
    const offset: number = Math.floor(element.scrollTop / ITEM_HEIGHT) + 1;
    console.log(offset, "offset");
    const viewItemSize: number = Math.ceil(element.clientHeight / ITEM_HEIGHT);
    const startSize: number = offset - PRE_LOAD_COUNT;
    const endSize: number = viewItemSize + offset + PRE_LOAD_COUNT;
    setShowPageRange({
      start: startSize < 0 ? 0 : startSize,
      end: endSize > sourceData.length ? sourceData.length : endSize, }); }};Copy the code

Scrollbar migration

Mentioned above, we can according to containerRef. Current. ScrollTop calculating the height of the scroll. So the question is, how do you push up the height when there are no real elements on the page?

Currently, the most popular solutions are MarinTop and TranForm to offset from the top to achieve height spread.

  • Margin is a layout property, and changes to this property will cause the page to be rearranged
  • Transform is a composition property. The browser creates a separate composition layer for the element. When the content of the element does not change, the layer is not redrawn.

There is not much difference between the two schemes, and both can be used to offset the top position to the extent that the actual height of the list is pushed out.

Next, I’ll tackle the problem in MarinTop’s way to complete the current virtual list.

First, we need to figure out how far the list page is from the MarginTop at the top. Using the formula: The start of the current virtual list * the height of the list items, we can calculate the current scrollTop distance.

Use useMemo to cache the logic with the dependency showrange.start and update the marginTop height calculation when showrange.start changes.

/** * scrollView offset */
 const scrollViewOffset = useMemo(() = > {
  console.log(showRange.start, "showRange.start");
  return showRange.start * ITEM_HEIGHT;
}, [showRange.start]);
Copy the code

On the page, bind the marginTop: scrollViewOffset property to the list window, and subtract scrollViewOffset from the total height to maintain balance and avoid extra distance to the white background.

The following code

<div
    style={{
        width: "100% ",height: scrollViewHeight - scrollViewOffset.marginTop: scrollViewOffset
    }}
>
Copy the code

Now that we have a basic virtual list, let’s see what it looks like in action.

Combined with paging loading

Once we have a virtual list, we can try to implement a long, lazily loaded virtual list in combination with paging loading.

If you do too much page scrolling loading friends may immediately think of the idea of implementation, do not understand the students do not worry, the following I will bring you to achieve a virtual list with paging loading, I believe you will have a deeper understanding of this kind of problem after reading.

Determine whether the bottom

To enable paging loading of the list, we need to bind the onScroll event to determine whether the current scroll window has reached the bottom, and then add data to the sourceData. It will also move the pointer to point the data to the next starting point.

The reachScrollBottom function returns the value of whether the current scrollBottom window has reached the bottom. Therefore, we are conditioned by the return value of the function. Once at the bottom, we simulate a batch of data and set the source data with setSourceData. The interval of content interception is reset after the execution of calculateRange.

/** * onScroll event callback *@param Event {UIEvent<HTMLDivElement>} ScrollView scrolling parameter */
 const onContainerScroll = (event: UIEvent<HTMLDivElement>) = > {
  event.preventDefault();
  if (reachScrollBottom()) {
    // Simulate data addition, which is actually await asynchronous request as data addition
    let endIndex = showRange.end;
    let pushData: number[] = [];
    for (let index = 0; index < 20; index++) {
      pushData.push(endIndex++);
    }
    setSourceData((arr) = > {
      return [...arr, ...pushData];
    });
  }
  calculateRange();
};
Copy the code

So how does calculatScrollTop determine if the current bottom has been reached?

Looking at the figure above, I can either use containerRef to get the scrollHeight or souredata. length * ITEM_HEIGHT to get the height of the scrollwindow.

At the same time, I can also get the height of scrollTop from the top and the current window height of clientHeight. According to the relationship among the three, the conditional formula can be obtained: scrollTop + clientHeight >= scrollHeight. If this condition is met, it means that the current window has reached the bottom. Let’s write this as the reachScrollBottom method, as follows:

/** * calculate whether the current bottom *@returns Whether to reach the bottom */
 const reachScrollBottom = (): boolean= > {
  // The scroll bar is at the top
  constcontentScrollTop = containerRef.current? .scrollTop ||0; 
  // Visual area
  constclientHeight = containerRef.current? .clientHeight ||0; 
  // The total height of the scroll bar contents
  constscrollHeight = containerRef.current? .scrollHeight ||0;
  if (contentScrollTop + clientHeight >= scrollHeight) {
    return true;
  }
  return false;
};
Copy the code

Infinite List demo

So far, our virtual list implementation has been basically completed, let’s take a look at the effect, here first a simple simulation of a list of goods as a demonstration page, the effect is as follows:

Resources to recommend

  • # article source address
  • # “Front-end Advanced” high performance render 100,000 data (virtual list)
  • How to implement a highly adaptive virtual list
  • # ahooks Virtual list

conclusion

In this article, I’ve covered some of the scenarios where a long list appears in a mall project, along with a list of different solutions and their pros and cons for each scenario. In the process of choosing the combination of paging + virtual list to solve the problem, I took you step by step to implement a simple paging virtual list, to help you understand its internal principle.

Of course, this scheme still has a lot of areas to improve, I also here to say it needs to be optimized.

  • Rolling events can add throttling events to avoid performance waste.
  • List item height is not fixed need to be given a default height after setting the new height at the start and end of the refresh easy to intercept position.
  • You can try dynamic loading display transition to optimize some details of the experience.
  • There are shadow elements in the list item that need to be considered for cache handling, otherwise scrolling will inevitably cause a reload.

There are many open source libraries on the market that can solve these problems. For example, the Ahooks in React have relatively complete virtual list practices. The code in this paper is also a source analysis of them.

In general, we do not need to build a perfect wheel from scratch in real development, directly using a mature solution, with a good product design can solve most problems.

For a mall project, its challenge lies not in the realization of function logic, but in the optimization of part of the visual experience and experience. If you think the article is helpful to you, you can point to 👍, give me a gas. If you want to know more about the front-end e-commerce project Yoyo can pay attention to this column.

The recent oliver

  • # million PV mall practice series – front end picture resources optimization actual combat
  • WebStorage Cache Usage Guide
  • # ME & Nuggets, one year after graduation, I was signed by the Nuggets | Mid-2021 summary
  • Summarize the application experience of TypeScript in project development

endnotes

This article was first published in: Gold mining technology community type: signed article author: Wangly19 collection in column: # million PV mall practice series public number: ItCodes program life