Blog
Feature
5 janvier 20269 min

Feed React Native : de 3s a 200ms avec cache et RPC

Chaque app a cet ecran qui concentre toute la complexite. Pour TAMSIV, c'est le Feed. Un flux unique qui melange tout : activite recente, taches completees, memos crees, evenements du calendrier, badges debloques, progression de niveau, activite de groupe. C'est l'ecran le plus ambitieux de l'app — et celui qui m'a pris le plus de temps a construire.

Ce que je vais te raconter ici, c'est le parcours complet : de la premiere version qui mettait 3 secondes a charger a la version actuelle qui se charge instantanement depuis le cache. Les choix techniques, les optimisations, et les lecons apprises en trois semaines de travail intense.

Points cles a retenir :
- Une seule RPC PostgreSQL (get_consolidated_feed) agrege tous les types de contenu
- L'optimisation des JOINs et l'indexation ont reduit le chargement de 3s a 200ms
- Le cache L1 (memoire) + L2 (AsyncStorage) permet un affichage instantane
- Les signed URLs Supabase expirent apres 60 minutes — le refresh batch est critique
- Chaque type d'element a son composant dedie pour une FlatList performante

Pourquoi un feed unifie plutot que des ecrans separes ?

La question est legitime. Pourquoi ne pas avoir un ecran "taches recentes", un ecran "badges", un ecran "activite de groupe" ? Plusieurs raisons :

1. Reduction de la charge cognitive. L'utilisateur ouvre une seule vue et voit tout ce qui s'est passe. Pas besoin de naviguer entre 4 onglets pour avoir une vue d'ensemble. C'est le pattern qu'utilisent toutes les apps sociales — de Instagram a LinkedIn — et les utilisateurs le comprennent intuitivement.

2. Decouverte passive. Un utilisateur qui ouvre le feed pour voir ses taches recentes va aussi decouvrir qu'il a debloque un badge ou qu'un collegue a commente dans un groupe. C'est du cross-engagement : une feature en pousse une autre.

3. Engagement par la gamification. Le systeme de gamification de TAMSIV (12 niveaux, 10 badges, streaks, challenges quotidiens) n'a d'impact que s'il est visible. Un badge debloque dans un ecran cache, personne ne le voit. Un badge dans le feed, tout le monde le celebre.

Ecran de smartphone montrant un flux d'activite avec des badges de reussite colores, des barres de progression et des cartes de notification
Le Feed TAMSIV : taches, memos, badges et activite de groupe dans un flux unifie.

Comment fonctionne la RPC consolidee ?

Le coeur technique du feed, c'est une seule fonction PostgreSQL : get_consolidated_feed. Cette RPC agrege des donnees de 5 sources differentes :

  • Taches recentes (schema privat.)
  • Memos recents (schema privat.)
  • Evenements du calendrier (schema privat.)
  • Activite de gamification (schema gamification.) — badges, level ups, streaks
  • Activite de groupe (schema collaborative.) — taches partagees, commentaires, assignations

Le tout retourne dans un type unifie avec un champ item_type pour le routing cote frontend. Chaque element du feed est identifie par son type, et le frontend sait exactement quel composant rendre.

Pourquoi une RPC plutot que des requetes separees ? Parce qu'une seule requete SQL est toujours plus rapide que 5 requetes separees, meme avec le connection pooling de Supabase. Moins d'allers-retours reseau, moins de latence, et surtout la possibilite de trier et paginer au niveau de la base plutot que cote client.

Comment optimiser une requete de 3 secondes a 200ms ?

Les premieres versions du feed etaient penibles. 3 secondes de chargement. Pour un ecran d'accueil, c'est eliminatoire — les utilisateurs ferment l'app avant que le contenu apparaisse.

Le probleme : des JOINs mal optimises. La RPC initiale faisait des JOINs sur des tables sans index, avec des sous-requetes correlees. EXPLAIN ANALYZE montrait des sequential scans la ou il fallait des index scans.

Ecran d'ordinateur montrant un tableau de bord de monitoring de performance de base de donnees avec des graphiques colores
Monitoring de performance : chaque milliseconde compte dans un feed.

Les optimisations appliquees :

1. Indexation ciblee. J'ai ajoute des index composites sur les colonnes utilisees dans les WHERE et ORDER BY de la RPC. Un index sur (user_id, created_at DESC) a divise le temps de la requete par 3 a lui seul.

2. Reecriture des JOINs. Les sous-requetes correlees (SELECT dans SELECT) ont ete remplacees par des JOINs lateraux. PostgreSQL les optimise beaucoup mieux.

3. Limitation des colonnes. Au lieu de SELECT *, je ne recupere que les colonnes necessaires pour l'affichage dans le feed. Moins de donnees transferees = moins de temps.

4. Pagination server-side. La RPC accepte des parametres p_offset et p_limit. Seuls 20 elements sont charges a la fois. La pagination infinie cote client demande les 20 suivants quand l'utilisateur approche la fin de la liste.

Resultat : de 3 secondes a 200ms. Un facteur 15 d'amelioration. C'est le genre d'optimisation qui transforme une app "utilisable" en une app "agreable".

Comment renderer chaque type d'element efficacement ?

Le feed melange des elements tres differents : une tache a un titre, une priorite, une date limite. Un badge a un icon, un nom, une description. Un evenement a une heure, un lieu, des participants. Chaque type necessite un composant dedie.

Les composants du feed :

  • FeedTaskItem : tache avec priorite, date, assignation
  • FeedMemoItem : memo avec apercu du contenu et image de couverture
  • FeedGamificationItem : badge, level up, streak milestone
  • FeedGroupItem : activite collaborative (nouveau membre, tache assignee, commentaire)
  • FeedCalendarItem : evenement a venir

Le tout dans une FlatList de react-native-gesture-handler (obligatoire, pas celle de react-native — voir les gotchas du gesture-handler) avec getItemLayout pour le calcul de hauteur et keyExtractor base sur le couple (item_type, id).

Le pattern getItemLayout est crucial pour la performance : il permet a la FlatList de calculer la position de chaque element sans le rendre. Sans ca, le scroll saccade quand la liste contient des centaines d'elements.

Comment resoudre le probleme des images expirees ?

C'est un des pieges les plus vicieux de Supabase Storage. Les signed URLs expirent apres 60 minutes. Si un element du feed contient une image (piece jointe d'une tache, couverture d'un memo), l'URL stockee dans le feed expire et l'image ne s'affiche plus.

La solution en deux parties :

1. Stocker le storage_path, pas l'URL. La RPC get_consolidated_feed retourne un champ firstImageStoragePath en plus de firstImageUri. Le path est permanent, l'URL est temporaire.

2. Refresh batch des URLs. GamificationService.refreshFeedImageUrls() appelle StorageService.refreshAttachmentUrlsBatch() pour regenerer toutes les URLs expirees en un seul appel batch. Pas d'appel individuel par image — un seul appel pour toutes les images du feed.

Ce pattern est detaille dans l'article sur la reduction de l'egress Supabase. Les signed URLs sont un pattern puissant pour la securite, mais elles creent une complexite de gestion que beaucoup de developpeurs sous-estiment.

Comment fonctionne le cache multi-niveaux ?

Le cache du feed est le secret de l'affichage instantane. Il fonctionne en trois niveaux :

Cache L1 — Memoire (Map). Les donnees du feed sont gardees dans une Map en memoire via le ContentCacheService. C'est le plus rapide : acces en O(1), pas de deserialization. Le feed se charge depuis le L1 en moins de 10ms.

Cache L2 — AsyncStorage. Si le L1 est vide (premier lancement, redemarrage de l'app), les donnees sont recuperees depuis AsyncStorage. Plus lent que la memoire (~50ms) mais plus rapide qu'un appel reseau.

Source de verite — Supabase. En arriere-plan, les donnees fraiches sont recuperees depuis Supabase et le cache est mis a jour. L'utilisateur voit les donnees du cache immediatement, puis la vue se met a jour silencieusement si de nouvelles donnees sont disponibles.

Personne detendue sur un canape faisant defiler un flux d'application mobile avec du contenu qui charge de maniere fluide
Le feed se charge instantanement depuis le cache, puis se met a jour en arriere-plan.

Le pattern "cache-first + background refresh" est utilise par la plupart des apps performantes. C'est ce que SWR fait sur le web (stale-while-revalidate). En React Native, je l'ai implemente manuellement avec le ContentCacheService.

Supabase Realtime ajoute-t-il de la valeur au feed ?

Oui, et c'est ce qui rend le feed "vivant". Supabase Realtime est configure sur les tables privat.tasks et privat.memos. Deux channels sont ouverts : content-cache-tasks et content-cache-memos.

Quand une tache est creee, completee ou modifiee (par l'utilisateur ou par un membre de son groupe), l'evenement Realtime est recu et le cache L1 est invalide. La prochaine fois que le feed est affiche, il recupere les donnees fraiches.

Le resultat : quand un collegue complete une tache dans un groupe collaboratif, l'activite apparait dans ton feed en quelques secondes. Sans refresh manuel, sans pull-to-refresh — ca arrive tout seul.

Quelles sont les performances sur les appareils d'entree de gamme ?

Un feed complexe avec des images, des badges et des animations peut etre problematique sur les appareils modestes. Voici les optimisations specifiques :

  • Recyclage des composants : la FlatList ne rend que les elements visibles. Les elements hors ecran sont recycles, pas supprimes et recrees.
  • Images lazy-loaded : les images ne sont chargees que quand elles entrent dans le viewport + une marge de 300px.
  • Animations reduites : sur les appareils detectes comme "lents" (via InteractionManager), les animations sont simplifiees.
  • Batch des view stats : les statistiques de vue (combien de fois un element a ete vu) sont envoyees en batch, pas individuellement. C'est passe de N+1 requetes a 1 seule grace aux RPC getTaskViewStatsBatch et getMemoViewStatsBatch.

Ces optimisations permettent au feed de fonctionner correctement sur des appareils avec 2 Go de RAM, ce qui couvre la majorite du marche Android.

Comment la gamification s'integre-t-elle dans le feed ?

Le systeme de gamification de TAMSIV comprend 12 niveaux, 10 badges, des streaks (jusqu'a 365 jours), et des challenges quotidiens. Chaque evenement de gamification (badge debloque, level up, streak milestone) apparait dans le feed comme un element dedie.

Le FeedGamificationItem est visuellement distinct des autres elements : une couleur d'accent differente, une animation subtile, et un message de felicitation. C'est un moment de celebration dans le flux — il casse le rythme monotone des taches et memos et injecte de l'emotion.

L'integration se fait via le GamificationService (singleton) qui est appele depuis useTaskDetail (a la completion d'une tache) et memoCreation.ts (a la creation d'un memo). Le service verifie les conditions de badges et de niveaux et cree les elements feed correspondants via les RPC dediees. Le systeme de notifications est aussi declenche pour les achievements importants.

Ce que j'ai appris en construisant le feed

Cet ecran m'a pris trois semaines. C'est aussi celui dont je suis le plus fier. Pas parce qu'il est spectaculaire visuellement — c'est un feed assez classique — mais parce qu'il fonctionne bien. Il est rapide, fiable, et agreable a utiliser.

La lecon principale : la performance est une feature. Un feed qui met 3 secondes a charger, personne ne l'utilise, peu importe la quantite de features qu'il contient. Un feed qui se charge instantanement, les utilisateurs y reviennent naturellement.

C'est la meme philosophie que j'ai appliquee a toute l'app : les micro-interactions fluides, l'onboarding sans friction, la recherche instantanee. La vitesse et la fluidite sont les meilleures features de retention qu'un dev solo puisse implementer.

FAQ

Combien d'elements le feed peut-il contenir ?

Techniquement, le feed est infini grace a la pagination server-side (20 elements par page). En pratique, les utilisateurs actifs accumulent quelques centaines d'elements par mois. La FlatList avec recyclage gere des milliers d'elements sans probleme de memoire.

Le feed consomme-t-il beaucoup de donnees mobiles ?

Non. Le cache L1/L2 evite les requetes repetees. Les images sont compressees et lazy-loaded. Un refresh complet du feed consomme environ 50 Ko de donnees (hors images). Les images representent l'essentiel du trafic, mais ne sont chargees qu'une fois et mises en cache.

Peut-on filtrer le feed par type de contenu ?

Pas encore dans la version actuelle. Le feed affiche tout par ordre chronologique. C'est un choix delibere : le feed est un lieu de decouverte, pas de recherche. Pour trouver une tache specifique, il y a la recherche dediee.

Le Realtime fonctionne-t-il en arriere-plan ?

Non. Les channels Supabase Realtime sont fermes quand l'app passe en arriere-plan (pour economiser la batterie). Quand l'app revient au premier plan, les channels sont reouverts et le cache est rafraichi. Le delai de reconnexion est de 1 a 2 secondes.

Comment le feed gere-t-il les contenus supprimes ?

Les elements supprimes sont retires du cache L1 et L2 immediatement via les evenements Realtime (event type DELETE). Si un element supprime est encore visible dans la FlatList, il disparait avec une animation de fade-out.