preface

Making: github.com/tingyuxuan2… NPM package: www.npmjs.com/package/tar… Taro Materials Market: taro-ext.jd.com/plugin/view… At present, this component has been used in production environment and runs stably.

background

The React syntax is fully supported. There are no more restrictions on the React syntax. The configurable level of the project is also greatly liberalized, giving developers the freedom to use it. But because Taro3 runtime architecture, is part of the cost of performance at the expense of page, this also indirectly led to abnormal caton our list page, as a result of our list page is a one-time request all the data, then apply colours to a drawing, so the page node initializes the rendering will render many nodes, plus some screening, say nothing of the user, Catton’s already making us sick…

Cause analysis,

  1. Page nodes are too many and render time is too long, which impedes the user’s need for fast operation
  2. The amount of setState data in the list is too large, resulting in a long communication time between the logical layer and the rendering layer
  3. Changing state, such as clicking on a list filter, causes the list data to be heavily re-rendered, causing the page to stagnate

The solution

Solution 1: back-end paging

The first thing we wanted to do was to paginate the interface so that we didn’t have to do a lot of initial rendering, and then listen for the drop down time, and then render the data in turn; However, the scheme was killed at the first time for the following reasons:

  1. List page interface is not only used by small programs, app clients also share the same set of interface, if you want to change the interface, then the APP client also modify the logic (list page logic is also quite complex), because we try to increase the demand of the client, it is not very kind
  2. Even if the client and server are persuaded to modify together, the initialization speed of our page is improved. However, as the page is pulled up, more and more data are loaded. When a certain amount of data is loaded, the operation of the filter items of the page will still lead to operation lag

Conclusion: Therefore, virtual lists are the only way to keep the page initialization and data loading complete without lag, unless the setState data is reduced and the total number of render nodes on the page is reduced

Scheme 2: Official Virtual List (3.2.1)

The official document: docs. Taro. Zone/docs/virtua…

Principle: Only render the data nodes in the current viewable area, listen to the page viewable area, not render the nodes not in the viewable area, thus greatly reducing the number of page nodes to render

Effect: The team tried the virtual list at the first time, but the effect was not very ideal. The main problems are as follows: 1. Since not all items in our list are of equal height, the virtual list will dynamically calculate the height of each Item during each rendering, resulting in the jitter of the list height transformation 2. In the process of pull-up loading, the problem of infinite sliding loading occasionally appears, resulting in page disorder 3. Swiping too fast can result in a blank screen for long periods of time and a poor experience

Summary: The known problems need to be solved by the official team, but they need to wait, and the problem of Item height being different and requiring frequent dynamic calculation of Item height is not easy to solve, and there is no particularly good solution on the market at present, so the solution is also stranded…

Project analysis

  1. Reduce the number of nodes on the page: only virtual lists can be used, rendering only nodes in the current viewable area;
  2. Reduce the amount of setState data: can not not go to the full setState every time;
  3. Dynamic calculation of Item height: recalculate each Item height each time, too much calculation, will also hinder page rendering;

Based on the above problems, our team eventually produced a better (not best, only better) virtual list solution.

The ultimate (better) solution

Results the overview

GIF preview:

Take a look at the virtual list node composition

Preliminary thinking 🤔

  1. Keep listening to the viewable area and only render nodes within the viewable area;
  2. Due to the problem of unequal height of items, we need to dynamically calculate the height of each Item, and the effect is not good, so we give up. Because we only render the visible area of the data, then we can to each panel data for a dimension (line), when a panel data rendering is finished, we record the node of the whole screen height, when the node again into the viewing area of the screen, we will record the height to reactivate the screen, Does that take a lot of computing away? 🤔
  3. So in order to reduce the amount of setState data, can we use the height of the screen (a simple object data structure) to occupy the data of those screens in the viewable area? 🤔

As if train of thought can say of the past, that in the end can not be feasible, below we come to explore after all 😄

Coding

Formatting data

First, we need to pass in the list data from the outside, and then we need to process it inside the component. According to the idea of one-screen rendering, we will change the list to a two-dimensional array for the moment, one dimension is one screen of data.

export default class VirtialList extends Component { constructor(props) { super(props) this.state = { twoList: []. // componentDidMount() {// componentDidMount() {// componentDidMount() {// componentDidMount() {// componentDidMount() {// componentDidMount() const {list} = this.props InitList = [] // Carries the initialized two-dimensional array, which will not change after initialization, Unless the external list changes /** * format the list as two-dimensional * @param list list */ formatList(list) {// User can customize the amount of data for each dimension of the two-dimensional array const {segmentNum} = This. Props let arr = [] const _list = [] // array.list ((item, Index) = > {arr. Push (item) if ((index + 1) % segmentNum = = = 0) {/ / the quantity of a dimension is enough into _list _list. Push (arr) arr = []}}) / / Slice (_list.length * segmentNum) if (restList? .length) { _list.push(restList) } this.initList = _list this.setState({ twoList: _list.slice(0, 1), // first render, } render() {const {twoList, } = this.state // const {onRender} = this.props return (<ScrollView> <View className="zt-main-list"> {twoList? .map((item, PageIndex) => {return (// Each screen is wrapped with a node <View Key ={pageIndex} className={' wrap_${pageIndex} '}> {item.map((el, index) => { return onRender? .(el, (pageIndex * segmentNum + index), pageIndex) }) } </View> ) }) } </View> </ScrollView> ) } }Copy the code

Set screen Height

We have formatted the data into a two-dimensional array, and the initial rendering will only render the first dimension of the array, so after rendering the dimension node, we need to record the height of the dimension node on the screen

State = {wholePageIndex: 0,} formatList(list) {//... this.setState({ twoList: _list.slice(0, 1),}, () => {// note: Put it in the next event loop to get the node, () => {this.setheight ()})})} pageHeightArr = [] // The height of each screen setHeight():void {const {tar.nexttick (() => {this.setheight ()})} pageHeightArr = [] // setHeight():void {const { wholePageIndex } = this.state const query = Taro.createSelectorQuery() query.select(`.wrap_${wholePageIndex}`).boundingClientRect() query.exec((res) => { this.pageHeightArr.push(res? [0]? .height) }) }Copy the code

Pull on loading

Using the onScrollToLower property of ScrollView, the listener list is pulled down to the bottom, the next dimension is loaded, and the two-dimensional arraylist is stuffed

<ScrollView scrollY onScrollToLower={this.renderNext} lowerThreshold={250} > //... </ScrollView> renderNext = () => { Modify screen index const page_index = this. State. WholePageIndex + 1 enclosing setState ({wholePageIndex: }, () => {const {wholePageIndex, twoList} = this.state TwoList [wholePageIndex] = this.initList[wholePageIndex] this.setState({twoList: [...twoList], }, () => { Taro.nextTick(() => { this.setHeight() }) }) }) }Copy the code

Monitor viewable area

Observe, observe the current viewable area and render the corresponding dimension data. How to handle the data that is not in the viewable area? This is also the most important part of this component, when the data is not in the visible area, because we have previously recorded a height of the dimension node after rendering, then we use a node to give the corresponding height, for space!

setHeight() { //... This.observe ()} observe = () => {const {wholePageIndex} = this.state // Component height const {scrollViewProps} = Const scrollHeight = scrollViewProps? // Const scrollHeight = scrollViewProps? .style? The height | | this. WindowHeight / / set the range of listening, We here by default to monitor the height of the upper and lower two screens const observer = Taro. CreateIntersectionObserver (this) currentPage) page). RelativeToViewport ({top: 2 * scrollHeight, bottom: 2 * scrollHeight, }) observer.observe(`.wrap_${wholePageIndex}`, (res) => { const { twoList } = this.state if (res? .IntersectionRatio <= 0) {// If there is no intersecting area with the current viewport, twoList[wholePageIndex] = {height: this.pageHeightArr[wholePageIndex] } this.setState({ twoList: [...twoList], }) } else if (! twoList[wholePageIndex]? .length) {twoList[wholePageIndex] = this.initList[wholePageIndex] this.setState({twoList: [...twoList], }) } }) } render() { return ( <ScrollView> <View className="zt-main-list"> { twoList? .map((item, pageIndex) => { return ( <View key={pageIndex} className={`wrap_${pageIndex}`}> { item? .length > 0 ? ( <Block> { item.map((el, index) => { return onRender? .(el, (pageIndex * segmentNum + index), pageIndex) }) } </Block> ) : ( <View style={{'height': `${item? .height}px`}}></View> ) } </View> ) }) } </View> </ScrollView> ) }Copy the code

Performance improvement

The following is the comparison of several groups of data before and after the optimization of zhihang small program ticket list page:

List page rendering time

Mainly refers to the time interval from entering the page to the complete rendering of the page node, in milliseconds (including interface request time) :

Filter item response time

It mainly refers to the time interval between the bottom floating layer leaking from the time of clicking the filter button at the bottom of the page, in milliseconds:

Summary of Performance Improvement

As you can see, after optimizing the page using virtual lists, there is a qualitative improvement in the overall rendering performance of the page, with page rendering speed increasing by nearly 23% and button click response speed increasing by nearly 50%! 😄 at present, we only use virtual list to optimize the flight list. There is also a point of performance loss on the page is the calendar list at the top. Later, we will change the calendar list into virtual list, and I believe the performance will be further improved!

conclusion

Component implementation is relatively simple, and the key is:

  1. Format the list data as a two-dimensional array
  2. Data that is not in the viewable area is populated with {height: xx px}, reducing the amount of setState for list data
  3. Dynamically calculate the height of each screen and record, reduce the amount of calculation

The last

If this article has helped you with your current development, or given you some ideas to think about, feel free to join us.