Offline-first: design pattern pentru conexiuni proaste
Offline-first înseamnă că aplicația funcționează complet fără rețea și sincronizează cu serverul când conexiunea revine. SQLite, CRDT, conflict resolution.
Cuprins
- Ce înseamnă offline-first ca design pattern?
- Cum stochezi local (SQLite, Realm, IndexedDB, AsyncStorage) și ce alegi când?
- Cum sincronizezi și rezolvi conflicte (optimistic UI, CRDT, last-write-wins)?
- Cum testezi degradarea conexiunii (airplane mode, throttle, Network Link Conditioner)?
- Când NU merită complexitatea (apps care cer fresh state continuu)?
O aplicație offline-first este proiectată să funcționeze complet fără conexiune la rețea: citirile și scrierile se fac în stocarea locală, iar sincronizarea cu serverul are loc în fundal când conexiunea revine. Nu este o optimizare de performanță, ci o decizie arhitecturală luată de la începutul proiectului.
Confuzia frecventă este că offline-first înseamnă „aplicația merge și fără internet". Merge și o aplicație cu date statice în cache. Diferența reală este că o aplicație offline-first permite scrieri în timp ce ești deconectat, le stochează local și le reconciliază cu serverul fără pierdere de date când conexiunea revine. Asta implică o strategie clară de stocare locală, o logică de sincronizare și o politică de rezolvare a conflictelor.
Ce înseamnă offline-first ca design pattern?
Offline-first inversează prioritatea implicită a majorității aplicațiilor web. Modelul clasic: clientul face un request la server și afișează răspunsul. Modelul offline-first: clientul citește din baza de date locală, afișează imediat și, în paralel, trimite sau primește date de la server. Rețeaua devine un canal de sincronizare, nu o condiție prealabilă.
Arhitectural, asta înseamnă că fiecare operație de scriere a utilizatorului este mai întâi persistată local, marcată ca pending, și abia apoi propagată la server. Dacă serverul nu e accesibil, operația rămâne în coada locală și se trimite când conexiunea revine. Aplicația nu blochează utilizatorul în așteptare.
- Local-first, sync ulterior. Baza de date locală este sursa de adevăr pentru interfață, nu răspunsul din rețea.
- Conflict resolution explicit. Când doi utilizatori modifică aceeași înregistrare offline, trebuie o politică clară: cine câștigă și ce se pierde.
- Idempotență obligatorie. Operațiile sincronizate la reconectare pot fi trimise de mai multe ori (timeout-uri, retransmisii). Serverul trebuie să le trateze idempotent.
Cum stochezi local (SQLite, Realm, IndexedDB, AsyncStorage) și ce alegi când?
Alegerea motorului de stocare locală depinde de platforma țintă și de complexitatea interogărilor:
- SQLite. Cel mai matur, disponibil nativ pe iOS și Android. Suportă SQL complet, tranzacții, indecși, JOIN-uri. Potrivit pentru seturi de date mai mari și structuri relaționale. Schema trebuie definită explicit și migrată la update-uri.
- Realm. Bază de date orientată obiect, cu binding-uri pentru React Native, Swift și Kotlin. Vine cu Atlas Device Sync (MongoDB) care gestionează sincronizarea și conflictele, dar te leagă de infrastructura MongoDB Atlas.
- IndexedDB. Singurul mecanism de stocare persistent în browser, asincron prin design. Librăriile precum Dexie.js simplifică API-ul verbos. Esențial pentru Progressive Web App-uri care vor funcționa offline.
- AsyncStorage. Stocare cheie-valoare simplă pentru React Native. Recomandat exclusiv pentru date mici: preferințe, tokens de sesiune. Nu pentru seturi de date structurate.
Regula practică: interogări cu filtrări sau relații: SQLite. Sync gestionat în ecosistem MongoDB: Realm. Web sau PWA: IndexedDB cu Dexie.js. AsyncStorage numai pentru preferințe.
Cum sincronizezi și rezolvi conflicte (optimistic UI, CRDT, last-write-wins)?
Sincronizarea are două dimensiuni: direcția fluxului de date și politica de rezolvare a conflictelor când aceleași date sunt modificate concomitent.
Pe direcția fluxului, trei abordări comune:
- Optimistic UI. Scrierile sunt aplicate imediat local, trimise la server în fundal. Dacă serverul refuză, interfața rollback-uiește. Produce experiență fluidă dar cere gestionarea erorilor de rollback.
- Pull-based sync. Clientul întreabă periodic serverul: „ce s-a schimbat de la timestamp-ul X?". Simplu de implementat pe orice API REST cu un parametru
updated_since. Potrivit când real-time nu e critic. - Push-based sync. Serverul notifică clientul via push notifications sau WebSocket când există date noi. Mai rapid, mai complex, cere menținerea conexiunii sau a unui canal de push activ.
Pe rezolvarea conflictelor:
- Last-write-wins. Câștigă scrierea cu timestamp-ul cel mai recent. Simplu, ușor de implementat, dar pierde date când doi utilizatori editează concomitent. Acceptabil pentru un singur utilizator pe mai multe dispozitive.
- CRDT (Conflict-free Replicated Data Types). Structuri de date proiectate să convergă matematic la același rezultat indiferent de ordinea actualizărilor. Tipuri comune: G-Counter, LWW-Register (last-write-wins per câmp), OR-Set. Elimină conflictele structural, cu cost de complexitate și stocare mai mari. Soluții existente: Yjs, Automerge.
- Rezolvare manuală. Interfața prezintă utilizatorului ambele versiuni, similar unui merge conflict în Git. Potrivit pentru documente importante unde nicio decizie automată nu e acceptabilă.
Cum testezi degradarea conexiunii (airplane mode, throttle, Network Link Conditioner)?
Testarea offline-first cere simularea a trei scenarii distincte, nu doar dezactivarea Wi-Fi-ului:
- Pierdere bruscă în mijlocul unui request. Cel mai frecvent bug: operația e trimisă la server dar răspunsul nu mai vine. Aplicația trebuie să detecteze timeout-ul, să marcheze operația ca pending și să o retrimită la reconectare, nu să o piardă.
- Offline prelungit cu scrieri multiple. Utilizatorul face 20 de operații offline. La reconectare, toate trebuie sincronizate în ordinea corectă, fără duplicare. Testează explicit că coada de pending operations e procesată complet.
- Reconectare cu conflict. Același câmp a fost modificat și local și pe server în perioada offline. Politica ta de conflict resolution se aplică conform așteptărilor?
Instrumente concrete: Network Link Conditioner pe iOS pentru latențe, pierdere de pachete și profil Offline. Pe Android, Developer Options are throttle pentru rețea. Pe web, Chrome DevTools (tab Network, dropdown „Offline"). Charles Proxy și mitmproxy permit scenarii mai fine: răspunsuri parțiale, erori HTTP intermitente, latență variabilă per endpoint.
La crawlerra, când proiectele de aplicații mobile necesită testare de rețea, recomandăm documentarea scenariilor de degradare în specificațiile tehnice, similar modului în care documentăm comportamentele de observabilitate pentru serviciile backend. Comportamentul în condiții de rețea degradată trebuie specificat înainte de implementare, nu descoperit în producție.
Când NU merită complexitatea (apps care cer fresh state continuu)?
Offline-first adaugă complexitate reală: un motor de stocare locală, logică de sync, politică de conflict resolution, teste suplimentare, și o suprafață mai mare de bug-uri legate de stare divergentă. Merită adoptată doar când cazul de utilizare o justifică.
Aplicații unde offline-first nu ajută sau activat înrăutățește lucrurile:
- Live trading și instrumente financiare. Prețurile au valabilitate de secunde. O decizie luată pe date cu 30 de secunde întârziere poate produce pierderi. Aplicația trebuie să blocheze activ operațiile când nu are date proaspete, nu să permită scrieri pe date stale.
- Group chat în timp real. Optimistic UI pentru mesaje individuale funcționează, dar conversațiile multi-utilizator cu branching (replies la mesaje vechi, ștergeri concomitente) produc stări locale divergente greu de reconciliat fără CRDT. Costul implementării corecte depășește beneficiul pentru majoritatea aplicațiilor de chat.
- Fluxuri de aprobare sau workflow-uri colaborative complexe. Dacă un utilizator aprobă o cerere offline, iar între timp cererea a fost respinsă sau modificată de altcineva, conflictul nu are o rezolvare automată acceptabilă. Aceste fluxuri cer coordonare centralizată și trebuie să blocheze explicit operațiile când serverul nu e disponibil.
- Aplicații cu date foarte frecvent actualizate de server. Dacă stocarea locală devine stale în câteva secunde (dashboards live, feed-uri de date IoT), overhead-ul de sync și conflict resolution nu aduce valoare. Conexiunea persistentă via WebSocket sau push notifications e soluția corectă.
Semnalul că offline-first e justificat: utilizatorii tăi pierd efectiv rețeaua în cursul utilizării normale (tehnicieni în teren, aplicații medicale pentru vizite la domiciliu, livrare în zone rurale) și fac scrieri în acea perioadă. Dacă scenariul „fără internet" e teoretic și apare rar, un mesaj de eroare clar și reîncercarea automată sunt suficiente și mult mai simple. Detalii despre update-uri fără reconectare completă în OTA updates și navigare între ecrane fără request suplimentar în deep linking.
Întrebări frecvente
SQLite, Realm sau IndexedDB: care aleg pentru o aplicație mobile?
SQLite pentru React Native și aplicații native care au nevoie de interogări complexe; Realm când vrei sync built-in cu MongoDB Atlas; IndexedDB când construiești un PWA sau o aplicație web. AsyncStorage e potrivit doar pentru preferințe sau tokens, nu pentru date structurate voluminoase. Factorul decisiv nu este performanța brută, ci ce infrastructure de sync ai deja și cât de complexe sunt interogările tale.
Ce este un CRDT și când am nevoie de el?
CRDT (Conflict-free Replicated Data Type) este o structură de date care garantează matematic că toate replicile converg la același rezultat, indiferent de ordinea în care sosesc actualizările. Ai nevoie de CRDT când mai mulți utilizatori editează aceleași date concomitent fără coordonare centrală: documente colaborative, liste comune, note partajate. Pentru aplicații cu un singur utilizator, last-write-wins este de obicei suficient și mult mai simplu de implementat.
Cum testez offline-first fără device fizic?
Pe iOS folosești Network Link Conditioner din Developer Tools pentru a simula pierderea rețelei; pe Android, airplane mode direct sau opțiunea de throttle din Developer Options. Pentru web, Chrome DevTools are profilul Offline în panoul Network. Charles Proxy sau mitmproxy permit scenarii mai nuanțate: latențe variabile, pierdere parțială de pachete, răspunsuri parțiale. Testează cel puțin trei scenarii: pierdere bruscă în mijlocul unui request, reconectare după 5 minute, și reconectare cu conflict de date pe server.
Offline-first funcționează cu un backend REST clasic?
Da, dar ai nevoie de endpoint-uri care suportă sync incremental: un parametru de tip updated_since sau un mecanism de cursori care returnează doar înregistrările modificate după un timestamp. Fără sync incremental, clientul trebuie să descarce toată baza de date la fiecare reconectare, ceea ce face offline-first inutilizabil în practică. Alternativele de sync în timp real (WebSocket, SSE) completează dar nu înlocuiesc logica de reconciliere locală.
Aplicația mea trebuie neapărat să fie offline-first?
Nu. Offline-first adaugă complexitate semnificativă și merită adoptată doar când utilizatorii tăi pierd efectiv rețeaua în timpul utilizării normale. Aplicații de livrare în zone rurale, field service tools, aplicații medicale pentru vizite la domiciliu sunt cazuri clare. Un panou de admin intern, o aplicație de live trading sau un group chat în timp real nu beneficiază de offline-first și pot deveni mai fragile dacă e implementat forțat.