React.memo, useCallback, useMemoFlatList với các prop quan trọng và biết khi nào dùng FlashListInteractionManager.runAfterInteractions để defer heavy tasks3 thread giao tiếp qua Bridge bằng cách serialize data thành JSON. Đây chính là bottleneck lớn nhất của RN cũ — mỗi lần gửi message qua Bridge đều phải JSON.stringify → truyền → JSON.parse.
New Architecture (Fabric + TurboModules): Sử dụng JSI (JavaScript Interface) cho phép JS gọi trực tiếp C++ functions — không cần serialize/deserialize. Giảm latency đáng kể.
| Thread | Vai trò | Nếu bị block |
|---|---|---|
| JS Thread | Business logic, state, event handlers | UI không respond, animations giật |
| UI Thread | Native rendering, touch, animations | App freeze, ANR trên Android |
| Shadow Thread | Yoga layout calculation | Layout chậm, render delay |
Ví dụ flow khi user nhấn button:
User tap (UI Thread)
→ Event serialize to JSON (Bridge)
→ JS Thread nhận event, xử lý handler
→ setState() → tính Virtual DOM diff
→ Gửi layout instructions qua Bridge
→ Shadow Thread tính Yoga layout
→ UI Thread render native views
Wrap component bằng React.memo để skip re-render khi props không đổi (shallow comparison).
const UserCard = React.memo(function UserCard({ name, avatar }: Props) {
return (
<View>
<Image source={{ uri: avatar }} />
<Text>{name}</Text>
</View>
);
});
Component UserCard chỉ re-render khi name hoặc avatar thật sự thay đổi. Nếu parent re-render nhưng truyền cùng props, UserCard sẽ skip.
React.memo chỉ shallow compare. Nếu bạn truyền object/array mới mỗi render (dù nội dung giống), memo sẽ vô hiệu. Ví dụ style={{ padding: 10 }} tạo object mới mỗi render → memo không có tác dụng.
Memoize function reference để tránh tạo function mới mỗi render. Kết hợp với React.memo mới có ý nghĩa.
const Parent = () => {
const [count, setCount] = useState(0);
const handlePress = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onPress={handlePress} />;
};
const Child = React.memo(({ onPress }: { onPress: () => void }) => {
return <Button onPress={onPress} title="Increment" />;
});
Nếu không dùng useCallback, mỗi lần Parent render sẽ tạo handlePress mới → Child luôn re-render dù đã memo.
Memoize kết quả tính toán nặng. Chỉ tính lại khi dependencies thay đổi.
const filteredData = useMemo(() => {
return largeDataset.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [largeDataset, searchTerm]);
Nếu largeDataset có 50,000 items, filter mỗi render sẽ rất tốn CPU. useMemo cache kết quả cho đến khi searchTerm hoặc largeDataset thay đổi.
Memoize cũng có cost: lưu previous value, so sánh dependencies. Đừng memoize mọi thứ mà không đo đạc.
| Prop | Default | Giải thích |
|---|---|---|
getItemLayout |
undefined | Cung cấp height/offset của mỗi item khi biết trước size. Skip layout measurement → scroll cực mượt, hỗ trợ scrollToIndex instant. |
initialNumToRender |
10 | Số items render ban đầu. Giảm xuống đủ fill 1 screen để giảm initial render time. Quá ít → user thấy blank. |
maxToRenderPerBatch |
10 | Số items render mỗi batch khi scroll. Tăng → ít blank areas nhưng JS thread busy hơn. Giảm → responsive hơn nhưng có thể thấy blank. |
windowSize |
21 | Đơn vị: số viewport heights. windowSize=21 nghĩa là render 10 viewport trên + 1 hiện tại + 10 viewport dưới. Giảm → ít memory nhưng dễ thấy blank khi scroll nhanh. |
removeClippedSubviews |
false | Detach views ngoài viewport khỏi native view hierarchy. Giảm memory trên list dài. Cẩn thận: có thể gây bugs với absolute positioning. |
keyExtractor |
item.key / index | Phải trả về unique stable key. Dùng index làm key → re-render sai khi data thay đổi thứ tự. |
Ví dụ FlatList tối ưu cho list cố định chiều cao:
const ITEM_HEIGHT = 72;
const getItemLayout = (_: any, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
});
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
getItemLayout={getItemLayout}
initialNumToRender={8}
maxToRenderPerBatch={5}
windowSize={11}
removeClippedSubviews={true}
/>
import { FlashList } from "@shopify/flash-list";
<FlashList
data={data}
renderItem={({ item }) => <UserCard user={item} />}
estimatedItemSize={72}
keyExtractor={item => item.id}
/>
estimatedItemSize là prop bắt buộc — FlashList dùng giá trị này để ước lượng scroll position. Không cần getItemLayout vì FlashList tự đo dynamic heights.
| Đặc điểm | JIT (JavaScriptCore) | AOT (Hermes) |
|---|---|---|
| Compile | Runtime — parse + compile khi app chạy | Build time — compile thành bytecode lúc build |
| Startup time | Chậm (phải parse toàn bộ JS bundle) | Nhanh (load bytecode trực tiếp) |
| Memory | Cao (lưu AST + compiled code) | Thấp (chỉ cần bytecode) |
| Peak performance | Có thể nhanh hơn (JIT optimize hot paths) | Hơi chậm hơn peak (không có JIT optimization) |
| Bundle size | Plain JS text | Bytecode nhỏ hơn text |
Kết quả thực tế: giảm 30-50% startup time, giảm 20-30% memory usage.
Kiểm tra Hermes đang bật chưa:
const isHermes = () => !!global.HermesInternal;
console.log("Using Hermes:", isHermes());
Dựa trên SDWebImage (iOS) và Glide (Android) — 2 thư viện native image loading tốt nhất. So với <Image> mặc định:
import FastImage from "react-native-fast-image";
<FastImage
style={{ width: 200, height: 200 }}
source={{
uri: "https://example.com/photo.jpg",
priority: FastImage.priority.high,
cache: FastImage.cacheControl.immutable,
}}
resizeMode={FastImage.resizeMode.cover}
/>
InteractionManager.runAfterInteractions defer task cho đến khi tất cả animations/interactions hoàn thành.
import { InteractionManager } from "react-native";
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
fetchHeavyData();
processExpensiveComputation();
});
return () => task.cancel();
}, []);
Flow: Navigate → Animation chạy mượt → Animation xong → runAfterInteractions callback được gọi → Heavy task chạy. User thấy transition mượt, sau đó data mới load.
ActivityIndicator hoặc skeleton UI.
| # | Loại Leak | Nguyên nhân | Cách Fix |
|---|---|---|---|
| 1 | Uncleared timers | setInterval/setTimeout không clear khi unmount |
Clear trong useEffect cleanup |
| 2 | Event listeners | addEventListener không remove khi unmount | Remove trong cleanup function |
| 3 | Async operations | setState sau khi component đã unmount (API callback) | AbortController hoặc isMounted flag |
| 4 | Closures giữ reference | Closure capture biến lớn, không release | Nullify references, dùng WeakRef |
| 5 | Global state bloat | Tích lũy data trong global store không bao giờ cleanup | Pagination, eviction policy cho cache |
| 6 | Image cache không giới hạn | Cache hàng trăm ảnh full-size trong memory | Giới hạn cache size, dùng disk cache |
Ví dụ fix leak #1 và #3:
useEffect(() => {
const controller = new AbortController();
const intervalId = setInterval(() => {
fetch("/api/status", { signal: controller.signal })
.then(res => res.json())
.then(data => setStatus(data))
.catch(() => {});
}, 5000);
return () => {
clearInterval(intervalId);
controller.abort();
};
}, []);
Cleanup function trong useEffect chạy khi component unmount. AbortController cancel pending fetch, clearInterval dừng timer.
Công cụ visualize bundle — cho thấy mỗi dependency chiếm bao nhiêu % trong bundle.
npx react-native-bundle-visualizer
Output: treemap interactives hiển thị size của từng module. Từ đó xác định thư viện nào quá nặng cần thay thế hoặc lazy load.
import { format } from "date-fns/format";
import { debounce } from "lodash/debounce";
Thay vì import { format } from "date-fns" hoặc import _ from "lodash" sẽ kéo toàn bộ thư viện vào bundle.
react-native-bootsplash hoặc expo-splash-screen để hiển thị splash ngay lập tức (native level), ẩn khi JS bundle ready.import { BootSplash } from "react-native-bootsplash";
const App = () => {
useEffect(() => {
const init = async () => {
await loadEssentialData();
await BootSplash.hide({ fade: true });
InteractionManager.runAfterInteractions(() => {
initAnalytics();
initCrashReporting();
registerPushNotifications();
});
};
init();
}, []);
return <Navigation />;
};
Flow: Native splash hiện ngay → JS bundle load → essential data ready → hide splash với fade → defer analytics/crash/push sau khi animation xong.
| Tool | Mục đích | Platform |
|---|---|---|
| Flipper | All-in-one debugger: network, layout, performance, logs | iOS + Android |
| React DevTools Profiler | Measure component render time, tìm re-render thừa | Cross-platform |
| Systrace | Low-level tracing cho JS + native threads | Android |
| Xcode Instruments | CPU, Memory, GPU profiling chi tiết | iOS |
| Android Studio Profiler | CPU, Memory, Network, Energy profiling | Android |
| Performance Monitor | In-app overlay hiển thị FPS, RAM, JS thread | iOS + Android |
| Why Did You Render | Tự động log re-render không cần thiết | Cross-platform |
| Triệu chứng | Kiểm tra | Fix thường gặp |
|---|---|---|
| FlatList scroll giật | Render count, item height, image size, key, JS thread | Memo item, stable props, getItemLayout/FlashList, resize image |
| Input search lag | Filter/sort chạy mỗi keypress, list size, deferred work | Debounce/deferred value, memo filtered data, move heavy work |
| Startup chậm | Task chạy trước first screen, sync storage, native init | Defer non-critical task, lazy init, reduce bundle/startup work |
| Memory tăng sau khi vào ra screen | Timer/listener/request/image cache cleanup | Cleanup effect, abort request, remove listener, limit cache |
Khung trả lời phỏng vấn: Performance phải nói theo vòng đo: triệu chứng, metric, bottleneck, fix nhỏ, đo lại. Tránh trả lời kiểu liệt kê memo/useCallback khi chưa biết bottleneck.
3 thread chính:
Trong New Architecture (Fabric), Shadow Thread được tích hợp chặt hơn với UI Thread, và JSI thay thế Bridge cho phép synchronous calls giữa JS và native.
Nguyên tắc: React.memo cho components, useCallback cho functions, useMemo cho values. Luôn đo đạc trước khi memoize — premature optimization có thể tăng complexity mà không cải thiện performance.
Các props quan trọng:
{ length, offset, index } cho mỗi item. Khi biết trước chiều cao cố định, FlatList skip bước đo lường layout → scroll mượt hơn và scrollToIndex hoạt động tức thì.Ngoài ra: luôn dùng keyExtractor với unique stable key, tách renderItem thành memo component riêng, tránh anonymous function trong renderItem.
JavaScriptCore (JSC) dùng JIT compilation: parse JS text → compile thành machine code tại runtime. Mỗi lần app start đều phải lặp lại quá trình này.
Hermes dùng AOT compilation: compile JS thành bytecode tại build time. Khi app start, chỉ cần load bytecode — không cần parse/compile.
3 lý do startup nhanh hơn:
Tradeoff: Hermes không có JIT optimization nên peak performance có thể hơi thấp hơn JSC cho CPU-intensive tasks. Nhưng cho mobile app, startup time và memory quan trọng hơn peak throughput.
setInterval/setTimeout không clear → fix bằng clearInterval trong useEffect cleanup.AbortController để cancel fetch, hoặc guard với isMounted check.Detect: Dùng Flipper Memory plugin hoặc Xcode Instruments Allocations để track memory growth over time.
Defer expensive tasks cho đến khi tất cả animations và interactions hiện tại hoàn thành. Use case chính: khi navigate sang screen mới, animation transition đang chạy trên UI thread. Nếu JS thread cũng bận xử lý heavy task (API call, data processing), animation sẽ giật.
runAfterInteractions đặt task vào queue, chờ animation xong mới execute. Kết hợp với skeleton UI: hiển thị placeholder ngay, load data sau khi transition mượt.
Trả về object có method cancel() — luôn cancel trong useEffect cleanup để tránh memory leak.
Cold start: App process chưa tồn tại trong memory. OS phải: fork process → load native libraries → initialize JS runtime → load JS bundle → execute JS → render first frame. Thường mất 1-5 giây.
Warm start: App đã trong memory (user quay lại từ background). Process vẫn sống, chỉ cần resume activity — gần như instant.
Tối ưu cold start:
Cơ chế khác biệt cốt lõi: FlatList mount/unmount components khi scroll. FlashList tái sử dụng cells đã mount (cell recycling) — tương tự RecyclerView (Android) hoặc UICollectionView (iOS).
Ưu điểm FlashList:
Khi nào dùng: List > 100 items, complex render items, user scroll nhanh, thiết bị low-end. Với list < 50 items đơn giản, FlatList đủ tốt.
npx react-native-bundle-visualizer để biết dependency nào chiếm nhiều nhất.import debounce from "lodash/debounce" thay vì import _ from "lodash".no-unused-vars.Quy trình 5 bước:
Common findings: Unnecessary re-renders (70% cases), unoptimized images (15%), heavy computation on JS thread (10%), memory leaks (5%).
Cho component tree: App → UserList → UserCard (x100). Mỗi lần search text thay đổi, tất cả 100 UserCard đều re-render dù chỉ filtered list thay đổi.
Yêu cầu:
React.memo cho UserCarduseMemo cho filtered resultsuseCallback cho callback truyền xuốngAcceptance: Khi gõ search, chỉ cards matching mới re-render. Profiler cho thấy giảm ≥ 50% render time.
Yêu cầu:
Acceptance: Có bảng so sánh số liệu cụ thể giữa 2 approaches. FlashList phải cho FPS cao hơn khi fast scroll.
Tạo app có intentional memory leaks (timer không clear, event listener không remove, setState sau unmount). Sau đó dùng Flipper/Instruments để detect và fix từng leak.
Yêu cầu:
Acceptance: Memory graph sau fix phải flat (không tăng liên tục khi navigate back-and-forth).
Yêu cầu:
Acceptance: Cold start time giảm ≥ 20%. Bundle size giảm ≥ 10%.
Yêu cầu:
<Image> mặc định, load full-sizeAcceptance: Version 2 phải load nhanh hơn ≥ 2x, memory thấp hơn ≥ 30%.
Case: Feed 200 item giật khi gõ search. Team thêm React.memo nhưng không cải thiện vì renderItem và style object đều tạo mới mỗi render.
Cách xử lý: Đo render count, tách item component, ổn định props/callback/style, memoize filtered data, và kiểm tra lại bằng Profiler thay vì cảm giác.
Performance work phải có vòng lặp: đo triệu chứng, tìm bottleneck, sửa nhỏ, đo lại. Không có số liệu thì rất dễ tối ưu sai chỗ.
React.memo vô tác dụng?