fix: render-optimiste + timing for widget toggle handlers (#71) #73

Merged
maximus merged 3 commits from issue-71-widget-expand-perf into master 2026-04-19 20:24:19 +00:00
Owner

Fixes #71

Resume

Le widget Android mettait plusieurs secondes a afficher les sous-taches sur tap expand. Cause confirmee/suspectee = cold start du service headless Android (irreductible cote JS) + ordre des operations dans le handler qui faisait attendre l'utilisateur sur l'ecriture AsyncStorage avant de rendre.

Changements

  • Render-optimiste sur les 3 handlers TOGGLE_* : renderWithState / forceWidgetRefresh est appele AVANT await setWidgetState. L'utilisateur voit le widget mis a jour immediatement ; la persistance continue en arriere-plan (mais reste awaitee pour la coherence du headless task lifecycle).
  • Debounce expand : 2000ms -> 600ms. Toujours assez pour bloquer un double-tap accidentel (~300-500ms typique), mais permet a l'utilisateur de replier juste apres avoir etendu.
  • persistState() wrappe setWidgetState dans try/catch. Si l'ecriture echoue, le prochain handler call relira l'etat anterieur depuis AsyncStorage et re-rendra a partir de la — l'UI s'auto-corrige a l'interaction suivante.
  • Timing instrumentation (timed() helper, gate __DEV__) : log chaque etape (getState, render, setState, db) + un total par handler vers logcat. Lecture :
    adb logcat -s ReactNativeJS | grep '\[widget\]'
    

Hors scope

  • Cold start du headless task Android : suspecte comme contributeur principal de la lenteur percue. Pas adressable cote JS.
  • Debounce sur TOGGLE_COMPLETE / TOGGLE_SUBTASK : non ajoute. TOGGLE_COMPLETE retire la tache de la liste (re-tap impossible), TOGGLE_SUBTASK est une action utilisateur intentionnelle. Ouvrir une issue separee si besoin.
  • Split state (expandedTaskIds dans son propre key AsyncStorage) : option B de l'analyse, gardee en reserve si les mesures Phase 1 montrent que serialization JSON est le bottleneck.

Test plan

  • Build dev : npm run android et installer sur appareil reel
  • Lancer adb logcat -s ReactNativeJS | grep '\[widget\]' pour collecter les mesures de chaque etape
  • TOGGLE_EXPAND : tap sur une tache avec sous-taches dans le widget medium/large, verifier que les sous-taches apparaissent en < 1s (perception)
  • TOGGLE_EXPAND debounce : double-tap rapide, verifier que la tache reste etendue (pas de toggle/untoggle visible)
  • TOGGLE_EXPAND + collapse : tap pour etendre, attendre 700ms, tap pour replier, verifier que ca repond
  • TOGGLE_COMPLETE : tap sur la checkbox d'une tache top-level, verifier que la tache disparait du widget et qu'elle est cochee dans l'app au reload
  • TOGGLE_SUBTASK : etendre une tache, tap sur la checkbox d'une sous-tache, verifier que la sous-tache se coche, le compteur (2/3) se met a jour, et les 3 widgets (small/medium/large) reflettent le changement
  • No regression : cycle d'usage normal pendant 5min, pas de crash, pas d'etat incoherent
  • Coller la sortie logcat dans un commentaire de l'issue #71 pour valider Phase 3 (re-mesure)
Fixes #71 ## Resume Le widget Android mettait plusieurs secondes a afficher les sous-taches sur tap expand. Cause confirmee/suspectee = cold start du service headless Android (irreductible cote JS) + ordre des operations dans le handler qui faisait attendre l'utilisateur sur l'ecriture AsyncStorage avant de rendre. ## Changements - **Render-optimiste** sur les 3 handlers `TOGGLE_*` : `renderWithState` / `forceWidgetRefresh` est appele AVANT `await setWidgetState`. L'utilisateur voit le widget mis a jour immediatement ; la persistance continue en arriere-plan (mais reste awaitee pour la coherence du headless task lifecycle). - **Debounce expand** : 2000ms -> 600ms. Toujours assez pour bloquer un double-tap accidentel (~300-500ms typique), mais permet a l'utilisateur de replier juste apres avoir etendu. - **persistState()** wrappe `setWidgetState` dans try/catch. Si l'ecriture echoue, le prochain handler call relira l'etat anterieur depuis AsyncStorage et re-rendra a partir de la — l'UI s'auto-corrige a l'interaction suivante. - **Timing instrumentation** (`timed()` helper, gate `__DEV__`) : log chaque etape (getState, render, setState, db) + un total par handler vers logcat. Lecture : ```bash adb logcat -s ReactNativeJS | grep '\[widget\]' ``` ## Hors scope - **Cold start du headless task Android** : suspecte comme contributeur principal de la lenteur percue. Pas adressable cote JS. - **Debounce sur TOGGLE_COMPLETE / TOGGLE_SUBTASK** : non ajoute. TOGGLE_COMPLETE retire la tache de la liste (re-tap impossible), TOGGLE_SUBTASK est une action utilisateur intentionnelle. Ouvrir une issue separee si besoin. - **Split state** (expandedTaskIds dans son propre key AsyncStorage) : option B de l'analyse, gardee en reserve si les mesures Phase 1 montrent que serialization JSON est le bottleneck. ## Test plan - [ ] Build dev : `npm run android` et installer sur appareil reel - [ ] Lancer `adb logcat -s ReactNativeJS | grep '\[widget\]'` pour collecter les mesures de chaque etape - [ ] **TOGGLE_EXPAND** : tap sur une tache avec sous-taches dans le widget medium/large, verifier que les sous-taches apparaissent en < 1s (perception) - [ ] **TOGGLE_EXPAND debounce** : double-tap rapide, verifier que la tache reste etendue (pas de toggle/untoggle visible) - [ ] **TOGGLE_EXPAND + collapse** : tap pour etendre, attendre 700ms, tap pour replier, verifier que ca repond - [ ] **TOGGLE_COMPLETE** : tap sur la checkbox d'une tache top-level, verifier que la tache disparait du widget et qu'elle est cochee dans l'app au reload - [ ] **TOGGLE_SUBTASK** : etendre une tache, tap sur la checkbox d'une sous-tache, verifier que la sous-tache se coche, le compteur (2/3) se met a jour, et les 3 widgets (small/medium/large) reflettent le changement - [ ] **No regression** : cycle d'usage normal pendant 5min, pas de crash, pas d'etat incoherent - [ ] Coller la sortie logcat dans un commentaire de l'issue #71 pour valider Phase 3 (re-mesure)
maximus added 3 commits 2026-04-19 20:18:17 +00:00
Lockfile version field was left at 1.5.1 after the 1.6.1 bump in 9a53022.
Design document for the Simpl-Liste Web frontend, Logto integration,
and hybrid mobile/web sync. Milestone spec-simpl-liste-web is fully
delivered — preserving the spec as historical reference.
Widget tap-to-expand felt slow (several seconds). Inverts the order in all
three click handlers so the widget re-renders BEFORE awaiting the
AsyncStorage write — the user sees the change immediately, persistence
finishes in the background.

- TOGGLE_COMPLETE / TOGGLE_EXPAND / TOGGLE_SUBTASK : render before persist
- EXPAND_DEBOUNCE_MS 2000 -> 600 (still blocks accidental double-taps,
  no longer feels laggy when collapsing right after expanding)
- persistState() wraps setWidgetState in try/catch — on failure the next
  handler call re-reads the prior state from AsyncStorage, UI self-heals
- Dev-only timed() helper logs each step to logcat for measurement:
  adb logcat -s ReactNativeJS | grep '\[widget\]'

Out of scope: cold start of the Android headless task service (suspected
main contributor to perceived slowness, unfixable from JS).
maximus added the
status:review
type:bug
labels 2026-04-19 20:18:26 +00:00
Author
Owner

Verdict : APPROVE

Resume

Fix cible et bien execute : render-optimiste avant persistance, debounce ramene a 600ms (raisonnable), instrumentation timing gate sur __DEV__, et persistState en best-effort avec auto-healing au prochain interaction. Code propre, commentaires expliquent les choix non-evidents (notamment pourquoi await persistState est garde malgre le pattern "optimiste").

Points positifs

  • Render-optimiste applique correctement aux 3 handlers TOGGLE_* — l'utilisateur voit le changement immediatement
  • timed() helper bien gate sur __DEV__ : zero overhead en production
  • persistState() swallow les erreurs avec un commentaire clair sur la strategie d'auto-healing
  • Debounce 600ms : bon compromis entre anti-double-tap (~300-500ms typique) et reactivite expand-then-collapse
  • Conventional commit respecte (fix: ...)
  • Pas de strings UI ajoutees — N/A pour i18n
  • Pas de secrets, pas de modifs de migrations, pas de tests skip/only

Suggestions non-bloquantes

  1. Test de regression : la checklist pr-review recommande un test de regression pour les type:bug. Le code headless Android est difficile a tester unit (mock AsyncStorage + requestWidgetUpdate), donc OK de skip, mais le PR body devrait idealement mentionner explicitement pourquoi pas de test (ou referencer une issue de suivi pour ajouter du tooling de test widget).

  2. Race condition theorique sur tap rapide multi-tache : si l'utilisateur tape rapidement sur TOGGLE_COMPLETE de 2 taches differentes, les 2 handlers peuvent lire l'etat initial avant qu'aucun ait persiste. Le 2e ecrit ecrasera la 1ere modification. Probablement seriallise par Android cote headless service, mais a verifier dans les mesures Phase 3 (logcat). Si confirme comme un probleme, l'option B (split state) du PR body est la bonne reponse.

  3. Coherence du commentaire TOGGLE_SUBTASK : le commentaire dit "Run before persist for immediate visual feedback" mais en pratique forceWidgetRefresh est awaite avant persistState. Donc l'attente d'execution du fan-out aux 3 widgets bloque encore avant la persistance. Le "avant" est vrai dans l'ordre, mais le "immediat" depend du temps que prend requestWidgetUpdate x 3. A re-mesurer en Phase 3 pour confirmer que ce n'est pas la le bottleneck.

  4. Le diff GitHub/Forgejo affiche spec-simpl-liste-web.md et package-lock.json : ces fichiers sont deja sur master a la merge-base (9cf5074). Le diff effectif est uniquement widgetTaskHandler.ts (52+/13-). Pas une issue de la PR, juste une note pour le reviewer humain qui pourrait etre confus.

Test plan a executer (rappel du PR body)

  • Build dev + logcat pour collecter les mesures Phase 3
  • Coller les mesures dans un commentaire de l'issue #71
  • Si le total reste > 1s, evaluer l'option B (split state)

Merge OK quand le test plan est valide sur appareil reel.

## Verdict : APPROVE ### Resume Fix cible et bien execute : render-optimiste avant persistance, debounce ramene a 600ms (raisonnable), instrumentation timing gate sur `__DEV__`, et persistState en best-effort avec auto-healing au prochain interaction. Code propre, commentaires expliquent les choix non-evidents (notamment pourquoi `await persistState` est garde malgre le pattern "optimiste"). ### Points positifs - Render-optimiste applique correctement aux 3 handlers `TOGGLE_*` — l'utilisateur voit le changement immediatement - `timed()` helper bien gate sur `__DEV__` : zero overhead en production - `persistState()` swallow les erreurs avec un commentaire clair sur la strategie d'auto-healing - Debounce 600ms : bon compromis entre anti-double-tap (~300-500ms typique) et reactivite expand-then-collapse - Conventional commit respecte (`fix: ...`) - Pas de strings UI ajoutees — N/A pour i18n - Pas de secrets, pas de modifs de migrations, pas de tests skip/only ### Suggestions non-bloquantes 1. **Test de regression** : la checklist `pr-review` recommande un test de regression pour les `type:bug`. Le code headless Android est difficile a tester unit (mock AsyncStorage + requestWidgetUpdate), donc OK de skip, mais le PR body devrait idealement mentionner explicitement pourquoi pas de test (ou referencer une issue de suivi pour ajouter du tooling de test widget). 2. **Race condition theorique sur tap rapide multi-tache** : si l'utilisateur tape rapidement sur TOGGLE_COMPLETE de 2 taches differentes, les 2 handlers peuvent lire l'etat initial avant qu'aucun ait persiste. Le 2e ecrit ecrasera la 1ere modification. Probablement seriallise par Android cote headless service, mais a verifier dans les mesures Phase 3 (logcat). Si confirme comme un probleme, l'option B (split state) du PR body est la bonne reponse. 3. **Coherence du commentaire TOGGLE_SUBTASK** : le commentaire dit "Run before persist for immediate visual feedback" mais en pratique `forceWidgetRefresh` est awaite avant `persistState`. Donc l'attente d'execution du fan-out aux 3 widgets bloque encore avant la persistance. Le "avant" est vrai dans l'ordre, mais le "immediat" depend du temps que prend `requestWidgetUpdate` x 3. A re-mesurer en Phase 3 pour confirmer que ce n'est pas la le bottleneck. 4. **Le diff GitHub/Forgejo affiche `spec-simpl-liste-web.md` et `package-lock.json`** : ces fichiers sont deja sur master a la merge-base (`9cf5074`). Le diff effectif est uniquement `widgetTaskHandler.ts` (52+/13-). Pas une issue de la PR, juste une note pour le reviewer humain qui pourrait etre confus. ### Test plan a executer (rappel du PR body) - [ ] Build dev + logcat pour collecter les mesures Phase 3 - [ ] Coller les mesures dans un commentaire de l'issue #71 - [ ] Si le total reste > 1s, evaluer l'option B (split state) Merge OK quand le test plan est valide sur appareil reel.
maximus merged commit 8a0cc97018 into master 2026-04-19 20:24:19 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/simpl-liste#73
No description provided.