Optimisation des performances SwiftUI pour les longues listes : solutions pratiques
Optimisation SwiftUI pour longues listes : solutions pratiques pour réduire les re-renders, garantir une identité de ligne stable, pagination, chargement d'images et défilement fluide sur iPhone anciens.

À quoi ressemblent les « listes lentes » dans de vraies apps SwiftUI
Une « liste lente » dans SwiftUI n'est généralement pas un bug. C'est le moment où l'interface n'arrive plus à suivre votre doigt. Vous le remarquez en défilant : la liste hésite, des images sont sautées, et tout semble lourd.
Signes typiques :
- Le défilement saccade, surtout sur les appareils plus anciens
- Les lignes scintillent ou affichent brièvement le mauvais contenu
- Les taps sont retardés, ou les actions de swipe démarrent en retard
- Le téléphone chauffe et la batterie se vide plus vite que prévu
- L'utilisation de la mémoire augmente au fur et à mesure du défilement
Les longues listes peuvent paraître lentes même si chaque ligne semble « petite », parce que le coût n'est pas seulement d'afficher des pixels. SwiftUI doit toujours identifier chaque ligne, calculer la mise en page, résoudre les polices et images, exécuter votre code de formatage, et diffinguer les mises à jour quand les données changent. Si une partie de ce travail se produit trop souvent, la liste devient un point chaud.
Il aide aussi de séparer deux idées. Dans SwiftUI, un « re-render » signifie souvent que le body d'une vue est recalculé. Cette partie est généralement peu coûteuse. Le travail cher est ce que ce recalcul déclenche : mise en page lourde, décodage d'images, mesure de texte, ou reconstruction de nombreuses lignes parce que SwiftUI pense que leur identité a changé.
Imaginez un chat avec 2 000 messages. De nouveaux messages arrivent chaque seconde, et chaque ligne formate des timestamps, mesure du texte multi-lignes et charge des avatars. Même si vous ajoutez un seul élément, un changement d'état mal ciblé peut pousser beaucoup de lignes à se réévaluer, et certaines à se redessiner.
L'objectif n'est pas la micro-optimisation. Vous voulez un défilement fluide, des taps instantanés, et des mises à jour qui touchent uniquement les lignes qui ont réellement changé. Les correctifs ci-dessous se concentrent sur une identité stable, des lignes moins coûteuses, moins de mises à jour inutiles, et un chargement contrôlé.
Les causes principales : identité, travail par ligne et tempêtes de mises à jour
Quand une liste SwiftUI paraît lente, ce n'est rarement « trop de lignes ». C'est du travail supplémentaire qui se passe pendant le défilement : reconstruction des lignes, recalcul de la mise en page, ou rechargement d'images en boucle.
La plupart des causes racines tombent dans trois catégories :
- Identité instable : les lignes n'ont pas d'
idconsistant, ou vous utilisez\\.selfpour des valeurs qui peuvent changer. SwiftUI ne peut pas associer les anciennes lignes aux nouvelles, donc il reconstruit plus que nécessaire. - Trop de travail par ligne : formatage de date, filtrage, redimensionnement d'images, ou tâches réseau/disque dans la vue de la ligne.
- Tempêtes de mises à jour : un changement (saisie, tick de timer, progression) déclenche des mises à jour fréquentes, et la liste se rafraîchit sans cesse.
Exemple : vous avez 2 000 commandes. Chaque ligne formate la monnaie, construit un attributed string, et lance une requête d'image. Pendant ce temps, un timer « last synced » met à jour la vue parente toutes les secondes. Même si les données de commande ne changent pas, ce timer peut invalider la liste suffisamment souvent pour rendre le défilement saccadé.
Pourquoi List et LazyVStack peuvent paraître différents
List est plus qu'une simple scroll view. Il est conçu autour du comportement table/collection et d'optimisations système. Il gère souvent les grands jeux de données avec moins de mémoire, mais il peut être sensible à l'identité et aux mises à jour fréquentes.
ScrollView + LazyVStack vous offre plus de contrôle sur la mise en page et le rendu, mais il est aussi plus facile d'y déclencher par erreur un travail de layout supplémentaire ou des mises à jour coûteuses. Sur les appareils plus anciens, ce travail supplémentaire se voit plus vite.
Avant de réécrire votre UI, mesurez d'abord. De petits correctifs comme des IDs stables, déplacer le travail hors des lignes, et réduire le churn d'état résolvent souvent le problème sans changer de conteneur.
Corriger l'identité des lignes pour que SwiftUI diff efficacement
Quand une longue liste paraît saccadée, l'identité est souvent en cause. SwiftUI décide quelles lignes peuvent être réutilisées en comparant les IDs. Si ces IDs changent, SwiftUI traite les lignes comme nouvelles, jette les anciennes, et reconstruit plus que nécessaire. Cela peut se traduire par des re-renders aléatoires, une perte de position de défilement, ou des animations qui se déclenchent sans raison.
Le gain le plus simple : rendez l'id de chaque ligne stable et lié à votre source de données.
Une erreur courante est de générer l'identité dans la vue :
ForEach(items) { item in
Row(item: item)
.id(UUID())
}
Cela force un nouvel ID à chaque rendu, donc chaque ligne devient « différente » à chaque fois.
Privilégiez les IDs qui existent déjà dans votre modèle, comme une clé primaire de base de données, un ID serveur, ou un slug stable. Si vous n'en avez pas, créez-le une fois lors de la création du modèle — pas dans la vue.
struct Item: Identifiable {
let id: Int
let title: String
}
List(items) { item in
Row(item: item)
}
Faites attention aux indices. ForEach(items.indices, id: \\.self) lie l'identité à la position. Si vous insérez, supprimez ou triez, les lignes « bougent », et SwiftUI peut réutiliser la mauvaise vue pour de mauvaises données. Utilisez les indices uniquement pour des tableaux vraiment statiques.
Si vous utilisez id: \\.self, assurez-vous que la valeur Hashable de l'élément est stable dans le temps. Si le hash change quand un champ est mis à jour, l'identité de la ligne change aussi. Une règle sûre pour Equatable et Hashable : basez-les sur un seul ID stable, pas sur des propriétés éditables comme name ou isSelected.
Vérifications de base :
- Les IDs viennent de la source de données (pas de
UUID()dans la vue) - Les IDs ne changent pas quand le contenu de la ligne change
- L'identité ne dépend pas de la position dans le tableau à moins que la liste ne soit jamais réordonnée
Réduire les re-renders en rendant les vues de ligne moins coûteuses
Une longue liste paraît souvent lente parce que chaque ligne fait trop de travail à chaque fois que SwiftUI réévalue son body. L'objectif est simple : rendre chaque ligne peu coûteuse à reconstruire.
Un coût caché fréquent est de passer des valeurs « lourdes » à une ligne. Gros structs, modèles profondément imbriqués, ou propriétés calculées lourdes peuvent déclencher du travail supplémentaire même si l'UI n'a pas changé visuellement. Vous pourriez reconstruire des chaînes, parser des dates, redimensionner des images, ou produire des arbres de layout complexes plus souvent que vous ne le pensez.
Déplacer le travail coûteux hors de body
Si quelque chose est lent, ne le reconstruisez pas sans cesse dans le body de la ligne. Précalculez-le quand les données arrivent, mettez-le en cache dans votre view model, ou mémoïsez-le dans un petit helper.
Coûts par ligne qui s'accumulent vite :
- Créer un nouveau
DateFormatterouNumberFormatterpar ligne - Formatage de chaînes lourd dans
body(joins, regex, parsing markdown) - Construire des tableaux dérivés avec
.mapou.filterdansbody - Lire de gros blobs et les convertir (par ex. décoder du JSON) dans la vue
- Layout trop complexe avec de nombreuses stacks imbriquées et conditionnelles
Un exemple simple : garder des formatters en static, et passer des chaînes déjà formatées à la ligne.
enum Formatters {
static let shortDate: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .none
return f
}()
}
struct OrderRow: View {
let title: String
let dateText: String
var body: some View {
HStack {
Text(title)
Spacer()
Text(dateText).foregroundStyle(.secondary)
}
}
}
Scinder les lignes et utiliser Equatable quand c'est pertinent
Si une petite partie seulement change (comme un badge de nombre), isolez-la dans une sous-vue pour que le reste de la ligne reste stable.
Pour une UI vraiment pilotée par des valeurs, rendre une sous-vue Equatable (ou l'encapsuler avec EquatableView) peut aider SwiftUI à sauter du travail quand les entrées n'ont pas changé. Gardez les entrées équatables petites et ciblées — pas tout le modèle.
Contrôler les mises à jour d'état qui déclenchent le rafraîchissement complet de la liste
Parfois les lignes vont bien, mais quelque chose continue de dire à SwiftUI de rafraîchir toute la liste. Pendant le défilement, même de petites mises à jour répétées peuvent provoquer des saccades, surtout sur des appareils anciens.
Une cause courante est de recréer trop souvent votre modèle. Si une vue parente se reconstruit et que vous avez utilisé @ObservedObject pour un view model que la vue possède, SwiftUI peut le recréer, réinitialiser les subscriptions, et déclencher de nouvelles publications. Si la vue possède le modèle, utilisez @StateObject pour qu'il soit créé une fois et reste stable. Utilisez @ObservedObject pour des objets injectés de l'extérieur.
Un autre tueur silencieux de performances est la publication trop fréquente. Timers, pipelines Combine, et mises à jour de progression peuvent se déclencher plusieurs fois par seconde. Si une propriété publiée affecte la liste (ou se trouve sur un ObservableObject partagé par l'écran), chaque tick peut invalider la liste.
Exemple : vous avez un champ de recherche qui met à jour query à chaque frappe, puis filtre 5 000 éléments. Si vous filtrez immédiatement, la liste se re-diff constamment pendant que l'utilisateur tape. Debouncez la requête et mettez à jour le tableau filtré après une courte pause.
Patrons qui aident généralement :
- Garder les valeurs à changement rapide en dehors de l'objet qui pilote la liste (utiliser des objets plus petits ou
@Statelocal) - Debouncez recherche et filtrage pour que la liste ne se mette à jour qu'après une pause de frappe
- Évitez les publications à haute fréquence ; mettez à jour moins souvent ou seulement quand une valeur change réellement
- Gardez l'état par ligne local (comme
@Statedans la ligne) plutôt qu'une valeur globale qui change constamment - Scindez les gros modèles : un
ObservableObjectpour les données de la liste, un autre pour l'état UI de l'écran
L'idée est simple : rendez le temps de défilement calme. Si rien d'important n'a changé, la liste ne devrait pas être invitée à faire du travail.
Choisir le bon conteneur : List vs LazyVStack
Le conteneur que vous choisissez affecte la quantité de travail que iOS fait pour vous.
List est généralement le choix le plus sûr quand votre UI ressemble à un tableau standard : lignes avec texte, images, actions de swipe, sélection, séparateurs, mode édition et accessibilité. Sous le capot, il bénéficie d'optimisations plateforme qu'Apple a peaufinées pendant des années.
Un ScrollView avec LazyVStack est idéal quand vous avez besoin d'un layout personnalisé : cartes, blocs de contenu mixtes, en-têtes spéciaux, ou un design de type feed. « Lazy » signifie qu'il construit les lignes au fur et à mesure qu'elles apparaissent à l'écran, mais il ne vous donne pas le même comportement que List dans tous les cas. Avec de très grands jeux de données, cela peut signifier une utilisation mémoire plus élevée et un défilement plus saccadé sur les appareils anciens.
Règle simple :
- Utilisez
Listpour des écrans table classiques : paramètres, inbox, commandes, listes admin - Utilisez
ScrollView+LazyVStackpour des mises en page personnalisées et du contenu mixte - Si vous avez des milliers d'items et avez juste besoin d'un tableau, commencez par
List - Si vous avez besoin d'un contrôle pixel-perfect, essayez
LazyVStack, puis mesurez mémoire et pertes de frames
Méfiez-vous aussi des styles qui ralentissent silencieusement le défilement. Les effets par ligne comme shadow, blur, et overlays complexes peuvent forcer du travail de rendu supplémentaire. Si vous voulez de la profondeur, appliquez des effets lourds à de petits éléments (icône) plutôt qu'à toute la ligne.
Exemple concret : un écran « Orders » avec 5 000 lignes reste souvent fluide dans List car les lignes sont réutilisées. Si vous passez à LazyVStack et construisez des lignes style carte avec de grosses ombres et plusieurs overlays, vous pouvez voir des saccades même si le code paraît propre.
Pagination qui paraît fluide et évite les pics de mémoire
La pagination garde les longues listes rapides parce que vous rendez moins de lignes, conservez moins de modèles en mémoire, et donnez moins de travail de diffing à SwiftUI.
Commencez avec un contrat de paging clair : une taille de page fixe (par exemple 30 à 60 items), un indicateur « plus de résultats ? », et une ligne de chargement qui n'apparaît que pendant la récupération.
Un piège courant est de déclencher la page suivante seulement quand la toute dernière ligne apparaît. C'est souvent trop tard, l'utilisateur atteint la fin et voit une pause. Chargez plutôt quand une des dernières lignes apparaît.
Voici un schéma simple :
@State private var items: [Item] = []
@State private var isLoading = false
@State private var reachedEnd = false
func loadNextPageIfNeeded(currentIndex: Int) {
guard !isLoading, !reachedEnd else { return }
let threshold = max(items.count - 5, 0)
guard currentIndex >= threshold else { return }
isLoading = true
Task {
let page = try await api.fetchPage(after: items.last?.id)
await MainActor.run {
let newUnique = page.filter { p in !items.contains(where: { $0.id == p.id }) }
items.append(contentsOf: newUnique)
reachedEnd = page.isEmpty
isLoading = false
}
}
}
Cela évite les problèmes courants comme les doublons (résultats API qui se chevauchent), les conditions de course depuis plusieurs appels onAppear, et charger trop d'items à la fois.
Si votre liste supporte le pull to refresh, réinitialisez l'état de paging soigneusement (vider items, réinitialiser reachedEnd, annuler les tâches en cours si possible). Si vous contrôlez le backend, des IDs stables et un paging par curseur rendent l'UI sensiblement plus fluide.
Images, texte et layout : garder le rendu des lignes léger
Les longues listes sont rarement lentes à cause du conteneur. La plupart du temps, c'est la ligne. Les images sont le coupable habituel : décodage, redimensionnement et dessin peuvent dépasser la vitesse de défilement, surtout sur appareils anciens.
Si vous chargez des images distantes, assurez-vous que le travail lourd ne se fait pas sur le thread principal pendant le défilement. Évitez aussi de télécharger des assets en pleine résolution pour une miniature de 44–80 pt.
Exemple : un écran « Messages » avec avatars. Si chaque ligne télécharge une image 2000×2000, la redimensionne et applique un blur ou une ombre, la liste va saccader même si votre modèle de données est simple.
Rendre le travail d'image prévisible
Bonnes pratiques à fort impact :
- Utiliser des miniatures côté serveur ou pré-générées proches de la taille affichée
- Décoder et redimensionner hors du thread principal quand c'est possible
- Cacher les miniatures pour que le défilement rapide ne refetch ni re-décode
- Utiliser un placeholder de la taille finale pour éviter flicker et sauts de layout
- Éviter les modifiers coûteux sur les images en ligne (ombres lourdes, masques, blur)
Stabiliser le layout pour éviter le thrash
SwiftUI peut passer plus de temps à mesurer qu'à dessiner si la hauteur des lignes change sans cesse. Essayez de garder des lignes prévisibles : frames fixes pour les miniatures, limites de lignes cohérentes, et espacements stables. Si le texte peut s'étendre, limitez-le (par ex. 1–2 lignes) pour qu'une mise à jour ne force pas de recalcul massif.
Les placeholders comptent aussi. Un cercle gris qui devient un avatar plus tard devrait occuper le même frame pour que la ligne ne reflow pas en plein défilement.
Comment mesurer : contrôles Instruments qui révèlent les vrais goulots d'étranglement
Travailler les performances à l'aveugle en se fiant au ressenti est hasardeux. Instruments vous dit ce qui s'exécute sur le main thread, ce qui est alloué pendant un défilement rapide, et ce qui cause des frames perdues.
Définissez une baseline sur un appareil réel (un ancien si vous le supportez). Faites une action répétable : ouvrez l'écran, scrollez du haut vers le bas rapidement, déclenchez un chargement de page, puis remontez. Notez les pires moments de hitch, le pic mémoire, et si l'UI reste réactive.
Les trois vues Instruments qui rapportent le plus
Utilisez-les ensemble :
- Time Profiler : cherchez les pics sur le main-thread pendant le défilement. Le layout, la mesure de texte, le parsing JSON et le décodage d'images expliquent souvent les hics.
- Allocations : surveillez les rafales d'objets temporaires pendant un défilement rapide. Cela pointe souvent vers des formatages répétés, des attributed strings recréés, ou des modèles par-ligne reconstruits.
- Core Animation : confirmez les frames perdues et les longs temps de frame. Cela aide à distinguer la pression de rendu du travail de données.
Quand vous trouvez un pic, cliquez dans l'arbre d'appels et demandez : cela se produit-il une fois par écran, ou une fois par ligne, par défilement ? Le second est ce qui casse le défilement fluide.
Ajouter des signposts pour les événements de scroll et pagination
Beaucoup d'apps font du travail supplémentaire pendant le défilement (chargements d'images, pagination, filtrage). Les signposts aident à voir ces moments sur la timeline.
import os
let log = OSLog(subsystem: "com.yourapp", category: "list")
os_signpost(.begin, log: log, name: "LoadMore")
// fetch next page
os_signpost(.end, log: log, name: "LoadMore")
Retestez après chaque changement, un à la fois. Si le FPS s'améliore mais que les Allocations empirent, vous avez peut-être échangé des saccades contre de la pression mémoire. Gardez vos notes baseline et ne conservez que les changements qui améliorent les bons chiffres.
Erreurs courantes qui tuent silencieusement les performances des listes
Certaines causes sont évidentes (grosses images, datasets énormes). D'autres n'apparaissent qu'avec la montée en volume, surtout sur appareils anciens.
1) IDs de lignes instables
Erreur classique : créer des IDs dans la vue, comme id: \\.self pour des reference types, ou UUID() dans le corps de la ligne. SwiftUI utilise l'identité pour diffing. Si l'ID change, SwiftUI traite la ligne comme nouvelle, la reconstruit, et peut perdre la mise en cache de layout.
Utilisez un ID stable depuis votre modèle (clé primaire, ID serveur, ou un UUID stocké une fois lors de la création de l'item). Si vous n'en avez pas, ajoutez-en un.
2) Travail lourd dans onAppear
onAppear s'exécute plus souvent qu'on ne le croit parce que les lignes entrent et sortent de l'écran pendant le défilement. Si chaque ligne démarre décodage d'image, parsing JSON, ou lookup DB dans onAppear, vous aurez des pics répétés.
Déplacez le gros travail hors de la ligne. Pré-calculer quand les données arrivent, cachez les résultats, et gardez onAppear pour des actions légères (par ex. déclencher la pagination quand on est proche de la fin).
3) Binder toute la liste aux modifications de lignes
Quand chaque ligne a un @Binding vers un grand tableau, une petite modification peut sembler être un gros changement. Cela peut pousser beaucoup de lignes à se réévaluer, et parfois rafraîchir toute la liste.
Préférez passer des valeurs immuables à la ligne et renvoyer les changements via une action légère (par ex. « toggle favorite for id »). Gardez l'état par-ligne local uniquement quand il appartient vraiment là.
4) Trop d'animations pendant le défilement
Les animations sont coûteuses dans une liste car elles peuvent déclencher des passes de layout supplémentaires. Appliquer animation(.default, value:) haut dans la hiérarchie (sur toute la liste) ou animer chaque petit changement peut rendre le défilement collant.
Gardez simple :
- Ciblez les animations sur la seule ligne qui change
- Évitez d'animer pendant un défilement rapide (surtout sélection/highlight)
- Méfiez-vous des animations implicites sur des valeurs qui changent fréquemment
- Préférez des transitions simples plutôt que des effets combinés complexes
Un exemple réel : une liste de chat où chaque ligne démarre une requête réseau dans onAppear, utilise UUID() pour id, et anime les changements de statut « seen ». Cette combinaison crée un churn constant. Corriger l'identité, mettre en cache le travail et limiter les animations rend souvent la même UI instantanément plus fluide.
Checklist rapide, un exemple simple et étapes suivantes
Si vous ne faites que quelques choses, commencez ici :
- Utilisez un
idstable et unique pour chaque ligne (pas l'indice du tableau, pas un UUID fraîchement généré) - Gardez le travail par ligne minime : évitez formatages lourds, grands arbres de vues et propriétés calculées coûteuses dans
body - Contrôlez les publications : ne laissez pas des états à changement rapide (timers, saisie, progression) invalider toute la liste
- Chargez par pages et préfetch pour que la mémoire reste plate
- Mesurez avant/après avec Instruments pour ne pas deviner
Imaginez une inbox support avec 20 000 conversations. Chaque ligne affiche un sujet, un aperçu du dernier message, un timestamp, un badge non lu, et un avatar. Les utilisateurs peuvent chercher, et de nouveaux messages arrivent pendant le défilement. La version lente fait souvent plusieurs choses à la fois : elle reconstruit les lignes à chaque frappe, re-mesure du texte trop souvent, et récupère trop d'images trop tôt.
Un plan pratique sans tout casser :
- Baseline : enregistrez un court défilement et une session de recherche dans Instruments (Time Profiler + Core Animation).
- Corriger l'identité : assurez-vous que votre modèle a un vrai id serveur/base, et que
ForEachl'utilise de façon cohérente. - Ajouter la pagination : commencez par les 50–100 éléments les plus récents, chargez-en plus quand l'utilisateur approche de la fin.
- Optimiser les images : miniatures plus petites, cache, et éviter le décodage sur le main thread.
- Re-mesurer : confirmez moins de passes de layout, moins de mises à jour de vues, et des temps de frame plus réguliers sur des appareils anciens.
Si vous construisez un produit complet (app iOS plus backend et panneau admin web), il peut aussi aider de concevoir le modèle de données et le contrat de paging tôt. Des plateformes comme AppMaster (appmaster.io) sont pensées pour ce workflow full-stack : vous pouvez définir les données et la logique métier visuellement, et générer du code source réel que vous pouvez déployer ou auto-héberger.
FAQ
Commencez par corriger l'identité des lignes. Utilisez un id stable provenant de votre modèle et évitez de générer des IDs dans la vue, car un ID qui change force SwiftUI à traiter les lignes comme entièrement nouvelles et à les reconstruire bien plus souvent que nécessaire.
Un recalcul de body est généralement peu coûteux ; ce qui coûte cher, ce sont les opérations qu'il déclenche. Les gros travaux comme la mise en page, la mesure du texte, le décodage d'images et la reconstruction de nombreuses lignes à cause d'une identité instable sont ce qui provoque typiquement des chutes d'images.
N'utilisez pas UUID() dans la vue ni les indices de tableau si les données peuvent être insérées, supprimées ou réordonnées. Préférez un identifiant serveur/base de données ou un UUID stocké sur le modèle au moment de sa création, afin que l'ID reste identique entre les mises à jour.
Oui, ça peut l'empirer, surtout si la valeur hashée change quand des champs éditables changent, parce que SwiftUI peut considérer la ligne comme différente. Si vous avez besoin de Hashable, basez-le sur un identifiant unique et stable plutôt que sur des propriétés modifiables comme name, isSelected ou du texte dérivé.
Évitez les travaux coûteux dans body. Préformatez dates et nombres, ne créez pas un nouveau DateFormatter par ligne, et ne construisez pas de grands tableaux dérivés avec map/filter dans la vue ; calculez ces valeurs une fois dans le modèle ou le view model et passez de petites valeurs prêtes à l'affichage à la ligne.
onAppear s'exécute souvent parce que les lignes entrent et sortent de l'écran pendant le défilement. Si chaque ligne lance un travail lourd là (décodage d'image, lectures base de données, parsing), vous aurez des pics répétés ; limitez onAppear à des actions peu coûteuses comme déclencher la pagination près de la fin.
Toute valeur publiée à haute fréquence partagée avec la liste peut l'invalider à répétition, même si les données des lignes n'ont pas changé. Gardez timers, saisie en cours et mises à jour de progression hors de l'objet principal qui pilote la liste, debouncez la recherche et scindez de gros ObservableObject en plus petits si nécessaire.
Choisissez List pour des écrans de type tableau (lignes standard, actions de swipe, sélection, séparateurs) pour bénéficier des optimisations système. Utilisez ScrollView + LazyVStack pour des mises en page personnalisées, mais mesurez mémoire et chutes d'images : il est plus facile d'y faire involontairement trop de travail de layout.
Commencez à charger avant d'atteindre la toute dernière ligne en déclenchant le chargement quand l'utilisateur atteint un seuil proche de la fin, et protégez-vous contre les déclenchements dupliqués. Gardez des tailles de page raisonnables, suivez isLoading et reachedEnd, et dédupliquez les résultats par IDs stables pour éviter les doublons et les diffs supplémentaires.
Faites un test de référence sur un appareil réel et utilisez Instruments pour trouver les pics sur le main-thread et les explosions d'allocations pendant un défilement rapide. Time Profiler montre ce qui bloque le défilement, Allocations révèle le churn par ligne, et Core Animation confirme les frames perdues pour distinguer la charge de rendu du travail de données.


