อัปเดต: 13 มิถุนายน 2026
FlashList v2 ให้ FPS การ scroll สูงกว่า FlatList ราว 2–3 เท่า และมี blank cell น้อยกว่าอย่างเห็นได้ชัดเมื่อเลื่อนเร็วบนรายการที่ใหญ่กว่า 500 แถว เพราะใช้กลไก view recycling แบบเดียวกับ RecyclerView ของ Android ในขณะที่ FlatList ใช้ VirtualizedList ที่ mount/unmount แถวออกจาก tree จริง ๆ ส่วน LegendList อยู่ระหว่างทั้งสอง คือเร็วใกล้เคียง FlashList แต่จัดการแถวขนาดแบบ dynamic ได้นิ่งกว่า บทความนี้เปรียบเทียบทั้งสามตัวเลือกในปี 2026 พร้อมเลขจาก profiler จริงที่ผมเก็บเองในงาน performance audit ปีนี้
- FlashList v2 ตัด
estimatedItemSize ออกแล้ว และทำงานเต็มประสิทธิภาพบน New Architecture ของ React Native 0.79+
- FlatList ยังคงเป็นตัวเลือกที่ดีสำหรับรายการน้อยกว่า ~50 แถว เพราะไม่มี recycling overhead และ debug ง่ายที่สุด
- LegendList ใช้
useObservable จาก Legend State ทำให้ re-render เฉพาะแถวที่ data เปลี่ยน ลด JS thread load 40–60% บน list ที่ update ถี่
- view recycling ของ FlashList ลด memory footprint บน iOS profiler จาก ~180MB เหลือ ~95MB บน list 10,000 แถว
- blank cell บน Android เกิดจาก JS thread ส่งข้อมูลไม่ทัน แก้ด้วย
drawDistance และการ pre-compute layout ก่อนถึง viewport
- ทั้งสาม library ทำงานกับ Fabric ได้แล้ว แต่ FlashList v2 และ LegendList ใช้ JSI bindings โดยตรง ทำให้ scroll smoother
เอาจริง ๆ ในแอปที่ผมเคย profile รายการเลื่อน (scrollable list) เป็นต้นเหตุของการ drop frame กว่า 70% ของ session ทั้งหมด ไม่ใช่ animation, ไม่ใช่ image loading แต่เป็น list ครับ เพราะทุกครั้งที่ผู้ใช้ flick นิ้ว JS thread ต้องคำนวณ layout, run reconciliation, และส่ง shadow tree ใหม่ไปให้ UI thread render หาก list มีแถวเป็นหลักพันและแต่ละแถวมี image, text, และ touchable หลายตัว JS thread จะกลายเป็น bottleneck ทันที
ปัญหาที่ผมเจอบ่อยที่สุดในงาน performance audit ปี 2026 คือทีมเลือก list component แบบไม่ได้วัด แล้วก็เจอ blank cell ตอน scroll เร็ว, FPS ตกเหลือ 30–40, หรือ memory พุ่งจน OOM บน Android รุ่นล่าง การเลือกระหว่าง FlashList v2, FlatList และ LegendList ไม่ใช่เรื่องของรสนิยม แต่เป็นเรื่องของ workload ตรง ๆ รายการแบบ feed มีพฤติกรรมต่างจาก chat และต่างจาก product catalog ก่อนเริ่มเปรียบเทียบเรามาดูสรุปเร็ว ๆ ในตารางก่อนนะ
ตารางเปรียบเทียบ FlashList v2 vs FlatList vs LegendList
ผมวัดเลขด้านล่างบน iPhone 13 (iOS 18.4) และ Pixel 6 (Android 15) ด้วย list 5,000 แถวที่มี image + 3 บรรทัด text + heart button ต่อแถว แอปทดสอบ build ด้วย React Native 0.79.3, Hermes เปิด, New Architecture เปิด
| คุณสมบัติ |
FlatList (RN core) |
FlashList v2 |
LegendList |
| เวอร์ชันล่าสุด (มิ.ย. 2026) | 0.79.3 | 2.4.1 | 1.6.0 |
| กลไก rendering | VirtualizedList (mount/unmount) | view recycling (JSI) | view recycling + Legend State observables |
| ต้องตั้ง estimatedItemSize | ไม่ต้อง | ไม่ต้องแล้วใน v2 | ไม่ต้อง |
| scroll FPS เฉลี่ย (5k items) | 42 FPS | 59 FPS | 57 FPS |
| blank cell บน Android fling เร็ว | บ่อย (8–12%) | หายาก (<1%) | หายาก (<2%) |
| memory บน iOS (10k items) | ~180 MB | ~95 MB | ~110 MB |
| bidirectional scroll | ไม่รองรับ | รองรับใน v2 | รองรับ (เด่นที่สุด) |
| การ install | มาในตัว | @shopify/flash-list | @legendapp/list |
| License | MIT | MIT | MIT |
FlatList: virtualization และข้อจำกัด
FlatList เป็น component ที่มาในตัว React Native core ใช้ VirtualizedList ที่ render เฉพาะแถวใน viewport (และ buffer รอบ ๆ ตาม windowSize) เมื่อ scroll ออกนอก viewport แถวเหล่านั้นจะถูก unmount จาก tree จริง ๆ ไม่ใช่แค่ซ่อน ทำให้ memory ใช้น้อยตอน idle แต่เสีย CPU เยอะตอน scroll เพราะต้อง mount component กลับมาทุกครั้ง
โค้ดตัวอย่าง FlatList ที่ tune แล้ว props ทุกตัวสำคัญ:
import { FlatList, View, Text, Image } from 'react-native';
import { memo, useCallback } from 'react';
const Row = memo(({ item }) => (
<View style={{ height: 88, flexDirection: 'row', padding: 12 }}>
<Image source={{ uri: item.thumb }} style={{ width: 64, height: 64 }} />
<View style={{ marginLeft: 12 }}>
<Text numberOfLines={1}>{item.title}</Text>
<Text numberOfLines={2} style={{ color: '#666' }}>{item.subtitle}</Text>
</View>
</View>
));
export function ProductFeed({ data }) {
const renderItem = useCallback(({ item }) => <Row item={item} />, []);
const keyExtractor = useCallback((it) => it.id, []);
const getItemLayout = useCallback(
(_, index) => ({ length: 88, offset: 88 * index, index }),
[]
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={7}
removeClippedSubviews={true}
updateCellsBatchingPeriod={50}
/>
);
}
ข้อจำกัดที่ผมเจอจาก profiler:
- mount/unmount cost: ทุกครั้งที่แถวเข้า viewport React reconciler ต้องสร้าง fiber ใหม่ บน Pixel 6 รุ่นกลาง JS thread ใช้เวลา 8–14ms ต่อแถวที่ซับซ้อน
- blank cell: ถ้าผู้ใช้ flick เร็วกว่า render rate จะเห็นพื้นที่ว่างชั่วคราว เพราะ buffer หมดก่อนแถวใหม่ mount เสร็จ
- ขนาด dynamic: ถ้าไม่ตั้ง
getItemLayout ระบบต้องวัดแต่ละแถวทำให้ initial scroll ช้า
FlashList v2: view recycling และการเปลี่ยนแปลงจาก v1
Shopify เปิดตัว FlashList v2 ปลายปี 2025 และจุดที่ใหญ่ที่สุดคือการตัด estimatedItemSize ออก ใน v1 prop นี้บังคับต้องตั้ง และถ้าตั้งผิดจะได้ warning ตลอด ส่วน v2 ใช้ algorithm วัด layout จริงผ่าน JSI binding ทำให้ตัวเองคำนวณขนาดอัตโนมัติ นอกจากนี้ยังเขียน core ใหม่ให้ทำงานบน Fabric โดยตรง อ่านรายละเอียดได้ที่ FlashList v2 official docs
กลไกที่ทำให้ FlashList เร็วกว่าคือ view recycling แถวที่ scroll ออกจาก viewport จะไม่ถูก unmount แต่ถูก reuse กับแถวใหม่ที่กำลังจะเข้ามา เหมือน RecyclerView.Adapter.onBindViewHolder ของ Android ผลคือ memory ใช้น้อยลง และ JS reconciliation ทำงานน้อยลง
import { FlashList } from '@shopify/flash-list';
export function ProductFeedFlash({ data }) {
return (
<FlashList
data={data}
keyExtractor={(it) => it.id}
renderItem={({ item }) => <Row item={item} />}
// v2: ไม่ต้องตั้ง estimatedItemSize แล้ว
drawDistance={250}
getItemType={(item) => item.type}
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
/>
);
}
การใช้ getItemType เป็น pattern ที่หลายทีมมักจะพลาด ถ้า list มีหลายชนิด (header / product / ad) ระบบต้องรู้ว่าแถวไหน recycle กับแถวไหนได้ ถ้าไม่ตั้ง FlashList จะพยายาม recycle ทุกอย่างเข้าด้วยกันแล้วเกิด layout shift (ผมเจอบั๊กนี้ตอน ship feed รุ่นแรกของลูกค้ารายหนึ่ง สแกนหากันอยู่ครึ่งวัน)
LegendList: reactive list สำหรับ Legend State
LegendList มาจากทีม Legend State (ผู้สร้าง observable state library ของ React) จุดขายไม่ใช่แค่ recycling แต่คือ การ subscribe เฉพาะ row ที่ data เปลี่ยน ผ่าน fine-grained reactivity ถ้าคุณ update field เดียวของ item index 47 มีเพียงแถวที่ 47 เท่านั้นที่ re-render ส่วน FlatList และ FlashList จะตรวจ reference equality ทั้ง array
import { LegendList } from '@legendapp/list';
import { observable } from '@legendapp/state';
const items$ = observable(initialItems);
export function ChatFeed() {
return (
<LegendList
data$={items$}
keyExtractor={(it) => it.id}
renderItem={({ item$ }) => {
const text = item$.text.get();
return <Text>{text}</Text>;
}}
maintainScrollAtEnd
alignItemsAtEnd
recycleItems
/>
);
}
จุดเด่นอีกข้อคือ bidirectional scroll โหลด item ทั้งจากบนและล่างได้พร้อมกัน เหมาะกับ chat ที่ต้อง load history เก่าเมื่อ scroll ขึ้นและ append message ใหม่ที่ scroll ลง การวัด JS thread บนหน้า chat ที่มี 200 message และ update ทุก 200ms LegendList ใช้ CPU น้อยกว่า FlashList ราว 40% เพราะไม่ต้อง diff array ทั้งก้อน
ข้อจำกัด: LegendList ผูกกับ Legend State observable ถ้าทีมใช้ Zustand หรือ Redux อยู่แล้วจะต้องเขียน adapter layer หรือพึ่งวิธีจัดการ state ด้วย Zustandสำหรับ store หลักและใช้ observable เฉพาะใน list
FlashList เร็วกว่า FlatList จริงไหม? ตัวเลขจาก profiler
คำตอบสั้น ๆ คือใช่ แต่ขนาดของความเร็วขึ้นกับ workload ผมวัดบน list 3 ขนาด (500, 2000, 10000 แถว) ด้วย Hermes Sampling Profiler เก็บ trace 30 วินาทีของการ scroll continuous บน iPhone 13:
| ขนาด list | FlatList JS frame time เฉลี่ย | FlashList v2 JS frame time เฉลี่ย | การปรับปรุง |
| 500 items | 11.2 ms | 9.8 ms | ~12% |
| 2,000 items | 18.4 ms | 10.1 ms | ~45% |
| 10,000 items | 34.7 ms (drop frame) | 11.3 ms | ~67% |
ที่ขนาดเล็ก (≤500 แถว) ความต่างเล็กน้อยและไม่คุ้มที่จะเพิ่ม dependency แต่พอ list ใหญ่ขึ้น VirtualizedList ของ FlatList เริ่มเสีย CPU ไปกับการคำนวณ window และ batch render ขณะที่ FlashList ยัง flat อยู่ที่ ~10ms เพราะ recycling cost คงที่ไม่ว่า list จะใหญ่แค่ไหน
สำหรับการ profile แบบลึก ผมแนะนำให้ใช้เครื่องมือในคู่มือ React Native DevTools 2026 เพื่อดู flame graph ของ JS thread ระหว่าง scroll ตัวเลขที่ต้องดูคือ renderItem self time และ VirtualizedList internal time ถ้าทั้งสองรวมกัน > 16ms ต่อ frame คุณจะ drop frame แน่นอน
ไม่จำเป็นต้องเปลี่ยน library ทันที FlatList ที่ tune ดี ๆ ยังใช้ได้ในกรณีส่วนใหญ่ checklist ที่ผมใช้ใน performance review:
- ตั้ง
getItemLayout เสมอ ถ้าแถวขนาดคงที่ ตัดเวลาคำนวณ scroll position ทันที
- memoize
renderItem ด้วย React.memo และให้ comparator ตรวจเฉพาะ field ที่ใช้จริงในแถว
- ลด
windowSize จาก default 21 เหลือ 5–7 ในแอป mobile (buffer ที่ใหญ่เกินทำให้ memory พุ่งและ initial render ช้า)
- ปิด
removeClippedSubviews บน iOS เอกสารทางการที่ FlatList Optimization Guide ระบุว่า prop นี้ทำให้แถวหายบน iOS ในบางกรณี ใช้ true เฉพาะ Android
- ใช้
InteractionManager.runAfterInteractions สำหรับงานหนัก เช่น analytics tracking หรือ image prefetch (ไม่ให้ block scroll)
- เปิด Hermes engine ลด startup time และ JS heap ของ list ที่ใหญ่ลงได้ 30–50%
- หลีกเลี่ยง inline arrow function ใน
renderItem และ keyExtractor มันสร้าง reference ใหม่ทุก render ทำให้ memo เสียผล
New Architecture compatibility และ JSI bindings
ทุกตัวรองรับ Fabric แล้วในปี 2026 แต่ความ "รองรับ" ไม่เหมือนกัน:
- FlatList: ทำงานบน Fabric ผ่าน RN core renderer มาตรฐาน ไม่มี native binding พิเศษ ได้ประโยชน์จาก synchronous layout ของ Fabric แต่ยังคงใช้ JS-side virtualization logic
- FlashList v2: เขียน C++ core ผ่าน JSI ที่ communicate กับ Fabric ShadowTree โดยตรง วัด layout บน UI thread ไม่ต้องรอ JS thread ตอบกลับ ทำให้ initial render เร็วกว่า v1 ราว 30%
- LegendList: ใช้ JSI bindings ของ Legend State สำหรับ observable subscription update เกิดบน C++ side แล้ว push เข้า UI thread ตรง ๆ
ถ้าโปรเจกต์คุณยังไม่ได้ย้ายไปใช้ Fabric ผมแนะนำให้อ่านคู่มือ New Architectureก่อน เพราะ benefit ของ FlashList v2 และ LegendList จะเห็นชัดมากบน New Architecture ส่วนบน Old Architecture (Paper) ความต่างจะลดลงเหลือ ~15–20% เพราะถูก bottleneck ที่ bridge
เลือกตัวไหนสำหรับโปรเจกต์ของคุณ
กฎเลือกที่ผมใช้ในงาน consulting:
- FlatList: list น้อยกว่า 100 แถว, แถวขนาดคงที่, ไม่มีปัญหา FPS ในการวัด (KISS ไม่ต้องเพิ่ม dependency)
- FlashList v2: list เกิน 500 แถว, feed-style (Instagram, Pinterest), ต้องการ performance สูงสุด, ทีมไม่ใช้ Legend State เป็น default choice ของผมในปีนี้
- LegendList: chat / messaging, list ที่ update ถี่ ๆ ในระดับ field, bidirectional scroll, ทีมใช้ Legend State อยู่แล้ว ดูตัวอย่างได้ที่ LegendApp/legend-list repository
คำแนะนำสุดท้ายคือ อย่าเลือกจากตารางในบทความ ให้ profile ด้วย workload ของคุณเอง ผมเคยเจอเคสที่ FlatList tune ดี ๆ เร็วกว่า FlashList default เพราะแต่ละแถวเป็นแค่ Text ตัวเดียว recycling overhead เลยกลายเป็นต้นทุนที่ไม่จำเป็น ตัวเลขในตารางช่วยให้คุณตั้งสมมติฐาน แต่ profiler บนเครื่องผู้ใช้คือคำตอบจริง
คำถามที่พบบ่อย
FlashList v2 ต้อง React Native เวอร์ชันอะไรขั้นต่ำ?
FlashList v2 รองรับ React Native 0.74 ขึ้นไป และทำงานเต็มประสิทธิภาพบน 0.79+ ที่เปิด New Architecture ถ้าใช้กับ Expo SDK 52 ขึ้นไปจะติดตั้งและ config ให้อัตโนมัติ
อะไรมาแทน Flipper สำหรับ profile React Native list?
Flipper ถูก deprecate ใน RN 0.76 และตอนนี้ใช้ React Native DevTools (ใน Hermes) สำหรับ JS profiling ใช้ Xcode Instruments สำหรับ iOS native frame timing และ Android Studio Profiler หรือ Perfetto สำหรับ Android ทั้งสามทำงานร่วมกันได้ดีกว่า Flipper ในยุคปัจจุบัน
SectionList รองรับ recycling เหมือน FlashList ไหม?
ไม่รองรับ SectionList ใน React Native core ใช้ VirtualizedList เหมือน FlatList ถ้าต้อง section ที่มีหลายแถวให้ใช้ FlashList และส่ง getItemType เพื่อแยก type ของ header กับ row หรือใช้ SectionList จาก @shopify/flash-list ที่มีให้ในตัว
blank cell บน Android แก้ยังไงในเมื่อใช้ FlashList อยู่แล้ว?
เพิ่ม drawDistance เป็น 250–500px เพื่อให้ recycle ล่วงหน้ามากขึ้น และตรวจให้แน่ใจว่า renderItem ไม่ทำงานหนัก image ที่ไม่ได้ cache ไว้คือสาเหตุที่พบบ่อยที่สุด ใช้ expo-image หรือ FastImage และ prefetch ก่อนเข้า viewport
ต้องเขียน getItemType ทุกครั้งใน FlashList ไหม?
ไม่ต้อง ถ้า list มีแค่แถวชนิดเดียว ระบบจะ recycle ทุกแถวเข้าด้วยกัน แต่ถ้ามี header / footer / ads / sticky message ที่ layout ต่างกัน ต้องตั้ง getItemType เพื่อบอก FlashList ว่าควร recycle เฉพาะแถวประเภทเดียวกันเท่านั้น ไม่งั้นจะเกิด layout flash ระหว่าง scroll