Mode simultané dans React: adaptation des applications Web aux appareils et à la vitesse Internet

Dans cet article, je présenterai le mode simultané dans React. Voyons de quoi il s'agit: quelles sont les fonctionnalités, quels nouveaux outils sont apparus et comment optimiser le fonctionnement des applications Web avec leur aide afin que tout puisse voler pour les utilisateurs. Le mode simultané est une nouvelle fonctionnalité de React. Sa tâche est d'adapter l'application aux différents appareils et vitesses du réseau. Jusqu'à présent, le mode simultané est une expérience qui peut être modifiée par les développeurs de la bibliothèque, ce qui signifie qu'il n'y a pas de nouveaux outils dans l'écurie. Je vous ai prévenu, et maintenant allons-y.



Actuellement, il existe deux limitations pour les composants de rendu: la puissance du processeur et le taux de transfert de données réseau. Chaque fois que quelque chose doit être montré à l'utilisateur, la version actuelle de React essaie de rendre chaque composant du début à la fin. Peu importe si l'interface peut se figer pendant quelques secondes. C'est la même histoire avec le transfert de données. React attendra absolument toutes les données dont le composant a besoin, au lieu de les dessiner pièce par pièce.







Le régime concurrentiel résout ces problèmes. Avec lui, React peut mettre en pause, hiérarchiser et même annuler des opérations qui bloquaient auparavant, de sorte qu'en mode simultané, vous pouvez démarrer le rendu des composants, que toutes les données aient été reçues ou seulement une partie de celles-ci.



Le mode simultané est l'architecture de fibre



Le mode compétitif n'est pas une nouveauté que les développeurs ont soudainement décidé d'ajouter, et tout a fonctionné là-bas. Préparé pour sa sortie à l'avance. Dans la version 16, le moteur React est passé à une architecture Fibre, qui, en principe, ressemble au planificateur de tâches dans le système d'exploitation. Le planificateur distribue les ressources de calcul entre les processus. Il est capable de basculer à tout moment, de sorte que l'utilisateur a l'illusion que les processus s'exécutent en parallèle.



L'architecture fibre fait la même chose, mais avec des composants. Malgré le fait qu'elle soit déjà dans React, l'architecture Fibre semble être en animation suspendue et n'utilise pas ses capacités au maximum. Le mode compétitif l'allumera à pleine puissance.



Lors de la mise à jour d'un composant en mode normal, vous devez dessiner un tout nouveau cadre sur l'écran. Tant que la mise à jour n'est pas terminée, l'utilisateur ne verra rien. Dans ce cas, React fonctionne de manière synchrone. La fibre utilise un concept différent. Toutes les 16 ms, il y a une interruption et un contrôle: l'arborescence virtuelle a-t-elle changé, de nouvelles données sont-elles apparues? Si tel est le cas, l'utilisateur les verra immédiatement.



Pourquoi 16ms? Les développeurs de React s'efforcent de redessiner l'écran à une vitesse proche de 60 images par seconde. Pour adapter 60 mises à jour en 1000 ms, vous devez les faire environ toutes les 16 ms. D'où le chiffre. Le mode compétitif sort de la boîte et ajoute de nouveaux outils qui améliorent la vie du front-end. Je vais vous parler de chacun en détail.



Le suspense



Suspense a été introduit dans React 16.6 comme mécanisme de chargement dynamique des composants. En mode concurrent, cette logique est conservée, mais des opportunités supplémentaires apparaissent. Suspense devient un mécanisme qui fonctionne en conjonction avec la bibliothèque de chargement de données. Nous demandons une ressource spéciale via la bibliothèque et en lisons les données.



Suspense lit simultanément les données qui ne sont pas encore prêtes. Comment? Nous demandons les données, et jusqu'à ce qu'elles soient complètes, nous commençons déjà à les lire en petits morceaux. La chose la plus cool pour les développeurs est de gérer l'ordre dans lequel les données chargées sont affichées. Suspense vous permet d'afficher les composants de la page simultanément et indépendamment les uns des autres. Cela rend le code simple: il vous suffit de regarder la structure Suspense pour voir dans quel ordre les données sont demandées.



Une solution typique pour charger des pages dans "l'ancien" React est Fetch-On-Render. Dans ce cas, nous demandons des données après le rendu dans useEffect ou componentDidMount. C'est la logique standard lorsqu'il n'y a pas de Redux ou autre couche de données. Par exemple, nous voulons dessiner 2 composants, dont chacun a besoin de données:



  • Demande de composant 1
  • Attente…
  • Obtenir des données -> composant de rendu 1
  • Demande de composant 2
  • Attente…
  • Obtenir des données -> composant de rendu 2


Dans cette approche, le composant suivant n'est demandé qu'après le rendu du premier. C'est long et peu pratique.



Considérons une autre façon, Fetch-Then-Render: d'abord nous demandons toutes les données, puis nous rendons la page.



  • Demande de composant 1
  • Demande de composant 2
  • Attente…
  • Obtenir le composant 1
  • Obtention du composant 2
  • Rendu des composants


Dans ce cas, nous déplaçons l'état de la requête quelque part vers le haut - nous le déléguons à la bibliothèque pour travailler avec les données. La méthode fonctionne très bien, mais il y a une nuance. Si l'un des composants prend beaucoup plus de temps à se charger que l'autre, l'utilisateur ne verra rien, même si nous pourrions déjà lui montrer quelque chose. Regardons un exemple de code de la démo avec 2 composants: utilisateur et publications. Nous enveloppons les composants dans Suspense:



const resource = fetchData() // -    React
function Page({ resource }) {
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </Suspense>
    )
}


Il peut sembler que cette approche est proche de Fetch-On-Render, lorsque nous avons demandé des données après le rendu du premier composant. Mais en fait, l'utilisation de Suspense obtiendra les données beaucoup plus rapidement. Cela est dû au fait que les deux requêtes sont envoyées en parallèle.



Dans Suspense, vous pouvez spécifier le remplacement, le composant que vous souhaitez afficher et transmettre la ressource implémentée par la bibliothèque de récupération de données à l'intérieur du composant. Nous l'utilisons tel quel. À l'intérieur des composants, nous demandons des données à la ressource et appelons la méthode read. Telle est la promesse que la bibliothèque nous fait. Suspense comprendra si les données ont été chargées, et si oui, le montrera.



Notez que les composants essaient de lire des données qui sont toujours en cours de réception:



function User() {
    const user = resource.user.read()
    return <h1>{user.name}</h1>
}
function Posts() {
    const posts = resource.posts.read()
    return //  
}


Dans les démos actuelles de Dan Abramov, une telle chose est utilisée comme un stub pour une ressource .



read() {
    if (status === 'pending') {
        throw suspender
    } else if (status === 'error') {
        throw result
    } else if (status === 'success') {
        return result
    }
}




Si la ressource est toujours en cours de chargement, nous lançons l'objet Promise comme une exception. Suspense intercepte cette exception, se rend compte qu'il s'agit d'une promesse et continue le chargement. Si, au lieu d'une promesse, une exception avec un autre objet arrive, il deviendra clair que la demande s'est terminée par une erreur. Lorsque le résultat final est renvoyé, Suspense l'affichera. Il est important pour nous d'obtenir une ressource et d'appeler une méthode dessus. La manière dont il est implémenté en interne est une décision des développeurs de la bibliothèque, l'essentiel est que Suspense comprenne leur implémentation.



Quand demander des données? Poser des questions au sommet de l'arbre n'est pas une bonne idée, car ils peuvent ne jamais être nécessaires. Une meilleure option est de le faire tout de suite lors de la navigation à l'intérieur des gestionnaires d'événements. Par exemple, obtenez l'état initial via un hook, puis effectuez une demande de ressources dès que l'utilisateur clique sur le bouton.



Voici à quoi cela ressemblera dans le code:



function App() {
    const [resource, setResource] = useState(initialResource)
    return (
        <>
            <Button text='' onClick={() => {
                setResource(fetchData())
            }}>
            <Page resource={resource} />
        </>
    );
}


Suspense est incroyablement flexible. Il peut être utilisé pour afficher les composants les uns après les autres.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </Suspense>
)


Ou en même temps, les deux composants doivent être enveloppés dans un seul Suspense.



return (
    <Suspense fallback={<h1>Loading user and posts...</h1>}>
        <User />
        <Posts />
    </Suspense>
)


Ou, chargez les composants séparément les uns des autres en les enveloppant dans Suspense indépendant. La ressource sera chargée via la bibliothèque. C'est très cool et pratique.



return (
    <>
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User />
        </Suspense>
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </>
)


En outre, les composants Error Boundary intercepteront les erreurs dans Suspense. Si quelque chose ne va pas, nous pouvons montrer que l'utilisateur a chargé, mais pas les messages, et donner une erreur.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User resource={resource} />
        <ErrorBoundary fallback={<h2>Could not fetch posts</h2>}>
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </ErrorBoundary>
    </Suspense>
)


Jetons maintenant un coup d'œil à d'autres outils qui peuvent tirer pleinement parti des avantages du régime concurrentiel.



SuspenseList



SuspenseList aide simultanément à contrôler l'ordre de chargement de Suspense. Si nous devions charger plusieurs Suspense strictement l'un après l'autre sans cela, ils devraient être imbriqués les uns dans les autres:



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
            <Suspense fallback={<h1>Loading facts...</h1>}>
                <Facts />
            </Suspense>
        </Suspense>
    </Suspense>
)


SuspenseList rend cela beaucoup plus facile:



return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
        <Suspense fallback={<h1>Loading facts...</h1>}>
            <Facts />
        </Suspense>
    </Suspense>
)


La flexibilité de SuspenseList est incroyable. Vous pouvez imbriquer SuspenseList les uns dans les autres comme vous le souhaitez et personnaliser l'ordre de chargement à l'intérieur car cela sera pratique pour afficher les widgets et tout autre composant.



useTransition



Un hook spécial qui reporte la mise à jour du composant jusqu'à ce qu'il soit complètement prêt et supprime l'état de chargement intermédiaire. Pourquoi est-ce? React s'efforce de faire la transition le plus rapidement possible lors du changement d'état. Mais parfois, il est important de prendre son temps. Si une partie des données est chargée sur une action de l'utilisateur, alors généralement au moment du chargement, nous montrons un chargeur ou un squelette. Si les données arrivent très rapidement, le chargeur n'aura pas le temps d'effectuer même un demi-tour. Il clignotera, puis disparaîtra, et nous dessinerons le composant mis à jour. Dans de tels cas, il est plus sage de ne pas montrer du tout le chargeur.



C'est là qu'intervient useTransition. Comment ça marche dans le code? Nous appelons le hook useTransition et spécifions le délai en millisecondes. Si les données ne sont pas fournies dans le délai spécifié, nous afficherons toujours le chargeur. Mais si nous les obtenons plus rapidement, il y aura une transition instantanée.



function App() {
    const [resource, setResource] = useState(initialResource)
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })
    return <>
        <Button text='' disabled={isPending} onClick={() => {
            startTransition(() => {
                setResource(fetchData())
            })
        }}>
        <Page resource={resource} />
    </>
}


Parfois, nous ne voulons pas afficher le chargeur lorsque nous allons sur la page, mais nous devons encore changer quelque chose dans l'interface. Par exemple, pendant la durée de la transition, bloquez le bouton. Ensuite, la propriété isPending vous sera utile - elle vous informera que nous sommes dans la phase de transition. Pour l'utilisateur, la mise à jour sera instantanée, mais il est important de noter ici que la magie useTransition n'affecte que les composants enveloppés dans Suspense. UseTransition lui-même ne fonctionnera pas.



Les transitions sont courantes dans les interfaces. La logique responsable de la transition serait géniale à coudre dans le bouton et à intégrer dans la bibliothèque. S'il existe un composant responsable des transitions entre les pages, vous pouvez encapsuler le onClick dans handleClick, qui est passé par les accessoires au bouton, et afficher l'état isDisabled.



function Button({ text, onClick }) {
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })

    function handleClick() {
        startTransition(() => {
            onClick()
        })
    }

    return <button onClick={handleClick} disabled={isPending}>text</button>
}


useDeferredValue



Donc, il y a un composant avec lequel nous faisons des transitions. Parfois, la situation suivante se produit: l'utilisateur souhaite accéder à une autre page, nous avons reçu certaines des données et sommes prêts à les afficher. Dans le même temps, les pages diffèrent légèrement les unes des autres. Dans ce cas, il serait logique d'afficher les données obsolètes de l'utilisateur jusqu'à ce que tout le reste soit chargé.



Désormais, React ne sait pas comment faire: dans la version actuelle, seules les données de l'état actuel peuvent être affichées sur l'écran de l'utilisateur. Mais useDeferredValue en mode concurrent peut renvoyer une version différée de la valeur, afficher des données obsolètes au lieu d'un chargeur clignotant ou de secours au moment du démarrage. Ce hook prend la valeur pour laquelle nous voulons obtenir la version différée et le délai en millisecondes.



L'interface devient super fluide. Les mises à jour peuvent être effectuées avec un minimum de données, et tout le reste est chargé progressivement. L'utilisateur a l'impression que l'application est rapide et fluide. En action, useDeferredValue ressemble à ceci:



function Page({ resource }) {
    const deferredResource = useDeferredValue(resource, { timeoutMs: 1000 })
    const isDeferred = resource !== deferredResource;
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={deferredResource} isDeferred={isDeferred}/>
            </Suspense>
        </Suspense>
    )
}


Vous pouvez comparer la valeur des accessoires avec celle obtenue via useDeferredValue. S'ils diffèrent, la page est toujours en cours de chargement.



Fait intéressant, useDeferredValue vous permettra de répéter l'astuce du chargement paresseux, non seulement pour les données transmises sur le réseau, mais également afin de supprimer le gel de l'interface en raison de gros calculs.



Pourquoi est-ce génial? Différents appareils fonctionnent différemment. Si vous exécutez une application utilisant useDeferredValue sur un nouvel iPhone, la transition de page en page sera instantanée, même si les pages sont lourdes. Mais lors de l'utilisation de debounce, le retard apparaîtra même sur un appareil puissant. UseDeferredValue et le mode concurrent s'adaptent au matériel: s'il fonctionne lentement, l'entrée continuera de voler et la page elle-même sera mise à jour comme le périphérique le permet.



Comment basculer un projet en mode simultané?



Le mode compétitif est un mode, vous devez donc l'activer. Comme un interrupteur à bascule qui fait fonctionner la fibre à pleine capacité. Par où commencez-vous?



Nous supprimons l'héritage. Nous nous débarrassons de toutes les méthodes obsolètes du code et nous nous assurons qu'elles ne sont pas dans les bibliothèques. Si l'application fonctionne correctement dans React.StrictMode, tout va bien - le déménagement sera facile. La complication potentielle réside dans les problèmes au sein des bibliothèques. Dans ce cas, vous devez soit mettre à niveau vers une nouvelle version, soit modifier la bibliothèque. Ou abandonner le régime concurrentiel. Après s'être débarrassé de l'héritage, il ne reste plus qu'à changer de racine.



Avec l'arrivée du mode simultané, trois modes de connexion racine seront disponibles:



  • L'ancien mode de

    ReactDOM.render(<App />, rootNode)

    rendu sera obsolète après la sortie du mode compétitif.
  • Mode de blocage

    ReactDOM.createBlockingRoot(rootNode).render(<App />)

    En tant qu'étape intermédiaire, un mode de blocage sera ajouté, qui donne accès à une partie des opportunités du mode compétitif sur des projets où il y a des héritages ou d'autres difficultés de délocalisation.
  • Mode compétitif

    ReactDOM.createRoot(rootNode).render(<App />)

    Si tout va bien, il n'y a pas d'héritage et le projet peut être changé immédiatement, remplacez le rendu du projet par createRoot - et partez pour un avenir radieux.


conclusions



Les opérations de blocage à l'intérieur de React sont rendues asynchrones en passant à la fibre. De nouveaux outils font leur apparition qui facilitent l'adaptation de l'application aux capacités de l'appareil et à la vitesse du réseau:



  • Suspense, grâce auquel vous pouvez spécifier l'ordre de chargement des données.
  • SuspenseList, ce qui le rend encore plus pratique.
  • useTransition pour créer des transitions fluides entre les composants encapsulés Suspense.
  • useDeferredValue - pour afficher les données périmées lors des mises à jour des E / S et des composants


Essayez d'expérimenter le mode simultané alors qu'il est toujours absent. Le mode simultané vous permet d'obtenir des résultats impressionnants: chargement rapide et fluide des composants dans n'importe quel ordre pratique, interface super fluide. Les détails sont décrits dans la documentation, il existe des démos avec des exemples qui méritent d'être explorés par vous-même. Et si vous êtes curieux de savoir comment fonctionne l'architecture Fibre, voici un lien vers une présentation intéressante.



Évaluez vos projets - que peut-on améliorer avec les nouveaux outils? Et lorsque le régime concurrentiel est sorti, n'hésitez pas à bouger. Tout ira bien!



All Articles