Accélération instagram.com. 3e partie

Aujourd'hui, nous publions une traduction de la troisième partie d'une série de documents sur l'accélération instagram.com. Dans la première partie, nous avons parlé du préchargement des données, dans la seconde , de l'envoi de données au client à l'initiative du serveur. Il s'agit de la mise en cache.



Le travail commence par un cache


Nous envoyons déjà des données à l'application cliente, en procédant le plus tôt possible lors du chargement de la page. Cela signifie que le seul moyen plus rapide de fournir des données serait celui qui n'implique pas du tout des étapes liées à la demande d'informations par le client ou à son envoi au client à l'initiative du serveur. Cela peut être fait en utilisant cette approche de la formation des pages, dans laquelle le cache vient au premier plan. Cela signifie cependant que nous devrons, bien que très brièvement, montrer à l'utilisateur des informations obsolètes. En utilisant cette approche, après avoir chargé la page, nous montrons immédiatement à l'utilisateur une copie en cache de son flux et de ses histoires, puis, une fois les dernières données disponibles, nous remplaçons tout cela par de telles données.

Nous utilisons Redux pour gérer le statut instagram.com. En conséquence, le plan de mise en œuvre global du schéma ci-dessus ressemble à ceci. Nous stockons un sous-ensemble du référentiel Redux sur le client, dans la table indexedDB, remplissant ce référentiel lors du premier chargement de la page. Cependant, travailler avec indexedDB, télécharger des données depuis le serveur et interaction de l'utilisateur avec la page sont des processus asynchrones. En conséquence, nous pouvons rencontrer des problèmes. Ils consistent dans le fait que l'utilisateur travaille avec l'ancien état mis en cache, et nous devons appliquer les actions de l'utilisateur au nouvel état lorsqu'il le reçoit du serveur.

Par exemple, si nous utilisons les mécanismes standard pour travailler avec le cache, nous pouvons rencontrer le problème suivant. Nous commençons le chargement parallèle des données à partir du cache et du réseau. Étant donné que les données du cache seront prêtes plus rapidement que les données du réseau, nous les montrons à l'utilisateur. L'utilisateur aime alors, par exemple, le message, mais après que la réponse du serveur, qui contient les dernières informations, arrive au client, ces informations écrasent les informations sur le message aimé. Dans ces nouvelles données, il n'y aura aucune information sur les goûts selon lesquels l'utilisateur a défini la version en cache de la publication. Voici à quoi ça ressemble.


État de concurrence critique qui se produit lorsqu'un utilisateur interagit avec des données mises en cache (les actions Redux sont surlignées en vert, le statut est gris)

Pour résoudre ce problème, nous devions changer l'état mis en cache conformément aux actions de l'utilisateur et enregistrer des informations sur ces actions, ce qui nous permettrait de les reproduire telles qu'elles étaient appliquées au nouvel état reçu du serveur. Si vous avez déjà utilisé Git ou un autre système de contrôle de version, cette tâche peut vous sembler familière. Supposons que l'état mis en cache de la bande soit la branche du référentiel local et que la réponse du serveur avec les dernières données soit la branche principale. Si c'est le cas, nous pouvons dire que nous voulons effectuer l'opération de relocalisation, c'est-à-dire que nous voulons prendre les modifications enregistrées dans une branche (par exemple, j'aime, commentaires, etc.) et les appliquer à une autre.

Cette idée nous amène à l'architecture système suivante:

  • Lorsque la page se charge, nous envoyons une demande au serveur pour télécharger de nouvelles données (ou attendons qu'elles soient envoyées à l'initiative du serveur).
  • Créez un sous-ensemble intermédiaire (intermédiaire) de l'état Redux.
  • En attendant les données du serveur, nous enregistrons les actions soumises.
  • Après avoir reçu des données du serveur, nous effectuons des actions avec les nouvelles données et lisons les actions stockées sur les nouvelles données, en les appliquant à l'état intermédiaire.
  • Après cela, nous validons les modifications et remplaçons l'état actuel par un état intermédiaire.


Résolution d'un problème causé par une condition de concurrence critique à l'aide d'un état intermédiaire (les actions Redux sont surlignées en vert, le statut est gris)

Grâce à l'état intermédiaire, nous pouvons réutiliser tous les réducteurs existants. De plus, cela vous permet de stocker un état intermédiaire (qui contient les dernières données) séparément de l'état actuel. Et comme le travail avec l’état intermédiaire est implémenté en utilisant Redux, il nous suffit d’envoyer des actions pour utiliser cet état!

API


L'API à état intermédiaire comprend deux fonctions principales. Il s'agit de stagingCommit et stagingCommit :

 function stagingAction(    key: string,    promise: Promise<Action>, ): AsyncAction<State, Action> function stagingCommit(key: string): AsyncAction<State, Action> 

Il existe plusieurs autres fonctions, par exemple, pour annuler les modifications et pour gérer les cas de frontière, mais nous ne les examinerons pas ici.

La fonction stagingAction accepte une promesse se résolvant en un événement qui doit être envoyé à un état intermédiaire. Cette fonction initialise un état intermédiaire et surveille les actions qui ont été envoyées depuis son initialisation. Si nous comparons cela avec les systèmes de contrôle de version, il s'avère que nous avons affaire à la création d'une branche locale. Les actions en cours seront mises en file d'attente et appliquées à l'état provisoire après l'arrivée de nouvelles données.

La fonction stagingCommit remplace l'état actuel par un état intermédiaire. De plus, si l'on s'attend à ce que certaines opérations asynchrones soient effectuées qui sont effectuées sur un état intermédiaire, le système attendra que ces opérations soient terminées avant de les remplacer. Cela est similaire à l'opération de relocalisation lorsque des modifications locales (de la branche qui stocke le cache) sont appliquées au-dessus de la branche principale (au-dessus des nouvelles données reçues du serveur), ce qui conduit au fait que la version locale de l'état est à jour.

Afin d'activer le système de travail avec un état intermédiaire, nous avons enveloppé le réducteur racine dans les capacités d'extension du réducteur. Il traite l'action stagingCommit et applique les actions précédemment enregistrées au nouvel état. Afin de profiter de tout cela, nous n'avons qu'à envoyer des actions, et tout le reste se fera automatiquement. Par exemple, si nous voulons charger une nouvelle bande et mettre ses données dans un état intermédiaire, nous pouvons faire quelque chose comme ceci:

 function fetchAndStageFeed() {    return stagingAction(        'feed',        (async () => {            const {data} = await fetchFeedTimeline();            return {                type: FEED_LOADED,                ...data,            };        })(),    ); } //          store.dispatch(fetchAndStageFeed()); //   ,    stagingCommit, //      'feed' //      store.dispatch(stagingCommit('feed')); 

L'utilisation d'une approche de rendu pour le flux et les histoires, dans laquelle le cache apparaît au premier plan, a accéléré la production de documents de 2,5% et 11%, respectivement. De plus, cela a contribué au fait que, dans la perception des utilisateurs, la version Web du système se rapprochait des clients Instagram pour iOS et Android.

Chers lecteurs! Utilisez-vous des approches pour optimiser la mise en cache lorsque vous travaillez sur vos projets?


Source: https://habr.com/ru/post/fr472336/


All Articles