Aventures dans un flux séparé. Rapport Yandex

Comment travailler avec des images sur le client, tout en conservant une interface utilisateur fluide? Le développeur de l'interface Pavel Smirnov en a parlé sur la base de l'expérience de développement de la recherche de photographies sur le marché. À partir du rapport, vous pouvez apprendre à utiliser correctement Web Workers et OffscreenCanvas.



- Pendant cette demi-heure, nous parlerons d'aventures. Je raconterai mon aventure et j'espère vraiment que mon rapport vous inspirera et que vous prendrez et ferez de même à la maison.

Au début, je voulais parler de certaines technologies nouvelles ou pas très nouvelles que nos navigateurs nous donnent et qui nous permettent de faire des choses sympas. Mais il me semble que ce ne serait pas très amusant, car tout le monde peut aller sur MDN et lire quelque chose. Par conséquent, je vais raconter l'histoire d'une fonctionnalité que j'ai réalisée avec l'équipe Market.

Présentons-moi encore une fois. Je m'appelle Pasha, je suis développeur d'interfaces au sein de l'équipe Market.



Je m'occupe principalement des interfaces mobiles - recherche de carte, carte d'offre. Je réécris également le code de l'ancienne pile vers la nouvelle, puis de la nouvelle vers une pile encore plus récente. Et j'essaye de rendre mes interfaces bonnes. Ici, il vaut la peine de dire ce qu'est une bonne interface.

Les bonnes interfaces ont des caractéristiques différentes. Premièrement, c'est pratique; deuxièmement, c'est beau; troisièmement, c'est abordable. Mais l'une des caractéristiques dont je veux parler aujourd'hui est la vitesse. Et la vitesse se manifeste souvent dans la finesse de son travail. Même de petites frises peuvent changer considérablement l'expérience utilisateur de nos interfaces.



Passons au plan de ma conversation d'aujourd'hui. Nous allons d'abord parler de la tâche que j'ai accomplie: trouver une image sur le marché. Ensuite, je vais vous dire quels problèmes j'ai dû résoudre pour implémenter cette fonctionnalité. Ici, nous rappelons un peu comment fonctionne votre script dans le navigateur et regardons les technologies qui m'ont aidé. Petit spoiler: ce sont Web Workers et OffscreenCanvas.

Revenons à la tâche. Il y a quelques mois, Luba, notre chef de produit, m'a approché. Lyuba s'occupe des problèmes de choix d'un produit sur le marché. Nous avons maintenant plusieurs options pour trouver des marchandises. L'un d'eux consiste à saisir quelque chose dans la barre de recherche.



Par exemple, «achetez un iPhone X rouge à Samara». Et nous trouverons quelque chose. Ou nous pouvons utiliser l'arborescence du catalogue. Dans ce catalogue, nous avons des catégories et sous-catégories.

Mais que se passe-t-il si je veux trouver quelque chose sur le marché, sans savoir comment ça s'appelle, mais soit j'ai une photo de cette chose, soit je la vois à la fête de quelqu'un?



Je vais raconter un cas réel. Je suis allé une fois avec mes amis dans un café. Nous avons commandé de la limonade là-bas, vous savez, dans une telle cruche, et cette cruche avait une chose tellement étrange. J'ai même gardé une photo. Il était destiné à ce que lorsque vous versez de la limonade dans un verre, la glace n'y pénètre pas. Nous pensions que c'était une bonne chose, mais nous avions des opinions différentes sur le nom de cette chose et, en général, à quoi elle était destinée. Par conséquent, nous l'avons trouvé sur Yandex.Pictures.

Mais j'ai pensé - ce serait cool si je pouvais non seulement rechercher cette chose, mais aussi l'acheter immédiatement ou au moins trouver le prix, lire les avis, les spécifications, etc. À ce stade, nos rêves ont coïncidé avec Any, et nous avons décidé faire une telle fonctionnalité sur le marché.

À quoi ressemble cette fonctionnalité? Il permet à l'utilisateur de télécharger une photo ou une image, vous pouvez même immédiatement prendre une photo et l'envoyer au marché. Nous analysons cette photo en utilisant les technologies de recherche Yandex, trouvons un produit dessus et montrons à l'utilisateur les résultats avec ces produits. Cela semble simple, mais si c'était aussi simple, je ne ferais pas mon rapport. Pour vous assurer de ce type de fonctionnalité, permettez-moi de le montrer.

Regardez la première démo

Je vais montrer sur la production. Commençons par télécharger la chose que nous recherchions et voyons ce qui se passe.

Nous avons trouvé des marchandises et plus précisément cette chose. Cette chose s'appelle une passoire. Pour trouver autre chose, hier j'ai pris une photo d'un collègue sur une table sur une table, cherchons-la. Voici un tel livre, peut-être que quelqu'un l'a lu. Cela s'appelle "Perfect Code". Il le trouve également d'une manière ou d'une autre, et pour une raison quelconque, avec une limite de 18+. C'est probablement un peu étrange.

Revenons à notre rapport. Quels problèmes ai-je rencontrés? Le premier problème est que l'utilisateur commence à télécharger quoi que ce soit, y compris des images énormes. Par exemple, mon téléphone prend des photos de trois à quatre mégaoctets, ce qui est beaucoup. L'envoi de telles photos au backend est inefficace. Cela prend beaucoup de temps, cela prend beaucoup de temps pour les analyser, vous devez donc faire quelque chose. Mais ici, tout est simple - nous recadrerons, compresserons, redimensionnerons cette photo sur le client.



Comment allons-nous procéder? Nous avons un dossier. Et nous lirons en quelque sorte ce fichier. Nous lirons à l'aide de l'API FileReader. Je vais vous dire brièvement ce que c'est.



Il s'agit d'une telle API de navigateur qui nous permet de lire le fichier téléchargé et de faire quelque chose avec. Vous pouvez lire de différentes manières, nous allons l'examiner maintenant. Voici ses fonctionnalités, et nous avons une sorte d'objet qui nous est retourné après avoir été entré par l'événement change. Essayons de le lire.



Le code ressemblera à ceci. Il n'y a encore rien de compliqué ici. Nous avons un objet Reader créé à partir du constructeur FileReader, sur lequel nous suspendons le développeur de l'événement de chargement. Ensuite, nous lirons ce fichier en tant que DataURL. DataURL - une chaîne qui représente le contenu du fichier encodé via Base64. Comme nous lisons, nous devons le couper en quelque sorte. Tout d'abord, chargeons le tout dans une image. Nous avons un élément tag ou img, et nous le chargeons ici.



Le code ressemblera à ceci. Nous créons un élément img, par l'événement load Reader nous chargeons notre ligne dans l'attribut src et nous ferons tout plus loin lorsque notre ligne aura fini de se charger dans img.

Nous ferons ce que nous voulions - recadrer l'image. Nous allons le compresser, et ici une chose telle que Canvas nous aidera, un outil très puissant. Cela vous permet de faire beaucoup. Mais ici, nous dessinons simplement notre image sur cette toile, et si les tailles d'image dépassent le maximum autorisé, nous les adapterons un peu. De plus, nous pouvons prendre cette image avec Canvas du taux de compression souhaité.



Quelque chose comme ça. Autre petit avertissement: le code ici est grandement simplifié, je ne précise pas tout. Nous avons la gestion des erreurs et d'autres choses, mais pour que tout tienne sur la diapositive et soit clair dans le rapport, j'ai omis certains détails.

Nous avons des tailles d'image, nous les regardons simplement. Certaines constantes nous sont autorisées. Si les tailles des images dépassent nos constantes, nous les coupons juste en dessous et définissons notre toile à ces mêmes tailles.

Ensuite, nous allons dessiner notre image sur cette toile.



Prenez le contexte 2D, nous avons besoin d'une image 2D et essayez de dessiner en utilisant la méthode drawImage. DrawImage est une méthode intéressante qui accepte, si je ne me trompe, neuf paramètres. Mais ils ne sont pas tous obligatoires, nous n'en utiliserons que cinq. Nous prenons Image et ces deux zéros, c'est un décalage ou une indentation de l'image. Nous avons besoin du point en haut à gauche. Dessinez avec les dimensions dont nous avons besoin.

De plus, à partir de ce canevas, nous prendrons notre chaîne Base64 encodée DataURL exactement de la même manière et la transformerons en blob - un objet spécial qui est pratique pour nous à envoyer au serveur. Il semble que ce soit tout. Tout fonctionne. L'image est rognée, l'image est envoyée, l'image est reconnue.

Mais alors j'ai commencé à remarquer quelque chose. Lorsque j'ai testé cette solution, lorsque j'ai téléchargé une image, en particulier sur des appareils faibles, mon interface s'est un peu ralentie. Soit le bouton n'a pas été enfoncé, alors l'élément n'a pas défilé ainsi. Avez-vous eu l'impression que votre code fonctionne dans 99% des cas et fonctionne bien, mais parfois il ne fonctionne tout simplement pas? Et vous pouvez le donner pour les tests, et probablement personne ne le remarquera. Et les utilisateurs ne le remarqueront probablement pas, en particulier sur les appareils faibles.

Cela ne m'est jamais arrivé et j'ai décidé de le réparer. Cela s'est avéré être un problème. Si l'image est grande, alors pendant les manipulations avec recadrage, compression, cela nous a pris du temps, et en ce petit, petit temps, notre interface ne répondait pas.

Au début, j'ai compris pourquoi cela se produisait. Ici, il vaut la peine de se rappeler comment JavaScript fonctionne dans le navigateur. Je n'entrerai pas dans les détails, c'est un sujet pour un gros rapport. N'oubliez pas certains points.



Nous avons JavaScript en cours d'exécution dans un seul thread, appelons-le principal. Et nous avons une telle chose dans le navigateur comme une boucle d'événements. Ici, nous disons immédiatement qu'il s'agit d'un modèle. Dans certains navigateurs, la boucle d'événements est organisée différemment, mais comme son nom l'indique, il s'agit généralement d'une boucle. Il traite certaines tâches dans la file d'attente dans l'ordre.

Un moment désagréable: tant qu'il n'aura pas exécuté une tâche, il ne passera pas à la suivante. Je vais montrer la démo que j'ai vue, elle la montre. C'est un classique.

Regardez la deuxième démo

J'ai une image GIF et une animation CSS réalisées de différentes manières: l'une en utilisant translatex, l'autre en position: relative à gauche, la troisième en JavaScript, à savoir requestAnimationFrame. C'est là que le hérisson tourne. Que vais-je faire?

Je vais bloquer le thread principal pendant cinq secondes. Vous savez, les durs à cuire calculent généralement le nième nombre de Fibonacci, mais j'ai écrit une boucle sans fin avec une pause en cinq secondes.

Que va-t-il se passer? Vous avez immédiatement remarqué que le hérisson a cessé de tourner et que le chat inférieur, animé par translatex, a également arrêté de rouler. Mais voyons la même démo dans un autre navigateur, par exemple Safari. Le chat GIF a cessé de courir.

Pourquoi est-ce que je montre tout ça? Premièrement, les navigateurs sont différents, vous devez en tenir compte. Deuxièmement, lorsque notre flux est bloqué par quelque chose, certaines choses cesseront de fonctionner. Par exemple - animation JavaScript. Ou bien montrons que le texte ne se distinguera plus pour nous, les boutons ne seront plus pressés.

Ceci est un exemple très abstrait. Ne bloquons pas le flux pendant cinq secondes, mais prenons notre tâche, téléchargeons une photo, recadrez-la, pressez-la et dessinez-la ici. Nous ne l'enverrons nulle part, ce ne sera pas très révélateur.

Regardez la troisième démo

J'ai un MacBook puissant ici, et pour que tout soit plus convaincant, nous allons ralentir le processeur de six fois. Cela vous permet de faire des DevTools. Téléchargez notre photo. Le code parfait nous aidera à nouveau. Comme nous le voyons, la même chose se produit que lors du blocage du thread principal.

Revenons ensuite à notre tâche et réfléchissons à la manière dont nous allons y faire face.



Soit dit en passant, si vous regardez le profileur, nous le verrons. Dans le cadre rouge est notre microtâche, qui bloque le fil principal. On voit qu'il le bloque pendant près de cinq secondes. C'est sur un ordinateur assez puissant, et sur des appareils plus faibles, ce sera encore plus visible.

Passons à la solution. Je dirai tout de suite ce que j'ai utilisé et ce que j'ai fait, puis nous analyserons toutes ces choses. Tout d'abord, j'ai utilisé Web Workers. Ils nous permettent de mettre certaines tâches dans un thread séparé. Et deuxièmement, dans le contexte des Web Workers, le DOM n'est pas disponible pour nous. Pour faire face à cette situation, nous utiliserons d'autres outils. L'image ne sera pas disponible pour nous, le canevas classique est disponible, et donc nous utilisons le canevas et quelques autres astuces.



Rappelons-nous rapidement ce que sont les travailleurs, à quoi ils servent. Ils vous permettent d'exécuter JavaScript dans un thread séparé, pas principalement. Et le flux Workers n'interfère pas avec le flux de rendu de l'interface principale. Par conséquent, nous pouvons effectuer des tâches de calcul complexes sans ralentir notre interface.

Nous avons un outil qui vous permet de transférer quelque chose aux travailleurs et de retourner quelque chose des travailleurs. Voyons un exemple.



Nous créons donc notre Worker en utilisant le constructeur. Là, vous devez transférer le chemin d'accès au fichier. On peut même passer blob. Et nous avons un gestionnaire d'événements Message. Dans ce cas, il affichera simplement quelque chose à l'écran. Ensuite, nous pouvons envoyer des données à notre travailleur.



Quel est le support? Tout va bien ici. Les travailleurs sont un outil bien connu, pas nouveau, mais beaucoup de mes amis pensent qu'ils ne sont pas toujours soutenus. Ce n'est pas le cas.



Regardons maintenant OffscreenCanvas. Comme nous l'avons déjà vu, Canvas est un outil très puissant, mais, malheureusement, il n'est pas disponible pour nous dans le contexte des Web Workers, nous allons donc utiliser une alternative. C'est une chose assez nouvelle appelée OffscreenCanvas. Il vous permet de faire à peu près les mêmes choses que Canvas, seulement déjà hors de l'écran, c'est-à-dire dans le contexte de Web Workers. Bien sûr, nous pouvons le faire également dans le contexte de la fenêtre, mais maintenant nous ne le ferons pas.



Qu'y a-t-il avec le support? Comme vous pouvez le voir, il y a beaucoup de rouge. OffscreenCanvas n'est normalement pris en charge que dans Chrome. Il existe également une option avec Firefox, mais jusqu'à présent, il existe un indicateur, et Canvas ne fonctionne qu'avec le contexte WebGL. Ici, vous pouvez demander - pourquoi est-ce que je parle d'une chose aussi cool que OffscreenCanvas, qui ne fonctionne nulle part?



Une petite digression. Nous avons certains niveaux de prise en charge des navigateurs sur le marché. Et nous avons deux quantités. Une valeur caractérise le navigateur, que nous ne prenons pas en charge du tout. Cela représente environ la moitié du pourcentage de popularité du navigateur.

Et il y a une deuxième quantité. Il comprend les navigateurs que nous prenons en charge, mais uniquement les fonctionnalités essentielles. Ici, sans Workers, toutes les fonctionnalités de recherche fonctionnent, mais avec de petites frises. Je pense que ça va, et notre équipe pense que ça va. Voyons comment nous allons mettre cela en œuvre.



Voici un schéma de ce que nous allons faire. Nous avons même des fichiers que nous lirons via FileReader. Mais dans le flux principal, nous l'enverrons à Web Workers, où il sera coupé, compressé et nous sera renvoyé, et nous l'enverrons déjà au serveur.



Voyons le code de notre Worker. Tout d'abord, nous créons une instance OffscreenCanvas avec la largeur et la hauteur dont nous avons besoin.

De plus, comme je l'ai dit, l'élément Image n'est pas disponible pour nous dans le contexte des travailleurs, nous utilisons donc ici la méthode createImageBitmap, qui fera de nous la structure de données qui caractérise notre image.

De l'intéressant: nous voyons ici soi. Ceux qui ne connaissent pas les Web Workers, cette chose indique le contexte d'exécution. Cela n'a pas d'importance pour nous ici, fenêtre ou ceci, nous utilisons soi-même. Cette méthode est asynchrone, j'ai utilisé l'attente ici pour la compacité et la commodité, pourquoi pas?

Ensuite, nous obtenons la même image et faisons la même chose qu'auparavant. Dessinez sur la toile et revenez.

Du simple. Nous avions l'habitude de prendre DataURL et de tout convertir en blob. Mais ici, la méthode convertToBlob est immédiatement disponible pour nous. Pourquoi ne l'ai-je pas utilisé auparavant? Parce que le soutien était pire. Mais depuis que nous sommes allés jusqu'ici et que nous utilisons OffscreenCanvas, qu'est-ce qui nous empêche d'utiliser convertToBlob?



Nous retournerons ce blob essentiellement un flux, d'où nous l'enverrons au serveur. Ou, comme dans les démos, dessinez-le.

Nous créons donc un Worker dans le thread principal, en écoutons certains messages et nous dessinons ou envoyons au serveur. Il n'y a rien d'important ici. Le travailleur acceptera nos fichiers.

Revenons à notre démo.

Regardez la quatrième démo

Tout de même démo, tout de même trois chats et un hérisson. Je réactiverai la limitation, ralentissant le processeur six fois. Je vais télécharger la même photo. Comme on le voit, au moment où l'image a été dessinée, les animations ne se sont pas arrêtées, le hérisson a continué à tourner, l'interface est restée et nous avons réalisé ce que nous voulions.

Mais cette décision peut-elle être améliorée?



Ici, au fait, le profileur. Ici, nous ne voyons pas les énormes microtâches pendant les cinq secondes que nous avons vues auparavant.

Une amélioration est possible. Utilisation d'objets transférables. Ici, il vaut la peine d'y retourner. Lorsque nous avons transmis notre DataURL ou blob via le mécanisme postMessage, nous avons copié ces données. Ce n'est probablement pas très efficace. Ce serait cool de l'éviter. Par conséquent, nous avons un mécanisme qui vous permet de transférer des données vers Web Workers comme dans un package.

Pourquoi dis-je «j'aime»? Lorsque nous transférons ces données aux travailleurs, nous perdons le contrôle sur eux dans le flux principal - nous ne pouvons en aucun cas interagir avec eux. Il y a une deuxième limitation ici. Nous ne pouvons pas transférer tous les types de données vers Web Workers. Nous ne pouvons pas faire cela avec une chaîne, nous le ferons différemment.



Regardons le code. Premièrement, nous transmettons les données un peu différemment. Voici notre postMessage. Vous voyez, il existe un tel tableau avec loadEvent.target.result. Une telle interface nous permet de transférer nos données en tant qu'objets transférables, en perdant le contrôle sur eux.

Soit dit en passant, quiconque écrit en Rust entendra probablement quelque chose de familier. Et nous lirons notre fichier non pas comme une chaîne, mais comme un ArrayBuffer. Il s'agit d'un flux de données binaires lidar auquel il n'y a pas d'accès direct. Par conséquent, nous devrons faire autre chose avec eux.



Retour à nos ImageWorkers. Ici, c'est devenu beaucoup plus intéressant. Tout d'abord, nous prenons notre tampon et faisons une chose aussi terrible que Uint8ClampedArray. Il s'agit d'un tableau typé. Comme son nom l'indique, les données qu'il contient sont les numéros de signe, c'est-à-dire des nombres de zéro à 255 qui représenteront le pixel de notre image.

Le troisième argument, nous passons une chose aussi étrange, comme la largeur, multipliée par la hauteur, multipliée par quatre. Pourquoi exactement quatre? Exactement, RGBA. Il existe trois valeurs par couleur et une par canal alpha.

Ensuite, nous créerons ImageData à partir de ce tableau, un type de données spécial qui peut être facilement dessiné sur le canevas. Rien d'intéressant ici. Nous prenons simplement un tableau et le transmettons au constructeur. De plus, de la même manière, nous dessinons notre image sur la toile, mais en utilisant une méthode différente, sous ImageData. De plus, tout est le même qu'avant.

Passons aux conclusions. Aujourd'hui, je vous ai parlé d'une tâche que je n'avais pas effectuée il y a si peu de temps. Qu'y ai-je remarqué?



La fluidité de l'interface est très importante. Lorsque l'utilisateur accuse un peu de retard, se fige un peu, le bouton n'est pas enfoncé, cela peut entraîner une grave détérioration de l'UX. Les navigateurs fonctionnent différemment. Nous avons examiné un exemple sphérique avec Safari et Yandex.Browser. Nous voyons que si vous avez vérifié la fluidité de votre interface dans un navigateur, vous devriez regarder les autres.

Vous devez faire quelque chose avec les scripts de blocage s'ils continuent pendant longtemps. Dans mon cas, je l'ai mis sur Web Workers. Mais il existe probablement d'autres approches, vous pouvez en quelque sorte les diviser en plus petites, vous devez réfléchir ici. , Web Workers, .

? . C'est très important. . , 200 , .

Web Workers . , , .

:


.

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


All Articles