למה כל app מובייל חייב להיות Offline-First ב-2025
תרחיש אמיתי: אפליקציית שירות שדה ישראלית, טכנאי של חברת תקשורת נוסע לבדיקה בנגב. ה-app נפתח בבוקר עם מלא רשת, ממלא טפסים, מצלם ממצאים, ואז נכנס לאזור ללא כיסוי. הapp מקרש. הנתונים אובדים. הטכנאי חוזר למשרד עם ידיים ריקות. זה לא edge case, זה המציאות של כל app שמניח שהרשת תמיד זמינה.
Offline-first לא אומר שה-app עובד ללא אינטרנט, זה אומר שה-app מניח שהרשת תיכשל. כל write נשמר locally תחילה, ורק אחר כך מסתנכרן לserver. כל read מגיע מcache, ורק אחר כך מה-network. הבדל קטן בגישה, הבדל ענק בחוויה. מחקר מ-2025 מצא שמעבר ל-local-first הוריד זמן טעינה מ-1.7 שניות ל-300ms על Android ממוצע באזורים עם קישוריות לא יציבה.
Claude עוזר לתכנן את ה-sync strategy, אבל רק אם נותנים לו את הפרטים הנכונים: scale, סוגי data, failure modes, וconflict scenarios. שיעור זה מלמד את הארכיטקטורה, ואיך לכתוב פרומפטים שמייצרים קוד production-ready.
בחירת Storage Layer, לא החלטה שמשנים אחר כך
הבחירה בstorage layer היא אחת ההחלטות הכי קשות לשנות בהמשך. הנה ההשוואה הפרקטית:
| ספרייה | מתי להשתמש | חיסרון עיקרי |
|---|---|---|
| MMKV | Settings, queue של pending ops, tokens | לא מתאים לdata רלציוני |
| WatermelonDB | Catalog, entities עם relations, sync מובנה | דורש native build (לא Expo Go) |
| Expo SQLite + Drizzle | Stack מודרני, TypeScript, בלי native build | sync logic כותבים לבד |
| PowerSync | Sync אוטומטי Postgres-SQLite, minimal code | vendor dependency, pricing |
| AsyncStorage | לא, לא להשתמש לdata קריטי | איטי, אין transactions, async בלבד |
הכלל הפשוט: MMKV לqueue, WatermelonDB או Expo SQLite+Drizzle לentities, react-native-fs לקבצים. לsync אוטומטי ללא כתיבת sync logic, PowerSync היא הבחירה היחידה ב-2025 עם first-class offline support (ElectricSQL ו-Zero הכריזו ש-offline out of scope).
Sync Queue, הלב של Offline-First
הרכיב הכי חשוב בארכיטקטורה הוא ה-sync queue: רשימה של pending operations שחייבת לשרוד app restart. טעות נפוצה: queue ב-memory. כשה-app נסגר, הqueue נמחק, והuser מאבד את כל מה שעשה offline.
הפרומפט לClaude לייצר sync queue מלא:
מה שגורם לפרומפט החזק לעבוד: role framing ברור, scale מוגדר (50K technicians), failure modes ספציפיים (שעות ללא רשת), storage מוגדר, ו-expected outputs ברשימה מספורת. Claude יוצר קוד production-ready כשהוא מקבל את ה-context הזה.
Conflict Resolution, הבעיה שמגלים מאוחר מדי
User פותח app במובייל, מוסיף 3 items לcart. User פותח את אותו app בweb (אם יש), מוסיף 2 items אחרים. שניהם offline. שניהם מסתנכרנים. מה ה-cart הסופי?
כפי שנכתב ב-LogRocket Blog על WatermelonDB: "כש-server מחזיר conflict (HTTP 409), הapp צריך לmerge את ה-cart המקומי עם זה שבserver". אבל איזה merge? יש שלוש אסטרטגיות:
- Last-Write-Wins (LWW), הtimestamp המאוחר ביותר מנצח. פשוט, אבל user יכול לאבד data. מתאים לfields כמו notes, status.
- Additive merge, לlists: אם productId קיים בשניהם, קחו max quantity. אם רק באחד, הוסיפו אותו. מתאים לcart, task list, photo list.
- Manual resolution, הapp מציג למשתמש שני גרסאות ומבקש לבחור. מתאים לdocuments חשובים, חוזים, דוחות חתומים.
הגדירו conflict strategy לכל entity מהיום הראשון, זה הרבה יותר קשה להוסיף בהמשך.
WatermelonDB: Lazy Sync עם lastPulledAt
WatermelonDB פותרת את בעיית ה-initial sync: בסנכרון ראשון היא שולחת null, ומקבלת כל הdata. בסנכרונים הבאים שולחת את ה-lastPulledAt timestamp, ומקבלת רק את השינויים מאז. עם 50,000 מוצרים בcatalog, זה ההבדל בין sync של 50MB לsync של 2KB. הספרייה נבדקה ב-15 apps production עם 500K+ users ו-99.8% sync success rate.
חשוב: WatermelonDB דורשת development build, לא עובדת עם Expo Go. אם הצוות שלכם עובד עם Expo managed workflow, בחרו Expo SQLite + Drizzle עם sync logic ידני.
טעויות נפוצות
- Queue ב-memory בלבד: app restart = כל pending ops נמחקים. תמיד שמרו ב-MMKV או SQLite.
- isConnected בלי isInternetReachable: NetInfo.isConnected מחזיר true גם ב-captive portal (WiFi מלון שדורש login). בדקו isInternetReachable לפני כל sync חשוב.
- Sync ב-app startup: מאיטים TTI (Time to Interactive). הפעילו sync ב-background אחרי first meaningful paint.
- אין conflict resolution מהיום הראשון: שם החבר הכי גרוע בפיתוח מובייל הוא "נוסיף את זה אחר כך".
סיכום, עקרונות שישנו את ה-App שלכם
- Offline-first = local תמיד ראשון, network הוא extension בלבד
- Queue חייב לשרוד app restart, MMKV או SQLite, לא memory
- Conflict strategy מוגדרת מהיום הראשון לכל entity
- lastPulledAt ב-WatermelonDB = sync של 2KB במקום 50MB
- isInternetReachable בנוסף לisConnected, captive portals הם אויב אמיתי
- Sync ב-background אחרי first paint, לא ב-startup
- Claude מייצר sync architecture מלא כשנותנים לו: scale, failure modes, storage מוגדר, וoutputs ברשימה
