preface

In a recent feature development, when using Ant Design’s dynamic Form item form. List, a performance problem occurred when the Form item reached a certain level, which was finally solved by using virtual List technology, as noted here.

Problem orientation

The problem comes from the self-developed user survey questionnaire management platform. According to users’ feedback, when the number of questionnaire questions set reaches a certain level, the page will be stuck or even crash or lose response.

Here is a simplified online demo replicating this scenario:

It can be seen that when the number of questionnaire questions reaches 1000, the page loading time is very long. In addition, it can also feel obvious lag when operating form items. The user hopes that the platform can support the setting of the upper limit of 2000 questions, obviously the current platform is not in line with the requirements (as for really someone will have the patience to fill in 2000 questions 🤔, I dare not ask, I dare not say 😂)

Let’s just get to work. First of all, we can get a preliminary inference from the phenomenon that the page crash is probably caused by excessive memory usage. Open the Chrome Developer Tools and switch to the Memory panel, you can see that the current TAB occupies more than 1GB of Memory, and the browser has a Memory limit for each TAB (see Dealing with Memory Limitations).

We could certainly use a snapshot of memory for this analysis, but Lighthouse — one of the built-in components of the Chrome developer tools — is a powerful reminder:

Lighthouse helps us analyze the most frequently asked questions about your site from multiple metrics and make recommendations that highlight them. Here we are focusing only on Performance, so simply check this box to reduce analysis time and click Generate Report to Generate the report. (To save time, set the number of questions to 100.)

In the Diagnostics section, you can find our problem and solution:

Excessive DOM elements have an excessive impact on memory performance and Avoid an excessive DOM size. Lighthouse also detected the use of React and recommended that we use the windowing library such as React-Window to minimize the number of DOM nodes.

To solve the process

1. Introduce the virtual list

Instead of loading all the DOM nodes at once, windowing is simply defined as rendering only long lists of data in the visible area. Its principle is not the focus of this paper, and will not be expanded here. As you can see, antD form. List is also rendered as a List, so virtual lists can theoretically be used. For convenience, the following uses lists to refer to virtual lists and List items to refer to each element in form. List.

In keeping with the principle of not reinventing the wheel, we introduced React-Window, a widely used library of virtual scroll implementations. React-window provides FixedSizeList, VariableSizeList, FixedSizeGrid, and VariableSizeGrid. Select them according to your application scenario. The following is an example: react-window.now.sh

Since form. List supports dynamic additions and decrement, the number of List items is not fixed accordingly, so select the VariableSizeList component as the outer component of the virtual List. VariableSizeList has several properties that must be specified:

  1. itemCountList item number
  2. heightList of highly
  3. width: List height
  4. itemSizeFunction returns the size of the list item or, for vertical lists, the height of the list item

The number of List items is the number of Form items (fields. Length). How to determine the list size, and list item size, go to 👇

2. Solve the size problem

For the list container’s width and height, we want it not to be fixed, but to fill the parent container 100%. Then we can play the React – Virtualized – Auto – Sizer game. This is the match recommended by the React – Window team (of course, we both have the same game writer).

The size of the list is determined, what about the list items? As you can see, in this case the number of options per item is not fixed, and the height of the list items is not fixed, so you need to calculate the height dynamically. Fortunately, the itemSize attribute is a function that takes a list item index parameter. Using this, you can store the height of each list item, returned by index.

const rowHeights = useRef({});

const getRowHeight = (index) = > {
  return rowHeights.current[index] || 200;
};
Copy the code

It is important to note that the React-Window determines the itemSize before rendering the list items, so it needs to give an estimated height (200 here), otherwise the React-Window cannot calculate which items need to be rendered. Then simply update the height value after the list item is rendered:

const listRef = useRef(null);

const setRowHeight = (index, size) = >{ rowHeights.current = { ... rowHeights.current, [index]: size }; listRef.current.resetAfterIndex(0);
};

const Row = () = > {
  const rowRef = useRef(null);

  useEffect(() = > {
    if (rowRef.current) {
      setRowHeight(index, rowRef.current.clientHeight);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [rowRef]);
}
Copy the code

We also need to call the resetAfterIndex method of VariableSizeList, because VariableSizeList caches the offsets and measurements of the list items of the index for performance purposes. This method should be called whenever the size of a list item changes. Ensure that list items in the viewable area are arranged correctly.

One other thing to note here is that react-Window does virtual scrolling by absolutely positioning list items (via inline styles), so you also need to append the style parameter to the DOM element being rendered. This is highlighted in the readme.md of react-Window: portals. If you forget to do this, you will have scrollbar flickering and blank content issues.

But when we add the style parameter, a new problem arises: the list item height is fixed! Don’t panic, check the list item elements to see:

This is the style parameter just passed, you can see that the style parameter contains absolute positioning information, as well as the height information, and this value is the estimated height value we set 200. In this case, we need a dynamic list item height value, so we just need to remove the height attribute. Note that style is an immutable object and cannot be deleted directly. You can shallow copy it or set height to auto:

style={{ ... style,height: 'auto' }}
Copy the code

This solves the problem of determining the height of list items during initialization. However, in this case, the height of the list item can be changed by adding the add option. Therefore, we need to monitor the change of the height of the list item and update the position information of the list item in time. Similarly, in accordance with the principle of not repeating the wheel, we use the ResizeObserver component in RC-REsize-Observer to achieve this, which is one of the dependencies of ANTD and does not need to be installed separately.

import ResizeObserver from 'rc-resize-observer'

// ...
<ResizeObserver
  onResize={() = > {
    setRowHeight(index, rowRef.current.clientHeight)
  }}
>
// ...
</ResizeObserver>
Copy the code

At this point, we have transformed the form. List into a virtual List 👏

3. Supplement form verification

Virtual lists are implemented, but some issues remain. As the form items are not all loaded, for the unrendered form items, the form verification provided by ANTD cannot be used to verify this table item, resulting in the submission of illegal form information. Therefore, in a virtual list scenario, additional processing of form data is required.

The idea is simple: verify the validity of the form data when submitting the form. If a form item fails to verify, scroll to that item. The simplified code is as follows:

const scrollToQuestion = (index) = > {
  listRef.current.scrollToItem(index, 'smart')
  // Trigger checksum
  form.validateFields().catch(({ errorFields }) = > {
    // Verification failed scroll to the first error field
    form.scrollToField(errorFields[0].name)
  })
}

const onFinish = values= > {
  const { questions} = values
  if (!Array.isArray(questions)) return

  let question
  for (let i = 0, len = questions.length; i < len; i++) {
    question = questions[i]
    if(! question.title)return scrollToQuestion(i)
    const { options } = question
    if (Array.isArray(options)) {
      for ( const option of options) {
        if(! option.title)return scrollToQuestion(i)
      }
    }
  }
}

// ...
<Form form={form} onFinish={onFinish} scrollToFirstError>
// ...

Copy the code

For rendered Form items, antD Form will trigger validation on submission, and onFinish will only be triggered if validation is successful, so just one more manual validation inside onFinish will find the error items in the unrendered Form items. Using the scrollToItem method of VariableSizeList, you can scroll the virtual list to the specified item, render the error item, and then trigger the antD Form validation function.

The final results

The following 👇

thinking

This paper mainly explains how to use virtual list to solve form performance problems in big data scenarios, and also provides some ideas for some common problems in using virtual list:

  1. The list item height is uncertain, you can use the estimated height and render with the real height instead. The estimated height should be as close as possible to the real height. In this case, a fixed value is taken, but in fact, the height of the list item has a positive correlation with the number of options. Therefore, it is better to estimate the height according to the correlation between the data of the list item and DOM, so as to avoid scroller position flickering caused by excessive difference.
  2. The height of the rendered list item changes, and the height value can be dynamically updated by monitoring the size of the list item element.

Although virtual lists can improve performance, they only have a significant effect in a large number of data scenarios. In addition, virtual lists have some problems, such as a short blank time when a page is quickly swiped, and users cannot call CTRL + F to search the page content. In cases where there is not enough data to cause performance problems, it can even be a negative optimization. Therefore, it is recommended to consider introducing virtual lists 🤔 only when performance problems occur

– End-