React Native FlatList делает приложение чрезвычайно медленным после 10 элементов

Я пытаюсь создать простое приложение для секундомера в среде реагирования. Я использую AsyncStorage для хранения записанных данных о времени в локальном хранилище, а также хочу отобразить таблицу, в которой показаны все записанные времена. Основная идея заключается в том, что когда человек нажимает и удерживает анимацию LottieView, он запускает таймер, когда он нажимает, таймер останавливается, записывает в AsyncStorage, а затем обновляет таблицу.

После 10 элементов мой FlatList (внутри TimeTable.jsx) становится очень медленным, и я не знаю, почему. Я считаю, что компонент, который вызывает эту ошибку, TimeTable.jsx, но я не совсем уверен, почему.

src/components/Timer/TimeTable.jsx

import React, {useState, useEffect} from 'react'
import { StyleSheet, FlatList  } from "react-native";
import { Divider, List, ListItem } from '@ui-kitten/components'
import AsyncStorage from '@react-native-async-storage/async-storage';

const getRecordedEventsTable = async (dbKey) => {
  try {
    let currentDataArray = await AsyncStorage.getItem(dbKey);
    return currentDataArray ? JSON.parse(currentDataArray) : [];
  } catch (err) {
    console.log(err);
  }
};

const renderItem = ({ item, index }) => (
  <ListItem
  title={`${item.timeRecorded / 1000} ${index + 1}`}
  description={`${new Date(item.timestamp)} ${index + 1}`}
   />
)

export const TimeTable = ({storageKey, timerOn}) => {
  const [timeArr, setTimeArr] = useState([]);

  useEffect(() => {
    getRecordedEventsTable(storageKey).then((res) => {
        setTimeArr(res)
    })

  }, [timerOn])

  return (
      <FlatList
        style={styles.container}
        data={timeArr}
        ItemSeparatorComponent={Divider}
        renderItem={renderItem}
        keyExtractor={item => item.timestamp.toString()}
      />
  );
};

const styles = StyleSheet.create({
    container: {
      maxHeight: 200,
    },
  });

src/components/Timer/Timer.jsx

import React, {useState, useEffect, useRef} from 'react'
import {
    View,
    StyleSheet,
    Pressable,
} from 'react-native';
import {Layout, Button, Text} from '@ui-kitten/components';
import LottieView from 'lottie-react-native'
import AsyncStorage from '@react-native-async-storage/async-storage';
import {TimeTable} from './TimeTable'

const STORAGE_KEY = 'dataArray'

const styles = StyleSheet.create({
    container: {
        flex: 1, 
        justifyContent: "center",
        alignItems: "center",
        backgroundColor: "#E8EDFF"
    },
    seconds: {
        fontSize: 40,
        paddingBottom: 50,
    }
})

const getRecordedEventsTable = async () => {
    try {
        let currentDataArray = await AsyncStorage.getItem(STORAGE_KEY)
        return currentDataArray ? JSON.parse(currentDataArray) : []
    } catch (err) {
        console.log(err)
    }
}

const addToRecordedEventsTable = async (item) => {
    try {
        let dataArray = await getRecordedEventsTable()
        dataArray.push(item)
    
        await AsyncStorage.setItem(
            STORAGE_KEY,
            JSON.stringify(dataArray)
            )
    } catch (err) {
        console.log(err)
    }
}

// ...

const Timer = () => {
    const [isTimerOn, setTimerOn] = useState(false)
    const [runningTime, setRunningTime] = useState(0)
    const animation = useRef(null);

    const handleOnPressOut = () => {
        setTimerOn(false)
        addToRecordedEventsTable({
            timestamp: Date.now(),
            timeRecorded: runningTime
        })

        setRunningTime(0)
    }

     useEffect(() => {
        let timer = null

        if(isTimerOn) {
            animation.current.play()

            const startTime = Date.now() - runningTime
            timer = setInterval(() => {
                setRunningTime(Date.now() - startTime)
            })
        } else if(!isTimerOn) {
            animation.current.reset()
            clearInterval(timer)
        }

        return () => clearInterval(timer)
    }, [isTimerOn])

    return (
        <View>
            <Pressable onPressIn={() => setTimerOn(true)} onPressOut={handleOnPressOut}>
                <LottieView ref={animation} style={{width: 300, height: 300}} source={require('../../../assets/record.json')} speed={1.5}/>
            </Pressable>
            <Text style={styles.seconds}>{runningTime/1000}</Text>
            <TimeTable storageKey={STORAGE_KEY} timerOn={isTimerOn} />
            <Button onPress={resetAsyncStorage}>Reset Async</Button>
        </View>
    )
}

export default Timer

Любая помощь, приветствуется. Спасибо.


РЕДАКТИРОВАТЬ: в консоли появилось следующее предупреждение:

VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
  "contentLength": 1362.5,
  "dt": 25161,
  "prevDt": 368776,

РЕДАКТИРОВАТЬ: В Timer.jsx у меня есть текстовое представление в функции рендеринга следующим образом: <Text style={styles.seconds}>{runningTime/1000}</Text> эта часть должна отображать значение секундомера и обновляться с помощью таймера. По мере того, как FlatList становится больше, эта часть становится чрезвычайно лаговой.

Я подозреваю, что, поскольку это пытается постоянно перерисовываться, дочерний компонент TimeTable.jsx также постоянно перерисовывается?


person mr3mo    schedule 11.02.2021    source источник


Ответы (3)


Мне кажется, что у вас есть петля здесь:

useEffect(() => {
    getRecordedEventsTable(storageKey).then((res) => {
        setTimeArr(res)
    })

  }, [timeArr, timerOn])

useEffect будет вызываться каждый раз при обновлении timeArr. Затем внутри вы вызываете свой асинхронный getRecordedEventsTable, и каждый раз, когда он завершается, он вызывает setTimeArr, который устанавливает timeArr, запуская последовательность для повторного запуска.

person jnpdx    schedule 11.02.2021
comment
Привет @jnpdx, я принял твое предложение, а также исправил небольшую ошибку в своем коде, из-за которой я неправильно распределял реквизит. Это повышает производительность и показывает список, однако проблема сохраняется после 30 элементов. Я думаю, что он слишком часто пытается повторно отобразить каждый элемент списка, не знаю, как это решить. - person mr3mo; 11.02.2021
comment
Меня бы это очень удивило, если бы это было большим успехом, но что произойдет, если вы создадите description какой-нибудь фиктивный текст прямо сейчас, который не будет каждый раз создавать новый Date для каждой строки? - person jnpdx; 11.02.2021
comment
Кажется, что это делает его немного лучше, но проблема все еще сохраняется. Также, как ни странно, в симуляторе iOS он работает лучше, чем в моем клиенте expo go на телефоне. @jnpdx - person mr3mo; 11.02.2021
comment
Вот гипотеза: может ли быть так, что в родительском компоненте Timer.jsx из-за того, что свойства timerOn меняются каждый раз, когда пользователь нажимает кнопку, весь дочерний компонент пытается выполнить повторную визуализацию, и каждый раз вызывается вызов AsyncStorage? Может ли это быть причиной того, что <Text style={styles.second}>{runningTime/1000}</Text> рендерится очень медленно? - person mr3mo; 11.02.2021
comment
Я говорю это, потому что кажется, что это компонент <Text style={styles.second}>{runningTime/1000}</Text> внутри Timer.jsx, который должен показывать значение текущего времени секундомера, которое постоянно отстает. Анимация LottieView работает плавно, а FlatList можно прокручивать. Но я предполагаю, что текст runningTime отстает, потому что FlatList пытается повторно отобразить, пока этот пользовательский интерфейс пытается обновить. Есть ли у вас какие-либо предложения по этому поводу? - person mr3mo; 11.02.2021
comment
Я думаю, что ваше подозрение правильное. Я бы предложил вырвать/издеваться над функциональностью, которая, как вы ожидаете, будет проблемой, и посмотреть, действительно ли это виновник. - person jnpdx; 11.02.2021

Для оптимизации FlatList вы можете использовать различные доступные параметры. Вы можете прочитать это https://reactnative.dev/docs/optimizing-flatlist-configuration.

Также вы можете использовать хук useCallback для функции renderItems.

Я бы рекомендовал прочитать это https://medium.com/swlh/how-to-use-flatlist-with-hooks-in-react-native-and-some-optimization-configs-7bf4d02c59a0

person awan_u    schedule 11.02.2021

Я смог решить эту проблему. Основной причиной медлительности было то, что в родительском компоненте Timer.jsx из-за того, что свойства timerOn меняются каждый раз, когда пользователь нажимает кнопку, весь дочерний компонент пытается выполнить повторную визуализацию, и этот вызов AsyncStorage вызывается каждый раз. Это причина того, что {runningTime/1000} рендерится очень медленно. Потому что каждый раз, когда компонент timerOn изменяется, все дочерние компоненты ставятся в очередь для повторного рендеринга.

Решение для этого состояло в том, чтобы визуализировать компонент таблицы из родителя таймера, а не внутри компонента таймера, и поддерживать состояние в таймере, которое передается обратно родительскому элементу, а затем передается компоненту таблицы.

Вот как теперь выглядит мой родительский компонент:

  const [timerStateChanged, setTimerStateChanged] = useState(false);

  return (
    <View style={styles.container}>
      <Timer setTimerStateChanged={setTimerStateChanged} />
      <View
        style={{
          borderBottomColor: "grey",
          borderBottomWidth: 1,
        }}
      />
      <TimeTable timerOn={timerStateChanged} />
    </View>
  );
};

Лучшим способом было бы использовать что-то вроде контекста React или Redux.

Спасибо за помощь.

person mr3mo    schedule 20.02.2021