원문: https://medium.com/stackademic/react-native-masters-4-optimizing-flatlist-performance-5204ab6751cf
FlatList는 React Native 프로젝트에서 효율적이고 스크롤 가능한 방식으로 대규모 데이터 세트를 렌더링하는 데 사용하는 고성능 인터페이스입니다.
긴 이야기가 될테니 ☺️, 커피 한 잔 하세요 ☕️
VirtualizedList: FlatList
(Virtual List
개념을 구현한 React Native의 컴포넌트)의 기반이 되는 컴포넌트
메모리 사용량: 앱 충돌로 이어질 수도 있는 List에 대한 정보가 메모리에 저장되는 양
응답성: 애플리케이션이 interaction에 반응하는 능력. 예를 들어 낮은 응답성은 컴포넌트를 터치했을 때 예상대로 즉시 응답하지 않고, 조금 기다렸다가 응답하는 것을 말합니다.
빈 영역: VirtualizedList
가 항목을 충분히 빠르게 렌더링할 수 없는 경우, 목록의 일부가 렌더링되지 않은 컴포넌트인 빈 영역으로 표시될 수 있습니다.
Viewport: 픽셀로 렌더링되는 콘텐츠의 표시 영역입니다.
Window: 항목이 마운트되어야 하는 영역으로, 일반적으로 Viewport보다 훨씬 큽니다.
keyExtractor
prop을 사용하여 key 부여하기지정된 인덱스에서 특정 항목의 고유 key를 추출하는 데 사용됩니다. key는 캐싱에 사용되며 아이템 재배치를 추적하는 react key로 사용됩니다. 기본 extractor는 item.key
를 확인한 다음 item.id
를 확인한 다음 React처럼 인덱스를 사용하는 것으로 돌아갑니다. keyExtractor
는 list에 기본 key
속성 대신 React key의 id
를 사용하도록 지시합니다.
const renderItem = useCallback(({ item }) => <Item title={item.title} />, []);
<FlatList
data={listArray}
keyExtractor={(item, index) => item.id}
renderItem={renderItem}
/>;
서로 다른 두 함수 인스턴스는 결코 동일하지 않으므로 inline 함수는 얕은 동일성 비교가 실패하여 항상 리렌더링을 트리거합니다.
나쁜 예시:
import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';
const InlineFunctionFlatList = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];
return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => alert(`Clicked ${item.text}`)}>
<Text>{item.text}</Text>
</TouchableOpacity>
)}
/>
</View>
);
};
export default InlineFunctionFlatList;
좋은 예시:
import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';
const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);
const NoInlineFunctionFlatList = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];
const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};
return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
/>
</View>
);
};
export default NoInlineFunctionFlatList;
두 번째 예시에서는 FlatList
컴포넌트 외부에 renderItem
함수를 정의했습니다. handleItemClick
함수 역시 FlatList
외부에 정의되어 있습니다. 이는 renderItem
프로퍼티에 inline 함수를 작성하는 것보다 더 나은 방법입니다.
참고: 클래스 컴포넌트(이전 React 및 RN 버전)를 사용하는 경우 renderItem을 render 함수 외부로 이동시키세요.
팁: 함수 컴포넌트를 사용하는 경우(적극 권장) renderItem 함수에 useCallback
을 사용하세요.
때때로(특히 저사양 디바이스의 경우) Android는 ListView(항목의 list를 렌더링하기 위해 Android에서 제공하는 기본 솔루션)에서 고해상도 이미지를 처리할 수 없습니다. 해상도를 즉시 줄일 수 있다고 해도 성능 문제가 발생할 수 있습니다. 따라서 sm/md 해상도 이미지를 사용하세요. 최대 720p를 사용할 수 있지만 충분히 작게 유지해야 합니다. 그렇지 않으면 이미지 품질에 영향을 미칩니다. 또한 이미지 fadeDuration을 줄여 빠르게 스크롤하는 동안 이미지 fade 문제를 방지할 수 있습니다.
initialNumToRender
prop 사용하기초기 배치에서 렌더링할 항목 수를 결정하려면 initialNumToRender
prop을 사용합니다. 화면을 채우기에 충분한 만큼의 수를 설정합니다. scroll-to-top 동작의 성능을 향상시키기 위해 이러한 항목은 window 렌더링의 일부로 마운트 해제되지 않습니다.
getItemLayout
prop 사용하기(data, index) => {length: number, offset: number, index:number}
getItemLayout
은 항목의 크기(높이 또는 너비)를 미리 알고 있으면 동적 내용 측정을 건너뛸 수 있는 선택적 최적화입니다. 다음과 같이 고정된 크기의 항목이 있는 경우 getItemLayout
이 효율적입니다.
import React from "react";
import { View, FlatList, Text, TouchableOpacity } from "react-native";
const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);
const List = () => {
const data = [
{ id: "1", text: "Item 1" },
{ id: "2", text: "Item 2" },
{ id: "3", text: "Item 3" },
// ... more data
];
const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};
const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});
return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
initialNumToRender={10}
getItemLayout={getItemLayout}
/>
</View>
);
};
export default List;
removeClippedSubviews
은 큰 list의 스크롤 성능을 향상시킬 수 있습니다. Android에서는 기본값이 true입니다. 이 프로퍼티는 off-screen view를 제거하여 리소스를 확보합니다. 큰 list에서는 매력적으로 작동할 수 있지만 로드되지 않은 view로 다시 스크롤할 때 지연이 발생할 수 있습니다.
참고: 상황에 따라 버그(콘텐츠 누락)가 있을 수 있습니다. 사용에 따른 책임은 사용자에게 있습니다.
각 배치에서 렌더링할 최대 항목 수를 제어하기 위해 maxToRenderPerBatch
로 큰 list를 최적화합니다. 이 프로퍼티의 역할은 배치당 렌더링되는 항목 수를 제한하는 것입니다. 특히 큰 list를 처리할 때 유용합니다.
장점: 숫자를 크게 설정하면 스크롤할 때 시각적으로 빈 공간이 줄어듭니다(채우기 비율이 증가합니다).
단점: 배치당 항목이 많으면 자바스크립트 실행 시간이 길어져 프레스와 같은 다른 이벤트 처리가 차단되어 응답성이 저하될 수 있습니다.
✨ 내가 제공해야 하는 값을 어떻게 알 수 있나요?
모든 프로젝트와 상황에 보편적이고 적합한 방법은 아니지만, 저는 이 방법을 프로젝트에 적용하고 있습니다. list 항목이 렌더링될 때 viewport 바깥쪽 하단에 2~3개의 항목이 더 있기를 원합니다. 따라서 뷰포트에 10개 항목이 있는 경우 보통 maxToRenderPerBatch={13}
을 만들지만, 4개 항목만 있는 경우 maxToRenderPerBatch={6}
을 만듭니다. 물론 list에 있는 항목의 성격과 필요에 따라 결정해야 합니다.
✨ 이제 List 컴포넌트가 한 번 더 최적화되었습니다.
import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';
const renderItem = (item, onPress) => (✨
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);
const List = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];
const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};
const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});
return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
initialNumToRender={10}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
maxToRenderPerBatch={13}
/>
</View>
);
};
export default List;
여기에 전달된 숫자는 측정 단위로, 1은 viewport 높이에 해당합니다. 기본값은 21(위쪽 viewport 10개, 아래쪽 viewport 10개, 중간 viewport 1개)입니다.
장점: windowSize
가 클수록 스크롤하는 동안 빈 공간이 표시될 가능성이 줄어듭니다. 반면 windowSize
가 작으면 동시에 마운트되는 항목 수가 줄어들어 메모리를 절약할 수 있습니다.
단점: windowSize
가 클수록 메모리를 더 많이 사용합니다. windowSize
작을수록 빈 공간이 표시될 확률이 높아집니다.
React.memo()
는 컴포넌트에 전달된 prop이 변경될 때만 다시 렌더링되는 메모화된 컴포넌트를 생성합니다. 이 함수를 사용해 FlatList의 컴포넌트를 최적화할 수 있습니다.
import React, { memo, useCallback } from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';
const renderItem = memo(({ item, onPress }) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
));
const List = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];
const handleItemClick = useCallback((text) => {
alert(`Clicked ${text}`);
}, []); // Empty dependency array because handleItemClick doesn't depend on any external variables
const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});
return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem({ item, onPress: handleItemClick })}
initialNumToRender={10}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
maxToRenderPerBatch={13}
windowSize={5}
/>
</View>
);
};
export default List;
빈 종속성 배열의 useCallback으로 handleItemClick
함수를 래핑했습니다. 이렇게 하면 컴포넌트가 다시 마운트되지 않는 한 해당 참조가 일정하게 유지됩니다. 또한 renderItem
을 memo
로 래핑하면 컴포넌트는 prop(item
및 onPress
)이 변경될 때만 다시 렌더링됩니다. 이 리팩터링은 list 항목의 불필요한 리렌더링을 방지하여 성능을 향상시킵니다.
커뮤니티 패키지(예: @DylanVann의 react-native-fast-image)를 사용하면 성능이 더 뛰어난 이미지를 얻을 수 있습니다. list의 모든 이미지는 new Image()
인스턴스입니다. loaded
훅에 더 빨리 도달할수록 JavaScript 스레드가 다시 해제되는 속도도 빨라집니다.