diff --git a/src/components/mobile/LessonCarousel.tsx b/src/components/mobile/LessonCarousel.tsx index af37177..e2328db 100644 --- a/src/components/mobile/LessonCarousel.tsx +++ b/src/components/mobile/LessonCarousel.tsx @@ -3,6 +3,7 @@ import { View, Text, ScrollView, + FlatList, Dimensions, StyleSheet, TouchableOpacity, @@ -47,7 +48,7 @@ export default function LessonCarousel({ onLastLessonNext, isLastLessonInSection = false, }: LessonCarouselProps) { - const scrollViewRef = useRef(null); + const scrollViewRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(0); const progressBarWidth = useRef(new Animated.Value(0)).current; @@ -78,8 +79,8 @@ export default function LessonCarousel({ const scrollToIndex = (index: number, animated = true) => { if (scrollViewRef.current) { - scrollViewRef.current.scrollTo({ - x: index * SCREEN_WIDTH, + scrollViewRef.current.scrollToOffset({ + offset: index * SCREEN_WIDTH, animated, }); } @@ -194,8 +195,10 @@ export default function LessonCarousel({ {/* Swipeable Content */} - item.id} horizontal pagingEnabled showsHorizontalScrollIndicator={false} @@ -206,9 +209,13 @@ export default function LessonCarousel({ snapToInterval={SCREEN_WIDTH} snapToAlignment="center" contentContainerStyle={styles.scrollContent} - > - {lessons.map((lesson, index) => ( - + getItemLayout={(_, index) => ({ length: SCREEN_WIDTH, offset: SCREEN_WIDTH * index, index })} + maxToRenderPerBatch={10} + windowSize={5} + initialNumToRender={2} + removeClippedSubviews={true} + renderItem={({ item: lesson }) => ( + - ))} - + )} + /> {/* Navigation Buttons */} diff --git a/src/components/mobile/MobileQuizManager/QuizCarousel.tsx b/src/components/mobile/MobileQuizManager/QuizCarousel.tsx index 0c6f403..7a4c1b5 100644 --- a/src/components/mobile/MobileQuizManager/QuizCarousel.tsx +++ b/src/components/mobile/MobileQuizManager/QuizCarousel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { Dimensions, ScrollView, StyleSheet, View } from 'react-native'; +import { Dimensions, ScrollView, FlatList, StyleSheet, View } from 'react-native'; import { Question } from '../../../types/course'; import MobileQuestionCard from './MobileQuestionCard'; @@ -25,15 +25,15 @@ export default function QuizCarousel({ onQuestionChange, onAnswerSelect, }: QuizCarouselProps) { - const scrollViewRef = useRef(null); + const scrollViewRef = useRef(null); const isScrollingRef = useRef(false); const scrollTimeoutRef = useRef(null); useEffect(() => { // Only scroll if not currently being scrolled by user if (scrollViewRef.current && !isScrollingRef.current) { - scrollViewRef.current.scrollTo({ - x: currentQuestionIndex * SCREEN_WIDTH, + scrollViewRef.current.scrollToOffset({ + offset: currentQuestionIndex * SCREEN_WIDTH, animated: true, }); } @@ -84,8 +84,10 @@ export default function QuizCarousel({ return ( - question.id} horizontal pagingEnabled showsHorizontalScrollIndicator={false} @@ -99,9 +101,13 @@ export default function QuizCarousel({ snapToAlignment="center" contentContainerStyle={styles.scrollContent} bounces={false} - > - {questions.map((question, index) => ( - + getItemLayout={(_, index) => ({ length: SCREEN_WIDTH, offset: SCREEN_WIDTH * index, index })} + maxToRenderPerBatch={10} + windowSize={5} + initialNumToRender={2} + removeClippedSubviews={true} + renderItem={({ item: question, index }) => ( + - ))} - + )} + /> ); } diff --git a/src/components/mobile/MobileSearch.tsx b/src/components/mobile/MobileSearch.tsx index e2acbca..d9629e6 100644 --- a/src/components/mobile/MobileSearch.tsx +++ b/src/components/mobile/MobileSearch.tsx @@ -268,6 +268,11 @@ export const MobileSearch = ({ ListEmptyComponent={ Try a different query or adjust filters. } + getItemLayout={(_, index) => ({ length: 120, offset: 120 * index, index })} + maxToRenderPerBatch={15} + windowSize={5} + initialNumToRender={10} + removeClippedSubviews={true} /> )} diff --git a/src/components/mobile/MobileSyllabus.tsx b/src/components/mobile/MobileSyllabus.tsx index 146b3c6..4f2fb93 100644 --- a/src/components/mobile/MobileSyllabus.tsx +++ b/src/components/mobile/MobileSyllabus.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Platform, - ScrollView, + FlatList, StyleSheet, Text, TouchableOpacity, @@ -80,27 +80,31 @@ export default function MobileSyllabus({ }; return ( - section.id} style={styles.container} contentContainerStyle={styles.contentContainer} showsVerticalScrollIndicator={false} - > - {/* Header */} - - 📚 Course Syllabus - - {sections.length} sections • {sections.reduce((acc, s) => acc + s.lessons.length, 0)}{' '} - lessons - - - - {/* Sections */} - {sections.map(section => { + maxToRenderPerBatch={10} + windowSize={5} + initialNumToRender={10} + removeClippedSubviews={true} + ListHeaderComponent={ + + 📚 Course Syllabus + + {sections.length} sections • {sections.reduce((acc, s) => acc + s.lessons.length, 0)}{' '} + lessons + + + } + renderItem={({ item: section }) => { const isExpanded = expandedSections.has(section.id); const sectionProgress = getSectionProgress(section); return ( - + {/* Section Header */} toggleSection(section.id)} @@ -207,8 +211,8 @@ export default function MobileSyllabus({ )} ); - })} - + }} + /> ); } diff --git a/tests/components/Performance.test.tsx b/tests/components/Performance.test.tsx new file mode 100644 index 0000000..d1eb787 --- /dev/null +++ b/tests/components/Performance.test.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import MobileSyllabus from '../../src/components/mobile/MobileSyllabus'; +import QuizCarousel from '../../src/components/mobile/MobileQuizManager/QuizCarousel'; +import LessonCarousel from '../../src/components/mobile/LessonCarousel'; +import { MobileSearch } from '../../src/components/mobile/MobileSearch'; + +// Mock course data for tests +const mockSections = [ + { + id: 's1', + title: 'Section 1', + lessons: Array.from({ length: 100 }).map((_, i) => ({ + id: `l${i}`, + title: `Lesson ${i}`, + content: 'Content', + duration: 10, + })), + }, +]; + +const mockQuestions = Array.from({ length: 100 }).map((_, i) => ({ + id: `q${i}`, + text: `Question ${i}`, + type: 'multiple-choice' as const, + options: ['A', 'B', 'C', 'D'], + correctAnswer: 'A', +})); + +describe('List Performance Optimization', () => { + it('MobileSyllabus uses FlatList with proper optimization props', () => { + const { getByType } = render( + {}} + /> + ); + const flatList = getByType('FlatList' as any); + expect(flatList.props.maxToRenderPerBatch).toBe(10); + expect(flatList.props.windowSize).toBe(5); + expect(flatList.props.initialNumToRender).toBe(10); + expect(flatList.props.removeClippedSubviews).toBe(true); + }); + + it('QuizCarousel uses FlatList with proper optimization props', () => { + const { getByType } = render( + {}} + onAnswerSelect={() => {}} + /> + ); + const flatList = getByType('FlatList' as any); + expect(flatList.props.maxToRenderPerBatch).toBe(10); + expect(flatList.props.windowSize).toBe(5); + expect(flatList.props.initialNumToRender).toBe(2); + expect(flatList.props.removeClippedSubviews).toBe(true); + expect(flatList.props.getItemLayout).toBeDefined(); + }); + + it('LessonCarousel uses FlatList with proper optimization props', () => { + const { getByType } = render( + {}} + renderLessonContent={() => <>} + /> + ); + const flatList = getByType('FlatList' as any); + expect(flatList.props.maxToRenderPerBatch).toBe(10); + expect(flatList.props.windowSize).toBe(5); + expect(flatList.props.initialNumToRender).toBe(2); + expect(flatList.props.removeClippedSubviews).toBe(true); + expect(flatList.props.getItemLayout).toBeDefined(); + }); + + it('MobileSearch uses FlatList with proper optimization props', () => { + const { getByType } = render(); + const flatList = getByType('FlatList' as any); + expect(flatList.props.maxToRenderPerBatch).toBe(15); + expect(flatList.props.windowSize).toBe(5); + expect(flatList.props.initialNumToRender).toBe(10); + expect(flatList.props.removeClippedSubviews).toBe(true); + expect(flatList.props.getItemLayout).toBeDefined(); + }); +});