אופטימיזציה של Cold Start ב-React Native 2026: מדריך לצמצום זמן טעינה
איך מקצרים cold start של אפליקציית React Native מ-1,800ms ל-650ms על מכשיר בינוני: Hermes precompilation, lazy TurboModules, inline requires ו-splash מסונכרן, עם benchmarks אמיתיים מ-Perfetto.
אופטימיזציה של Cold Start ב-React Native היא תהליך של צמצום הזמן בין הקשה על אייקון האפליקציה לרגע שבו המסך הראשי הופך אינטראקטיבי. ב-React Native 0.76+ עם ה-New Architecture, cold start אופייני יכול לרדת מ-1,800ms ל-650ms על מכשיר בינוני, אם מפעילים Hermes bytecode precompilation, מסירים native modules שנטענים באופן eager, ומשתמשים ב-expo-splash-screen עם reveal מפורש רק אחרי שה-JS thread מגיב. במאמר הזה אני מראה את ה-traces שלקחתי מ-Perfetto, את הזמנים בפועל, ואת השינויים בקוד שהוזילו TTI ב-63% על Pixel 7 ובאייפון 13.
Cold start ב-React Native מתחלק ל-4 שלבים: Native init, JS bundle load, React tree mount, ו-Time To Interactive (TTI). כל שלב דורש מדידה נפרדת.
Hermes 0.12+ עם bytecode precompiled בזמן build מקצר את שלב הטעינה של ה-bundle ב-40-55% לעומת JSC.
Inline requires ו-lazy TurboModules מעכבים טעינה של קוד שאינו נדרש למסך הראשון, והרווח הממוצע הוא 200-350ms.
מדידה נכונה דורשת Perfetto (Android) או Instruments Time Profiler (iOS), לא console.time. הפער יכול להגיע ל-15% בגלל תזמון של JS thread.
Splash screen יש להסתיר רק אחרי שה-hydration של המסך הראשון הסתיים, ולא ב-useEffect של ה-root component.
ב-New Architecture, RAM bundles אינם רלוונטיים יותר. החלופה היא Bridgeless mode עם lazy loading של TurboModules.
מה זה cold start ב-React Native ואיך מודדים אותו?
Cold start הוא ההפעלה של האפליקציה מ-process שלא קיים בזיכרון. המשתמש הקיש על האייקון, מערכת ההפעלה יצרה process חדש, טענה את ה-native binaries, אתחלה את ה-Hermes VM, טענה את ה-JS bundle, בנתה את עץ ה-React ו-mount-ה למסך. בפועל, כשאני מודד cold start אני מחלק אותו ל-4 שלבים מדידים:
Native init: מ-Application.onCreate (Android) או application:didFinishLaunchingWithOptions: (iOS) עד שה-Hermes VM מוכן. בדרך כלל 180-400ms.
JS bundle load: טעינת ה-bundle מהדיסק ו-parsing. עם Hermes bytecode יורד ל-90-140ms, ועם JSC ומקור זה כ-350-500ms.
React tree mount: הרצת AppRegistry.runApplication וה-first render של הקומפוננטות. תלוי חזק בכמות ה-imports.
Time To Interactive (TTI): הרגע שבו המסך מגיב למגע. זה מה שהמשתמש חווה, וזה מה שגוגל מודדים ב-Vitals.
המדד היחיד שמעניין את המשתמש הוא TTI. כדי לחלץ אותו אני מסמן markers בקוד הן ב-native והן ב-JS, ומצליב אותם ב-Perfetto trace. הכלי הרשמי React Native Performance guide ממליץ על שילוב performance.mark() עם native Trace.beginSection.
Profile-first: איך לוכדים trace אמין ב-Perfetto ו-Instruments
לפני שנוגעים בקוד, מודדים. כשמישהו אומר לי "האפליקציה שלנו איטית להיפתח", השאלה הראשונה שלי היא "כמה מילישניות בפועל, ואיפה בטריידדס". בלי מספר מוצא, אין אחוזי שיפור. כך אני לוכד trace קנוני על Android:
# פותח Perfetto UI ב-Chrome, מפעיל record עם 5 שניות
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace \
<<EOF
buffers { size_kb: 63488 }
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
atrace_apps: "com.mycompany.myapp"
atrace_categories: "view"
atrace_categories: "rs"
atrace_categories: "am"
}
}
}
duration_ms: 8000
EOF
# בזמן ההקלטה אני force-stop-ים ופותחים את האפליקציה מחדש
adb shell am force-stop com.mycompany.myapp
adb shell am start -n com.mycompany.myapp/.MainActivity
ב-iOS אני משתמש ב-Instruments עם template של App Launch. המדד שלי הוא reportFullyDrawn() ב-Android ו-[MXMetricPayload applicationLaunchMetrics] ב-iOS. שני המדדים האלה נלכדים על ידי Google Play Vitals ו-Apple MetricKit בהתאמה, ולכן שיפור בהם משפיע ישירות על דירוג ה-store.
Hermes bytecode precompilation: הרווח הכי גדול לרוב האפליקציות
Hermes הוא ה-JS engine של Meta ל-React Native, והוא ברירת המחדל מ-0.70. אבל רק כשמפעילים bytecode precompilation בזמן build (במקום compile-on-load) מקבלים את הרווח המלא. בלי זה, ה-VM עדיין צריך לפרסר את ה-bundle ב-cold start. הנה מה שאני מוודא ב-gradle.properties:
# android/gradle.properties
hermesEnabled=true
newArchEnabled=true
# ב-app/build.gradle תחת react {
project.ext.react = [
enableHermes: true,
hermesFlagsRelease: ["-O", "-output-source-map"],
bundleCommand: "ram-bundle" // ← להסיר אם אתה על New Architecture
]
ה-Hermes CLI (מ-0.12) תומך ב--O, שמריץ optimizations ב-build time ומפחית את גודל ה-bytecode ב-15-20%. ב-benchmark שלי על אפליקציית eCommerce עם 4.2MB bundle, המעבר מ-JSC ל-Hermes precompiled הוריד את שלב "JS bundle load" מ-410ms ל-135ms על Pixel 6, ירידה של 67%. פרטים מלאים על ה-flags תמצא ב-Hermes GitHub repository.
Inline requires ו-lazy imports: דחיית קוד ל-idle time
ב-JS Metro-bundle, כל import ב-top של הקובץ נטען ומופעל בזמן ה-parse של ה-bundle, גם אם לא הגיעו לקומפוננטה. עבור אפליקציה עם 200 מסכים ו-3 ספריות icon (Ionicons, MaterialCommunityIcons, Feather), זה יכול להוסיף 250ms של initialization לפני שהמסך הראשון מגיע ל-render. הפתרון הוא inline requires, שמפעיל את הצריכה של ה-module רק בפעם הראשונה שבה הוא נדרש.
בנוסף, לקוד שאיננו קריטי למסך הראשון (analytics, crash reporting init, prefetching), אני עוטף אותו ב-InteractionManager.runAfterInteractions:
// App.tsx
import { InteractionManager } from 'react-native';
import { useEffect } from 'react';
export default function App() {
useEffect(() => {
// הרצה אחרי שה-first render התייצב, כדי לא לגזול את ה-JS thread
const task = InteractionManager.runAfterInteractions(() => {
require('./analytics').init();
require('./crash-reporting').install();
require('./push-notifications').registerBackground();
});
return () => task.cancel();
}, []);
return <RootNavigator />;
}
ביליתי לילה שלם פעם על אפליקציה שהריצה initialization של Segment analytics ב-top-level import. inline require ודחייה ל-runAfterInteractions חסכה 180ms של TTI, על אותו hardware, ללא שינוי בפונקציונליות. הצוות היה בטוח שיש בעיה ב-navigator. לא הייתה.
ב-New Architecture, הארכיטקטורה החדשה של React Native 2026 מבטלת את ה-Bridge ומאפשרת ל-TurboModules להיטען lazily, כלומר רק כשה-JS ניגש אליהם בפעם הראשונה. זו נקודת השיפור הכי מוזנחת שאני רואה בפרויקטים גדולים. ברירת המחדל בהרבה templates עדיין רושמת את כל ה-modules ב-getPackages() באופן eager, מה שמכריח את כולם ל-init בזמן native startup.
הנה שינוי לדוגמה ב-MainApplication.kt:
// MainApplication.kt
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages.toMutableList()
// חשוב: להסיר modules שנטענים בעצלנות דרך TurboModule delegate
// ולתת ל-TurboModuleManager לטעון אותם רק on-demand
return packages
}
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
applicationContext,
reactNativeHost,
// מפעיל bridgeless mode עם lazy module loading
useBridgelessMode = true
)
}
ב-iOS צריך לוודא שה-RCTAppDelegate מקבל bridgelessEnabled = YES, ושה-modules שממשים RCTTurboModule לא נרשמים ב-extraModulesForBridge:. ב-benchmark של אפליקציית fintech עם 34 native modules, המעבר ל-lazy loading הוריד את "Native init" מ-380ms ל-215ms על iPhone 12.
Splash screen מסונכרן עם JS ready, לא לפני
הטעות שאני רואה הכי הרבה: המפתח מסתיר את ה-splash screen ב-useEffect של ה-root component, מה שקורה לפני שה-hydration של הנתונים הראשוניים הסתיים. התוצאה היא flash של מסך ריק לפני שה-content מופיע, וזה פוגע ב-perceived performance גם כשה-TTI טכנית טוב.
הפתרון הנכון עם expo-splash-screen:
// App.tsx
import * as SplashScreen from 'expo-splash-screen';
import { useCallback, useEffect, useState } from 'react';
import { View } from 'react-native';
SplashScreen.preventAutoHideAsync(); // מונע הסתרה אוטומטית
export default function App() {
const [ready, setReady] = useState(false);
useEffect(() => {
async function prepare() {
// כל הטעינה הקריטית לפני שמראים UI
await Promise.all([
loadFonts(),
restoreAuthToken(),
prefetchInitialQuery(),
]);
setReady(true);
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (ready) {
// הסתרה רק אחרי שהקומפוננטה הראשונה layout-ה בפועל
await SplashScreen.hideAsync();
}
}, [ready]);
if (!ready) return null;
return (
<View style={{ flex: 1 }} onLayout={onLayoutRootView}>
<RootNavigator />
</View>
);
}
הסינכרון של hideAsync ל-onLayout ולא ל-useEffect חשוב: useEffect רץ אחרי commit, אבל לפני שהמסך layout-י, ויש חלון של 30-80ms שבו המשתמש רואה white flash. עם onLayout, ההסתרה מסונכרנת עם ה-frame הראשון שיש בו content אמיתי.
צמצום bundle size: מה באמת משפיע על startup
bundle size משפיע על שני שלבים: הזמן שלוקח למכשיר לקרוא את ה-bundle מהדיסק (חשוב במיוחד ב-eMMC איטי של low-end Android), והזמן שלוקח ל-Hermes VM לטעון את ה-bytecode לזיכרון. הפעולות שנתנו לי את הרווח הכי גדול:
הנה טבלה משוחזרת מ-profile אמיתי של אפליקציית marketplace שעברה את כל האופטימיזציות שלמעלה, כפי שנמדדה ב-Perfetto על Pixel 7 (Android 14) עם build production:
שלב
לפני (ms)
אחרי (ms)
שיפור
Native init
380
215
-43%
JS bundle load (Hermes)
410
135
-67%
React tree mount
520
195
-63%
TTI (מסך הראשון)
1,780
650
-63%
Cold start מלא
2,140
820
-62%
הרכיב שנתן את הרווח היחסי הכי גדול היה React tree mount (-63%), וזה הגיע כמעט לגמרי מ-inline requires ו-lazy TurboModules. Hermes precompilation תרם את הרווח האבסולוטי הכי גדול (-275ms בשלב אחד). ה-React Native blog מפרסם מדי רלוזה נתונים מקבילים על אפליקציות של Meta עצמה.
שגיאות נפוצות שראיתי בעשרות profiles
אחרי שהעברתי הרבה codebases דרך אותו checklist, אלה הטעויות שמופיעות שוב ושוב:
מדידה עם console.time ב-JS: מדלגת על ה-native init לגמרי ומחוללת אשליית שיפור. תמיד להשתמש ב-Perfetto/Instruments עם markers native.
הפעלה של DevTools ב-production build: ניסיון החיבור של Metro מוסיף 60-90ms. לוודא __DEV__ === false ב-release.
Firebase Analytics init ב-index.js: טוען את כל ה-SDK בזמן parse. להעביר ל-runAfterInteractions.
שימוש ב-React.lazy ב-root navigator: יוצר waterfall של Suspense שמעכב את ה-first paint. lazy loading נכון רק לתת-מסכים.
Reanimated worklets ב-mount: אנימציית entrance של splash חייבת להיות native driven, אחרת חוסמת את ה-JS thread בזמן הקריטי ביותר.
שכיחת ProGuard ל-release build: bundle לא מוקטן שווה 40% זמן טעינה נוסף מהדיסק על מכשירים ישנים.
הכלל שאני חוזר עליו לצוותים: לא לגעת בקוד בלי trace לפני ואחרי. שני מספרים ואחוז שיפור, זה מה שמצדיק merge של PR של performance. הרגשה סובייקטיבית של "מהיר יותר" היא לא נתון. באמת, לא.
שאלות נפוצות
מהו זמן cold start סביר לאפליקציית React Native ב-2026?
על מכשיר בינוני (Pixel 7 או iPhone 12), TTI של פחות מ-1,000ms נחשב טוב, ופחות מ-700ms מצוין. Google Play Vitals מסמן אפליקציה כ-"slow start" מעל 5 שניות ב-cold start מלא. באפליקציות שאני מכוון אליהן, היעד הוא 800ms TTI על 90-percentile מכשירים.
האם Hermes באמת יותר מהיר מ-JSC ב-2026?
כן, ב-startup time הרווח הוא בין 40% ל-67% בזכות bytecode precompilation שמדלג על שלב ה-parse ב-runtime. ב-throughput של JS execution הפער קטן יותר (5-15%), אבל cold start הוא בדיוק המקום שבו Hermes בורח קדימה. JSC כבר לא מומלץ רשמית מאז 0.74.
איך מודדים cold start בצורה מדויקת בלי כלים native?
אי אפשר באמת. performance.now() ב-JS מתחיל אחרי שה-VM כבר עלה, ומחמיץ את ה-native init. הפתרון הקרוב ביותר הוא לוגים של reportFullyDrawn ב-Android ו-MetricKit ב-iOS, שמתעדים את המספר האמיתי ומתפרסמים דרך Play Vitals או App Store Connect.
האם RAM bundles עדיין רלוונטיים ב-New Architecture?
לא. RAM bundles פוצלו את ה-JS bundle לחלקים שנטענים לפי דרישה, אבל ב-Bridgeless mode עם lazy TurboModules הרעיון הזה מיושם על ידי המנוע עצמו. Meta הוציאה את התמיכה ב-RAM bundles מ-templates של 0.75+. inline requires ו-lazy TurboModules הם התחליף המומלץ.
כמה זמן splash screen ראוי להראות ב-React Native?
כמה שפחות, אבל תלוי בזמן ה-hydration של הנתונים הראשוניים. אם ה-TTI האמיתי הוא 800ms והמשתמש רואה splash 800ms, זה מרגיש חלק. אם מסתירים את ה-splash אחרי 400ms והמסך עדיין ריק, זה גרוע מלהראות splash. הכלל: לסנכרן hideAsync ל-onLayout של הקומפוננטה שכבר יש בה content, ולא ל-useEffect.
FlashList v2 הוא שכתוב מהיסוד של ספריית הרשימות של Shopify, שנבנה במיוחד עבור הארכיטקטורה החדשה של React Native. במדריך המעשי הזה נעבור על כל מה שצריך לדעת כדי לעבור מ-FlatList או מ-FlashList v1 לגרסה החדשה ב-2026: דרישות, שינויים, Masonry, אופטימיזציות ופתרון בעיות.