Optimasi Kinerja dan Debugging
Tujuan Pembelajaran Pertemuan
Setelah pertemuan ini, mahasiswa diharapkan mampu:
- Mengidentifikasi masalah performa dalam aplikasi React Native
- Menggunakan tools debugging untuk menemukan dan memperbaiki bug
- Menerapkan teknik optimasi untuk meningkatkan kinerja aplikasi
- Memahami best practices dalam pengembangan aplikasi React Native
Mengapa Optimasi Penting dalam Mobile Development
Optimasi penting karena:
- Perangkat mobile memiliki keterbatasan resource (CPU, RAM, battery)
- User ekspektasi: aplikasi harus responsif dan cepat
- Aplikasi lambat = user uninstall (53% user meninggalkan app yang load > 3 detik)
- Rating app store dipengaruhi performa
- Battery drain berdampak pada user satisfaction
Dampak Aplikasi Lambat terhadap User Experience
Konsekuensi aplikasi dengan performa buruk:
- Bounce rate tinggi
- Rating rendah di app store
- Uninstall rate meningkat
- Reputasi developer menurun
- Kehilangan revenue potensial
Contoh metrik ideal:
- Load time: < 2 detik
- Frame rate: 60 FPS
- Memory usage: optimal sesuai device
Metrik Kinerja Utama (FPS, Memory Usage, Load Time)
Metrik yang perlu dimonitor:
- FPS (Frames Per Second): Target 60 FPS untuk animasi smooth
- Memory Usage: Hindari memory leak
- Load Time: Waktu aplikasi mulai hingga siap digunakan
- Bundle Size: Ukuran file aplikasi
- API Response Time: Kecepatan fetch data
Debugging di React Native
Pengenalan Debugging dalam React Native
Debugging adalah proses menemukan dan memperbaiki bug dalam kode. Dalam React Native, kita memiliki beberapa layer debugging:
- JavaScript layer
- Native layer (Android/iOS)
- Network layer
- State management layer
Tools debugging di Expo:
- Expo Developer Menu
- Chrome DevTools
- React DevTools
- Console logging
- Expo Go App
- Error Boundaries
- Network inspector
Developer Menu dapat diakses dengan:
- Shake device (physical device)
- Cmd+D (iOS simulator)
- Cmd+M (Android emulator)
Menu options:
- Reload: Restart aplikasi
- Debug Remote JS: Buka Chrome DevTools
- Show Performance Monitor: Lihat FPS dan memory
- Toggle Element Inspector: Inspect UI elements
Aktifkan remote debugging untuk akses Chrome DevTools:
console.log("Debug message");
console.warn("Warning message");
console.error("Error message");
function calculateTotal(items) {
debugger;
return items.reduce((sum, item) => sum + item.price, 0);
}
Console Logging Best Practices
console.log(data);
console.log("User Data:", data);
console.table(users);
console.group("API Call");
console.log("URL:", url);
console.log("Response:", response);
console.groupEnd();
const DEBUG = __DEV__;
if (DEBUG) {
console.log("Development mode:", userData);
}
const logger = {
info: (message, data) => console.log(`[INFO] ${message}`, data),
error: (message, error) => console.error(`[ERROR] ${message}`, error),
};
React DevTools memungkinkan inspeksi component tree dan props/state:
function UserProfile({ user }) {
const [likes, setLikes] = useState(0);
return (
<View>
<Text>{user.name}</Text>
<Text>Likes: {likes}</Text>
</View>
);
}
Expo Go App untuk Testing Real-time
Expo Go memungkinkan testing langsung di device:
import { LogBox } from "react-native";
LogBox.ignoreLogs(["Warning: componentWillReceiveProps", "Setting a timer"]);
LogBox.ignoreAllLogs();
Error Boundaries dalam React Native
Error Boundaries menangkap error di component tree:
import React from "react";
import { View, Text, Button } from "react-native";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Error caught:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<View style={{ flex: 1, justifyContent: "center", padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 10 }}>
Oops! Something went wrong
</Text>
<Text style={{ marginBottom: 20 }}>{this.state.error?.message}</Text>
<Button
title="Try Again"
onPress={() => this.setState({ hasError: false })}
/>
</View>
);
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<MainApp />
</ErrorBoundary>
);
}
Menangani Runtime Errors
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch user:", error);
Alert.alert("Error", "Failed to load user data");
return null;
}
}
Promise.reject(new Error("Test error")).catch((error) => {
console.error("Promise rejected:", error);
});
ErrorUtils.setGlobalHandler((error, isFatal) => {
if (isFatal) {
Alert.alert(
"Unexpected error occurred",
`Error: ${error.name} ${error.message}`
);
}
});
Network Debugging dan Inspeksi API Calls
const fetchWithLogging = async (url, options = {}) => {
console.log(`[API] Request to: ${url}`);
console.log("[API] Options:", options);
const startTime = Date.now();
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
console.log(`[API] Response from ${url} (${duration}ms)`);
console.log("[API] Status:", response.status);
return response;
} catch (error) {
console.error(`[API] Error fetching ${url}:`, error);
throw error;
}
};
fetchWithLogging("https://api.example.com/products")
.then((res) => res.json())
.then((data) => console.log("[API] Data:", data));
import axios from "axios";
axios.interceptors.request.use((request) => {
console.log("Starting Request", request);
return request;
});
axios.interceptors.response.use((response) => {
console.log("Response:", response);
return response;
});
Optimasi Kinerja
Prinsip Dasar Optimasi React Native
Prinsip optimasi:
- Measure First: Identifikasi bottleneck sebelum optimasi
- Avoid Premature Optimization: Optimasi saat ada masalah nyata
- Profile Performance: Gunakan tools untuk measure
- Incremental Improvement: Optimasi bertahap
import { PerformanceObserver, performance } from "react-native-performance";
performance.mark("screen-render-start");
performance.mark("screen-render-end");
performance.measure(
"screen-render",
"screen-render-start",
"screen-render-end"
);
Component Re-rendering dan Cara Menghindarinya
function ParentComponent() {
const [count, setCount] = useState(0);
const user = { name: "John", age: 30 };
return (
<View>
<Button title="Increment" onPress={() => setCount(count + 1)} />
<ChildComponent user={user} />
</View>
);
}
const USER_DATA = { name: "John", age: 30 };
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<View>
<Button title="Increment" onPress={() => setCount(count + 1)} />
<ChildComponent user={USER_DATA} />
</View>
);
}
function ParentComponent() {
const [count, setCount] = useState(0);
const user = useMemo(
() => ({
name: "John",
age: 30,
}),
[]
);
return (
<View>
<Button title="Increment" onPress={() => setCount(count + 1)} />
<ChildComponent user={user} />
</View>
);
}
React.memo untuk Optimasi Komponen
function ExpensiveComponent({ data }) {
console.log("ExpensiveComponent rendered");
const processedData = data.map((item) => ({
...item,
computed: heavyComputation(item),
}));
return (
<View>
{processedData.map((item) => (
<Text key={item.id}>{item.name}</Text>
))}
</View>
);
}
const ExpensiveComponent = React.memo(({ data }) => {
console.log("ExpensiveComponent rendered");
const processedData = data.map((item) => ({
...item,
computed: heavyComputation(item),
}));
return (
<View>
{processedData.map((item) => (
<Text key={item.id}>{item.name}</Text>
))}
</View>
);
});
const ExpensiveComponent = React.memo(
({ data, userId }) => {
},
(prevProps, nextProps) => {
return (
prevProps.userId === nextProps.userId &&
prevProps.data.length === nextProps.data.length
);
}
);
useMemo dan useCallback Hooks
function ProductList({ products, category }) {
const filteredProducts = products.filter((p) => p.category === category);
const filteredProducts = useMemo(() => {
console.log("Filtering products...");
return products.filter((p) => p.category === category);
}, [products, category]);
return (
<FlatList
data={filteredProducts}
renderItem={({ item }) => <ProductItem product={item} />}
/>
);
}
function SearchScreen() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const handleSearch = (text) => {
setQuery(text);
};
const handleSearch = useCallback((text) => {
setQuery(text);
}, []);
const handleAddToCart = useCallback(
(productId) => {
addToCart(productId);
showNotification("Added to cart");
},
[addToCart, showNotification]
);
return (
<View>
<SearchInput onSearch={handleSearch} />
<ResultsList results={results} onAdd={handleAddToCart} />
</View>
);
}
function BadProductList({ products }) {
return (
<ScrollView>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</ScrollView>
);
}
function GoodProductList({ products }) {
const renderItem = ({ item }) => <ProductCard product={item} />;
return (
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
// Performance props
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
/>
);
}
function OptimizedProductList({ products }) {
const renderItem = useCallback(
({ item }) => <ProductCard product={item} />,
[]
);
const keyExtractor = useCallback((item) => item.id.toString(), []);
return (
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
);
}
Optimasi FlatList dengan Props Khusus
function OptimizedList({ data }) {
return (
<FlatList
data={data}
renderItem={({ item }) => <ListItem item={item} />}
keyExtractor={(item) => item.id}
// Optimization props
initialNumToRender={10} // Render 10 items pertama
maxToRenderPerBatch={5} // Render 5 items per batch
windowSize={5} // Viewport multiplier
removeClippedSubviews={true} // Unmount off-screen items
// Prevent re-render saat scroll
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
// Performance monitoring
onEndReachedThreshold={0.5} // Load more threshold
updateCellsBatchingPeriod={50} // Batch update interval
// Separator
ItemSeparatorComponent={() => (
<View style={{ height: 1, backgroundColor: "#ccc" }} />
)}
// Empty state
ListEmptyComponent={() => (
<Text style={{ textAlign: "center", padding: 20 }}>
No data available
</Text>
)}
/>
);
}
const ListItem = React.memo(({ item }) => {
return (
<View style={{ padding: 15, height: ITEM_HEIGHT }}>
<Text>{item.name}</Text>
<Text>{item.description}</Text>
</View>
);
});
const ITEM_HEIGHT = 80;
Image Optimization dan Caching
import { Image } from 'react-native';
import FastImage from 'react-native-fast-image';
<Image
source={{ uri: 'https://example.com/large-image.jpg' }}
style={{ width: 100, height: 100 }}
/>
<Image
source={{ uri: 'https://example.com/image-100x100.jpg' }}
style={{ width: 100, height: 100 }}
resizeMode="cover"
/>
import FastImage from 'react-native-fast-image';
<FastImage
style={{ width: 100, height: 100 }}
source={{
uri: 'https://example.com/image.jpg',
priority: FastImage.priority.high,
cache: FastImage.cacheControl.immutable,
}}
resizeMode={FastImage.resizeMode.cover}
/>
FastImage.preload([
{
uri: 'https://example.com/image1.jpg',
priority: FastImage.priority.high,
},
{
uri: 'https://example.com/image2.jpg',
},
]);
FastImage.clearMemoryCache();
FastImage.clearDiskCache();
function ProgressiveImage({ source, style }) {
const [loading, setLoading] = useState(true);
return (
<View>
{loading && (
<ActivityIndicator style={StyleSheet.absoluteFill} />
)}
<Image
source={source}
style={style}
onLoadEnd={() => setLoading(false)}
defaultSource={require('./placeholder.png')}
/>
</View>
);
}
Lazy Loading dan Code Splitting
import React, { lazy, Suspense } from "react";
import { ActivityIndicator } from "react-native";
import HomeScreen from "./screens/HomeScreen";
import ProfileScreen from "./screens/ProfileScreen";
import SettingsScreen from "./screens/SettingsScreen";
const HomeScreen = lazy(() => import("./screens/HomeScreen"));
const ProfileScreen = lazy(() => import("./screens/ProfileScreen"));
const SettingsScreen = lazy(() => import("./screens/SettingsScreen"));
function App() {
return (
<Suspense fallback={<ActivityIndicator />}>
<Navigation />
</Suspense>
);
}
const HeavyChart = lazy(() => import("./components/HeavyChart"));
function DashboardScreen() {
const [showChart, setShowChart] = useState(false);
return (
<View>
<Button title="Show Chart" onPress={() => setShowChart(true)} />
{showChart && (
<Suspense fallback={<ActivityIndicator />}>
<HeavyChart data={chartData} />
</Suspense>
)}
</View>
);
}
async function loadFeature(featureName) {
if (featureName === "analytics") {
const module = await import("./features/Analytics");
return module.default;
}
}
Mengurangi Bundle Size Aplikasi
import _ from 'lodash';
const result = _.uniq(array);
import uniq from 'lodash/uniq';
const result = uniq(array);
import * as icons from 'react-native-vector-icons';
import Icon from 'react-native-vector-icons/MaterialIcons';
{
"expo": {
"jsEngine": "hermes",
"android": {
"enableProguard": true
}
}
}
export const API_URL = 'https://api.example.com';
export const COLORS = { primary: '#007AFF' };
export const formatCurrency = (amount) => `$${amount.toFixed(2)}`;
Async Storage Best Practices
import AsyncStorage from "@react-native-async-storage/async-storage";
export const storage = {
async set(key, value) {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
} catch (error) {
console.error("Error saving data:", error);
}
},
async get(key) {
try {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error("Error reading data:", error);
return null;
}
},
async remove(key) {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error("Error removing data:", error);
}
},
async clear() {
try {
await AsyncStorage.clear();
} catch (error) {
console.error("Error clearing storage:", error);
}
},
};
async function saveMultipleData(data) {
const pairs = Object.entries(data).map(([key, value]) => [
key,
JSON.stringify(value),
]);
try {
await AsyncStorage.multiSet(pairs);
} catch (error) {
console.error("Error saving multiple data:", error);
}
}
const cache = {
async set(key, value, expirationMinutes = 60) {
const item = {
value,
expiry: Date.now() + expirationMinutes * 60 * 1000,
};
await storage.set(key, item);
},
async get(key) {
const item = await storage.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
await storage.remove(key);
return null;
}
return item.value;
},
};
await cache.set("user_data", userData, 30);
const cachedData = await cache.get("user_data");
Memory Leaks dan Cara Mencegahnya
function BadComponent() {
useEffect(() => {
setInterval(() => {
console.log("Running...");
}, 1000);
}, []);
}
function GoodComponent() {
useEffect(() => {
const intervalId = setInterval(() => {
console.log("Running...");
}, 1000);
return () => clearInterval(intervalId);
}, []);
}
function BadComponent() {
useEffect(() => {
const subscription = Notifications.addListener(handleNotification);
}, []);
}
function GoodComponent() {
useEffect(() => {
const subscription = Notifications.addListener(handleNotification);
return () => subscription.remove();
}, []);
}
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then((result) => {
setData(result);
});
}, []);
}
function GoodComponent() {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true;
fetchData().then((result) => {
if (isMounted) {
setData(result);
}
});
return () => {
isMounted = false;
};
}, []);
}
function BetterComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const abortController = new AbortController();
fetch("https://api.example.com/data", {
signal: abortController.signal,
})
.then((res) => res.json())
.then(setData)
.catch((error) => {
if (error.name !== "AbortError") {
console.error("Fetch error:", error);
}
});
return () => abortController.abort();
}, []);
}
import { PerformanceObserver, performance } from "react-native-performance";
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
observer.observe({ entryTypes: ["measure", "mark"] });
function HomeScreen({ navigation }) {
useEffect(() => {
performance.mark("home-screen-mount");
return () => {
performance.mark("home-screen-unmount");
performance.measure(
"home-screen-duration",
"home-screen-mount",
"home-screen-unmount"
);
};
}, []);
const handleNavigate = () => {
performance.mark("navigation-start");
navigation.navigate("Details");
performance.mark("navigation-end");
performance.measure(
"navigation-time",
"navigation-start",
"navigation-end"
);
};
return <Button title="Go to Details" onPress={handleNavigate} />;
}
async function fetchWithPerformance(url) {
performance.mark(`fetch-${url}-start`);
try {
const response = await fetch(url);
const data = await response.json();
performance.mark(`fetch-${url}-end`);
performance.measure(
`fetch-${url}`,
`fetch-${url}-start`,
`fetch-${url}-end`
);
return data;
} catch (error) {
console.error("Fetch error:", error);
}
}
const metrics = {
startTime: null,
startMeasure(name) {
this.startTime = performance.now();
performance.mark(`${name}-start`);
},
endMeasure(name) {
performance.mark(`${name}-end`);
const duration = performance.now() - this.startTime;
performance.measure(name, `${name}-start`, `${name}-end`);
return duration;
},
};
metrics.startMeasure("heavy-computation");
const result = performHeavyComputation();
const duration = metrics.endMeasure("heavy-computation");
console.log(`Computation took ${duration}ms`);
import { Profiler } from "react";
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
function ProductList({ products }) {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<FlatList
data={products}
renderItem={({ item }) => (
<Profiler id={`Product-${item.id}`} onRender={onRenderCallback}>
<ProductCard product={item} />
</Profiler>
)}
/>
</Profiler>
);
}
function usePerformanceTracking(componentName) {
const renderCount = useRef(0);
const startTime = useRef(performance.now());
useEffect(() => {
renderCount.current += 1;
const duration = performance.now() - startTime.current;
console.log(
`${componentName} render #${renderCount.current}: ${duration}ms`
);
startTime.current = performance.now();
});
}
function MyComponent() {
usePerformanceTracking("MyComponent");
return <View>{/* component content */}</View>;
}
<FlatList
data={items}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handlePress(item)}>
<Text>{item.name}</Text>
</TouchableOpacity>
)}
/>;
const renderItem = useCallback(
({ item }) => <ItemComponent item={item} onPress={handlePress} />,
[handlePress]
);
function ProductList({ products }) {
const sortedProducts = products
.sort((a, b) => a.price - b.price)
.filter((p) => p.inStock);
}
const sortedProducts = useMemo(() => {
return products.sort((a, b) => a.price - b.price).filter((p) => p.inStock);
}, [products]);
function Counter() {
const [count, setCount] = useState(0);
setInterval(() => {
setCount(count + 1);
}, 1000);
}
useEffect(() => {
const id = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
const [data, setData] = useState({
users: [],
products: [],
orders: [],
});
const [users, setUsers] = useState([]);
const [products, setProducts] = useState([]);
const [orders, setOrders] = useState([]);
function AppProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
Best Practices Ringkasan
const MyComponent = React.memo(({ data }) => {
});
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={getItemLayout}
/>
<FastImage
source={{ uri: imageUrl, priority: FastImage.priority.high }}
resizeMode={FastImage.resizeMode.cover}
/>
const LazyScreen = lazy(() => import('./screens/LazyScreen'));
<ErrorBoundary>
<App />
</ErrorBoundary>
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
Checklist Optimasi Sebelum Release
Checklist optimasi aplikasi:
Performance
- [ ] FlatList gunakan props optimasi (initialNumToRender, windowSize)
- [ ] Component berat di-wrap dengan React.memo
- [ ] useMemo/useCallback untuk expensive operations
- [ ] Images dioptimasi dan di-cache
- [ ] Bundle size < 20MB
- [ ] App startup time < 3 detik
- [ ] Smooth scrolling (60 FPS)
Code Quality
- [ ] No console.log di production
- [ ] Error boundaries implemented
- [ ] All timers/listeners cleaned up
- [ ] No memory leaks
- [ ] Proper error handling
User Experience
- [ ] Loading states untuk async operations
- [ ] Error messages user-friendly
- [ ] Offline handling
- [ ] Pull to refresh di lists
- [ ] Empty states implemented
Testing
- [ ] Test di berbagai devices
- [ ] Test dengan slow network
- [ ] Test dengan low memory devices
- [ ] Test orientasi landscape/portrait
if (__DEV__) {
console.log('Development mode - all features enabled');
} else {
console.log = () => {};
console.warn = () => {};
console.error = () => {};
}
{
"expo": {
"jsEngine": "hermes",
"android": {
"enableProguard": true,
"enableShrinkResources": true
}
}
}
Studi Kasus: Before & After Optimization
function SlowProductList({ products }) {
return (
<ScrollView>
{products.map((product) => {
// Heavy computation di setiap render
const discount = calculateDiscount(product);
const rating = calculateAverageRating(product.reviews);
return (
<View key={product.id} style={styles.card}>
<Image
source={{ uri: product.imageUrl }}
style={{ width: 300, height: 300 }}
/>
<Text>{product.name}</Text>
<Text>Price: ${product.price}</Text>
<Text>Discount: {discount}%</Text>
<Text>Rating: {rating}/5</Text>
<TouchableOpacity onPress={() => addToCart(product)}>
<Text>Add to Cart</Text>
</TouchableOpacity>
</View>
);
})}
</ScrollView>
);
}
const ProductCard = React.memo(({ product, onAddToCart }) => {
const discount = useMemo(
() => calculateDiscount(product),
[product.price, product.originalPrice]
);
const rating = useMemo(
() => calculateAverageRating(product.reviews),
[product.reviews]
);
return (
<View style={styles.card}>
<FastImage
source={{
uri: product.thumbnailUrl, // 100x100 thumbnail
priority: FastImage.priority.normal,
}}
style={{ width: 100, height: 100 }}
resizeMode={FastImage.resizeMode.cover}
/>
<Text>{product.name}</Text>
<Text>Price: ${product.price}</Text>
<Text>Discount: {discount}%</Text>
<Text>Rating: {rating}/5</Text>
<TouchableOpacity onPress={() => onAddToCart(product.id)}>
<Text>Add to Cart</Text>
</TouchableOpacity>
</View>
);
});
function FastProductList({ products }) {
const handleAddToCart = useCallback((productId) => {
addToCart(productId);
}, []);
const renderItem = useCallback(
({ item }) => <ProductCard product={item} onAddToCart={handleAddToCart} />,
[handleAddToCart]
);
const keyExtractor = useCallback((item) => item.id.toString(), []);
return (
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={keyExtractor}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
);
}
const ITEM_HEIGHT = 120;