Abenteuer in einem separaten Stream. Yandex-Bericht

Wie arbeite ich mit Bildern auf dem Client unter Beibehaltung einer reibungslosen Benutzeroberfläche? Der Schnittstellenentwickler Pavel Smirnov sprach darüber auf der Grundlage der Erfahrung bei der Entwicklung der Suche nach Fotografien auf dem Markt. Im Bericht erfahren Sie, wie Sie Web Worker und OffscreenCanvas richtig verwenden.



- In dieser halben Stunde werden wir über Abenteuer sprechen. Ich werde Ihnen von meinem Abenteuer erzählen und hoffe wirklich, dass mein Bericht Sie inspirieren wird und Sie das Gleiche zu Hause nehmen und tun werden.

Zuerst wollte ich über einige neue oder nicht sehr neue Technologien sprechen, die uns unsere Browser bieten und die es uns ermöglichen, coole Dinge zu tun. Aber es scheint mir, dass es nicht sehr lustig wäre, weil jeder zu MDN gehen und etwas lesen kann. Daher erzähle ich die Geschichte eines Features, das ich mit dem Market-Team gemacht habe.

Lassen Sie mich zunächst noch einmal vorstellen. Mein Name ist Pasha, ich bin ein Schnittstellenentwickler im Market-Team.



Ich beschäftige mich hauptsächlich mit mobilen Schnittstellen - Kartensuche, Angebotskarte. Ich schreibe auch den Code vom alten auf den neuen Stapel und dann vom neuen auf einen noch neueren Stapel. Und ich versuche meine Schnittstellen gut zu machen. Hier lohnt es sich zu sagen, was eine gute Schnittstelle ist.

Gute Schnittstellen haben unterschiedliche Eigenschaften. Erstens ist es bequem, zweitens ist es schön, drittens ist es erschwinglich. Aber eine der Eigenschaften, über die ich heute sprechen möchte, ist die Geschwindigkeit. Und Geschwindigkeit manifestiert sich oft in der Geschmeidigkeit seiner Arbeit. Selbst kleine Friese können die Benutzererfahrung unserer Schnittstellen erheblich verändern.



Kommen wir zum Plan für mein heutiges Gespräch. Zuerst werden wir über die Aufgabe sprechen, die ich erledigt habe: ein Bild auf dem Markt zu finden. Als Nächstes erkläre ich Ihnen, welche Probleme ich lösen musste, um diese Funktionalität zu implementieren. Hier erinnern wir uns ein wenig an die Funktionsweise Ihres Skripts im Browser und schauen uns die Technologien an, die mir geholfen haben. Kleiner Spoiler: Dies sind Web Worker und OffscreenCanvas.

Kommen wir zurück zur Aufgabe. Vor einigen Monaten hat sich Luba, unser Produktmanager, an mich gewandt. Lyuba befasst sich mit den Problemen bei der Auswahl eines Produkts auf dem Markt. Jetzt haben wir mehrere Möglichkeiten, Waren zu finden. Eine davon ist, etwas in die Suchleiste einzugeben.



Beispiel: "Kaufen Sie ein rotes iPhone X in Samara." Und wir werden etwas finden. Oder wir können den Katalogbaum verwenden. In diesem Katalog haben wir Kategorien und Unterkategorien.

Aber was ist, wenn ich etwas auf dem Markt finden möchte, ohne zu wissen, wie es heißt, aber entweder habe ich ein Bild von diesem Ding oder ich sehe es auf einer Party von jemandem?



Ich werde einen echten Fall erzählen. Ich war einmal mit meinen Freunden in einem Café. Wir haben dort Limonade in einem solchen Krug bestellt, und dieser Krug hatte so etwas Seltsames. Ich habe sogar ein Foto gemacht. Es war beabsichtigt, dass beim Eingießen von Limonade in ein Glas kein Eis eindringt. Wir fanden das cool, aber wir hatten unterschiedliche Meinungen darüber, wie dieses Ding heißt und wofür es im Allgemeinen gedacht ist. Deshalb haben wir es auf Yandex.Pictures gefunden.

Aber ich dachte - es wäre cool, wenn ich nicht nur nach diesem Ding suchen, sondern es auch sofort kaufen oder zumindest den Preis herausfinden, Bewertungen, Funktionen usw. lesen könnte. Zu diesem Zeitpunkt stimmten unsere Träume mit Any überein, und wir entschieden uns machen solche Funktionen auf dem Markt.

Wie ist diese Funktionalität? Es ermöglicht dem Benutzer, ein Foto oder Bild hochzuladen. Sie können sogar sofort ein Bild aufnehmen und es an den Markt senden. Wir analysieren dieses Foto mithilfe von Yandex-Suchtechnologien, finden ein Produkt darauf und zeigen dem Benutzer die Ergebnisse mit diesen Produkten. Es scheint einfach zu klingen, aber wenn es so einfach wäre, würde ich meinen Bericht nicht machen. Um sicherzugehen, um welche Art von Funktion es sich handelt, möchte ich sie zeigen.

Sehen Sie sich die erste Demo an

Ich werde auf Produktion zeigen. Lassen Sie uns zuerst genau das hochladen, wonach wir gesucht haben, und sehen, was passiert.

Wir haben einige Waren gefunden und speziell dieses Ding. Dieses Ding nennt man ein Sieb. Um etwas anderes zu finden, habe ich gestern ein Buch am Schreibtisch eines Kollegen fotografiert. Lassen Sie uns danach suchen. Hier ist so ein Buch, vielleicht hat es jemand gelesen. Es heißt "Perfect Code". Er findet es auch irgendwie und aus irgendeinem Grund mit einem Limit von 18+. Das ist wahrscheinlich etwas seltsam.

Kommen wir zurück zu unserem Bericht. Auf welche Probleme bin ich gestoßen? Das erste Problem ist, dass der Benutzer mit dem Herunterladen beginnt, einschließlich großer Bilder. Zum Beispiel nimmt mein Telefon Bilder mit einer Größe von drei bis vier Megabyte auf, was ziemlich viel ist. Das Senden solcher Fotos an das Backend ist ineffizient. Es dauert lange, es dauert lange, sie zu analysieren, also müssen Sie etwas dagegen tun. Aber hier ist alles einfach - wir werden dieses Foto auf dem Client zuschneiden, komprimieren und in der Größe ändern.



Wie machen wir das? Wir haben eine Datei. Und wir werden diese Datei irgendwie lesen. Wir werden mit der FileReader-API lesen. Ich werde Ihnen kurz sagen, was es ist.



Dies ist eine solche Browser-API, mit der wir die heruntergeladene Datei lesen und etwas damit anfangen können. Sie können auf verschiedene Arten lesen, wir werden es jetzt betrachten. Hier sind seine Funktionen, und wir haben eine Art Objekt, das von der Eingabe durch das Änderungsereignis an uns zurückgegeben wurde. Versuchen wir es zu lesen.



Der Code sieht folgendermaßen aus. Hier ist noch nichts kompliziert. Wir haben ein Reader-Objekt aus dem FileReader-Konstruktor erstellt, an das wir den Entwickler des Ladeereignisses hängen. Als nächstes werden wir diese Datei als DataURL lesen. DataURL - eine Zeichenfolge, die den Inhalt der über Base64 codierten Datei darstellt. Wie wir lesen, müssen wir es irgendwie schneiden. Laden wir zunächst alles in ein Bild. Wir haben ein Tag oder ein img-Element und laden es genau dort.



Der Code sieht ungefähr so ​​aus. Wir erstellen ein img-Element. Durch das load Reader-Ereignis laden wir unsere Zeile in das src-Attribut und werden alles weiter tun, wenn unsere Zeile mit dem Laden in img fertig ist.

Wir werden tun, was wir wollten - das Bild zuschneiden. Wir werden es komprimieren, und hier hilft uns so etwas wie Canvas, ein sehr mächtiges Werkzeug. Sie können viel tun. Aber hier zeichnen wir einfach unser Bild auf diese Leinwand, und wenn die Bildgrößen den maximal zulässigen Wert überschreiten, passen wir sie ein wenig an. Wir können dieses Bild auch mit Canvas mit dem gewünschten Komprimierungsverhältnis aufnehmen.



Ungefähr so. Ein weiterer kleiner Haftungsausschluss: Der Code hier ist stark vereinfacht, ich spezifiziere nicht alles. Wir haben Fehlerbehandlung und andere Dinge, aber damit alles auf die Folie passt und im Bericht klar ist, habe ich einige Details weggelassen.

Wir haben Bildgrößen, wir schauen sie uns nur an. Es sind uns einige Konstanten erlaubt. Wenn die Größe der Bilder unsere Konstanten überschreitet, schneiden wir sie einfach darunter und stellen unsere Leinwand auf die gleichen Größen ein.

Als nächstes werden wir unser Bild auf diese Leinwand zeichnen.



Nehmen Sie den 2D-Kontext, wir benötigen ein 2D-Bild und versuchen Sie, mit der drawImage-Methode zu zeichnen. DrawImage ist eine interessante Methode, die, wenn ich mich nicht irre, neun Parameter akzeptiert. Aber sie sind nicht alle obligatorisch, wir werden nur fünf verwenden. Wir nehmen Bild und diese beiden Nullen, dies ist Versatz oder Einrückung des Bildes. Wir brauchen den oberen linken Punkt. Zeichnen Sie mit den Abmessungen, die wir benötigen.

Aus diesem Canvas-Bereich nehmen wir unseren DataURL-codierten Base64-String genauso und verwandeln ihn in einen Blob - ein spezielles Objekt, das wir bequem an den Server senden können. Es scheint alles zu sein. Alles arbeitet. Das Bild wird beschnitten, das Bild wird gesendet, das Bild wird erkannt.

Aber dann bemerkte ich etwas. Als ich diese Lösung testete und ein Bild hochlud, insbesondere auf schwachen Geräten, verlangsamte sich meine Benutzeroberfläche etwas. Entweder wurde die Taste nicht gedrückt, dann hat das Element nicht so gescrollt. Hatten Sie das Gefühl, dass Ihr Code in 99% der Fälle funktioniert und gut funktioniert, aber manchmal funktioniert er einfach nicht? Und Sie können es zum Testen geben, und wahrscheinlich wird es niemand bemerken. Und Benutzer werden es wahrscheinlich nicht bemerken, insbesondere auf schwachen Geräten.

Das ist mir noch nie passiert und ich habe beschlossen, es zu beheben. Dies stellte sich als Problem heraus. Wenn das Bild groß ist, haben wir während der Manipulationen mit Zuschneiden und Komprimieren einige Zeit gebraucht, und in dieser kleinen, kleinen Zeit reagierte unsere Benutzeroberfläche nicht mehr.

Zuerst habe ich herausgefunden, warum das passiert. Hier lohnt es sich, sich ein wenig daran zu erinnern, wie JavaScript im Browser funktioniert. Ich werde nicht auf Details eingehen, dies ist ein Thema für einen großen Bericht. Denken Sie nur an einige Punkte.



Wir haben JavaScript in einem einzigen Thread, nennen wir es main. Und wir haben so etwas wie eine Ereignisschleife im Browser. Hier sagen wir sofort, dass dies ein Modell ist. In einigen Browsern ist die Ereignisschleife anders organisiert, aber wie der Name schon sagt, handelt es sich im Allgemeinen um eine Schleife. Es verarbeitet bestimmte Aufgaben in der Warteschlange der Reihe nach.

Ein unangenehmer Moment: Bis er eine Aufgabe bearbeitet, wird er nicht zur nächsten übergehen. Ich werde die Demo zeigen, die ich gesehen habe, sie zeigt es. Sie ist ein Klassiker.

Sehen Sie sich die zweite Demo an

Ich habe ein GIF-Bild und eine CSS-Animation auf verschiedene Arten erstellt: eine mit translatex, die andere mit position: relative left, die dritte mit JavaScript, nämlich requestAnimationFrame. Hier dreht sich der Igel. Was werde ich tun?

Ich werde den Haupt-Thread für fünf Sekunden blockieren. Weißt du, normalerweise berechnen harte Jungs die n-te Fibonacci-Zahl, aber ich habe eine Endlosschleife mit einer Pause in fünf Sekunden geschrieben.

Was wird passieren? Sie haben sofort bemerkt, dass der Igel aufgehört hat, sich zu drehen, und die untere Katze, die mit translatex animiert wurde, hat ebenfalls aufgehört zu reiten. Aber lassen Sie uns dieselbe Demo in einem anderen Browser sehen, zum Beispiel Safari. Die GIF-Katze hörte auf zu rennen.

Warum zeige ich das alles? Erstens sind Browser anders, das müssen Sie berücksichtigen. Zweitens, wenn unser Fluss durch etwas blockiert wird, funktionieren einige Dinge nicht mehr. Zum Beispiel - JavaScript-Animation. Oder lassen Sie uns zeigen, dass der Text für uns nicht mehr auffällt und die Tasten nicht mehr gedrückt werden.

Dies ist ein sehr abstraktes Beispiel. Lassen Sie uns den Fluss nicht für fünf Sekunden blockieren, sondern unsere Aufgabe übernehmen, ein Foto hochladen, zuschneiden, zusammendrücken und hier zeichnen. Wir werden es nirgendwo hinschicken, es wird nicht sehr aufschlussreich sein.

Sehen Sie sich die dritte Demo an

Ich habe hier ein leistungsstarkes MacBook, und damit alles überzeugender aussieht, werden wir den Prozessor um das Sechsfache verlangsamen. Auf diese Weise können Sie DevTools ausführen. Laden Sie unser Foto hoch. Der perfekte Code wird uns wieder helfen. Wie wir sehen, passiert dasselbe wie beim Blockieren des Hauptthreads.

Kehren wir dann zu unserer Aufgabe zurück und überlegen, wie wir damit umgehen werden.



Wenn Sie sich den Profiler ansehen, werden wir das übrigens sehen. Im roten Rahmen befindet sich unsere Mikrotask, die den Hauptfaden blockiert. Wir sehen, dass er es für fast fünf Sekunden blockiert. Es ist auf einem ziemlich leistungsfähigen Computer und auf schwächeren Geräten wird es noch deutlicher.

Fahren wir mit der Lösung fort. Ich werde sofort sagen, was ich verwendet und was ich getan habe, und dann werden wir all diese Dinge analysieren. Zuerst habe ich Web Worker verwendet. Sie ermöglichen es uns, einige Aufgaben in einen separaten Thread zu stellen. Und zweitens steht uns das DOM im Kontext von Web Workern nicht zur Verfügung. Um mit dieser Situation umzugehen, werden wir andere Tools verwenden. Das Bild wird uns nicht zur Verfügung stehen, das klassische Canvas ist verfügbar, und deshalb verwenden wir Canvas und einige andere Tricks.



Erinnern wir uns schnell daran, was Arbeiter sind, wofür sie sind. Mit ihnen können Sie JavaScript nicht hauptsächlich in einem separaten Thread ausführen. Und der Workers-Stream stört den Rendering-Fluss der Hauptschnittstelle nicht. Daher können wir einige komplexe Rechenaufgaben ausführen, ohne unsere Schnittstelle zu verlangsamen.

Wir haben ein Tool, mit dem Sie etwas an Arbeiter übertragen und etwas von Arbeitern zurückgeben können. Sehen wir uns ein Beispiel an.



Also erstellen wir unseren Worker mit dem Konstruktor. Dort müssen Sie den Pfad zur Datei übertragen. Wir können sogar Blob passieren. Und wir haben einen Message Event Handler. In diesem Fall wird einfach etwas auf dem Bildschirm angezeigt. Dann können wir einige Daten an unseren Mitarbeiter senden.



Was ist die Unterstützung? Hier ist alles gut. Workers ist ein bekanntes Werkzeug, kein neues, aber viele meiner Freunde denken, dass sie nicht immer unterstützt werden. Es ist nicht so.



Schauen wir uns nun OffscreenCanvas an. Wie wir bereits gesehen haben, ist Canvas ein sehr leistungsfähiges Tool, das uns im Kontext von Web Workers leider nicht zur Verfügung steht. Daher werden wir eine Alternative verwenden. Dies ist eine ziemlich neue Sache namens OffscreenCanvas. Sie können damit die gleichen Aktionen wie bei Canvas ausführen, nur außerhalb des Bildschirms, dh im Kontext von Web Workers. Natürlich können wir dies auch im Kontext des Fensters tun, aber jetzt werden wir es nicht tun.



Was gibt es mit Unterstützung? Wie Sie sehen können, gibt es viel Rot. OffscreenCanvas wird normalerweise nur in Chrome unterstützt. Es gibt auch eine Option mit Firefox, aber bisher gibt es ein Flag, und Canvas funktioniert nur mit dem WebGL-Kontext. Hier können Sie fragen - warum spreche ich über so eine coole Sache wie OffscreenCanvas, die nirgendwo funktioniert?



Ein kleiner Exkurs. Wir haben einige Ebenen der Browserunterstützung auf dem Markt. Und wir haben zwei Größen. Ein Wert kennzeichnet den Browser, den wir überhaupt nicht unterstützen. Dies ist ungefähr die Hälfte des Prozentsatzes der Browser-Popularität.

Und es gibt eine zweite Menge. Es enthält die von uns unterstützten Browser, jedoch nur wichtige Funktionen. Hier funktioniert ohne Arbeiter die gesamte Suchfunktion, jedoch mit kleinen Friesen. Ich denke, es ist in Ordnung und unser Team glaubt, dass es in Ordnung ist. Mal sehen, wie wir das umsetzen werden.



Hier ist ein Diagramm, was wir tun werden. Wir haben sogar Dateien, die wir über FileReader lesen werden. Aber im Hauptstrom werden wir es an Web Worker senden, wo es geschnitten, komprimiert und an uns zurückgegeben wird, und wir werden es bereits an den Server senden.



Sehen wir uns den Code für unseren Worker an. Zuerst erstellen wir eine OffscreenCanvas-Instanz mit der Breite und Höhe, die wir benötigen.

Wie ich bereits sagte, steht uns das Image-Element im Workers-Kontext nicht zur Verfügung. Daher verwenden wir hier die Methode createImageBitmap, mit der wir die Datenstruktur erhalten, die unser Bild charakterisiert.

Aus dem Interessanten: Wir sehen hier selbst. Diejenigen, die mit Web Workern nicht vertraut sind, verweisen auf den Ausführungskontext. Es ist uns hier egal, Fenster oder dies, wir benutzen uns selbst. Diese Methode ist asynchron, ich habe hier auf Kompaktheit und Bequemlichkeit gewartet, warum nicht?

Als nächstes erhalten wir das gleiche Bild und machen das Gleiche wie zuvor. Zeichne auf die Leinwand und kehre zurück.

Aus dem Einfachen. Früher haben wir DataURL genommen und alles in Blob konvertiert. Aber hier steht uns die convertToBlob-Methode sofort zur Verfügung. Warum habe ich es noch nie benutzt? Weil die Unterstützung schlechter war. Aber was hindert uns daran, convertToBlob zu verwenden, da wir den ganzen Weg hierher gegangen sind und OffscreenCanvas verwendet haben?



Wir werden diesen Blob im Grunde genommen als Stream zurückgeben, von wo aus wir ihn an den Server senden. Oder zeichnen Sie es wie in den Demos.

Also erstellen wir einen Worker im Haupt-Thread, hören einige Nachrichten davon ab und zeichnen oder senden sie an den Server. Hier gibt es nichts Wichtiges. Der Arbeiter akzeptiert unsere Dateien.

Kommen wir zurück zu unserer Demo.

Sehen Sie sich die vierte Demo an

Trotzdem die gleiche Demo, die gleichen drei Katzen und ein Igel. Ich werde die Drosselung wieder einschalten und den Prozessor sechsmal verlangsamen. Ich werde das gleiche Foto hochladen. Wie wir sehen können, hörten die Animationen zum Zeitpunkt der Bildzeichnung nicht auf, der Igel drehte sich weiter, die Benutzeroberfläche blieb erhalten und wir erreichten, was wir wollten.

Aber kann diese Entscheidung verbessert werden?



Hier übrigens der Profiler. Hier sehen wir die riesigen Mikrotasks für die fünf Sekunden, die wir zuvor gesehen haben, nicht.

Verbesserung ist möglich. Übertragbare Objekte verwenden. Hier lohnt es sich wieder zurückzukehren. Wenn wir unsere DataURL oder unseren Blob durch den postMessage-Mechanismus geleitet haben, haben wir diese Daten kopiert. Dies ist wahrscheinlich nicht sehr effektiv. Es wäre cool, es zu vermeiden. Daher haben wir einen Mechanismus, mit dem Sie Daten wie in einem Paket an Web Worker übertragen können.

Warum sage ich "Gefällt mir"? Wenn wir diese Daten an Worker übertragen, verlieren wir die Kontrolle über sie im Hauptstrom - wir können in keiner Weise mit ihnen interagieren. Hier gibt es eine zweite Einschränkung. Wir können nicht alle Datentypen an Web Worker übertragen. Wir können dies nicht mit einer Zeichenfolge tun, wir werden es anders machen.



Schauen wir uns den Code an. Erstens übertragen wir Daten etwas anders. Hier ist unsere postMessage. Sie sehen, es gibt ein solches Array mit loadEvent.target.result. Eine solche Schnittstelle ermöglicht es uns, unsere Daten als übertragbare Objekte zu übertragen und dabei die Kontrolle über sie zu verlieren.

Übrigens wird jeder, der in Rust schreibt, wahrscheinlich etwas Vertrautes hören. Und wir werden unsere Datei nicht als String lesen, sondern als ArrayBuffer. Dies ist ein Strom von Lidar-Binärdaten, auf die kein direkter Zugriff besteht. Deshalb müssen wir etwas anderes mit ihnen machen.



Zurück zu unseren ImageWorkern. Hier wurde es viel interessanter. Zuerst nehmen wir unseren Puffer und machen so etwas Schreckliches wie Uint8ClampedArray. Dies ist ein typisiertes Array. Wie der Name schon sagt, sind die darin enthaltenen Daten die Vorzeichen, dh Zahlen von Null bis 255, die das Pixel unseres Bildes darstellen.

Das dritte Argument, wir übergeben eine so seltsame Sache, wie die Breite, multipliziert mit der Höhe, multipliziert mit vier. Warum genau vier? Genau, RGBA. Es gibt drei Werte pro Farbe und einen pro Alphakanal.

Als Nächstes erstellen wir ImageData aus diesem Array, einem speziellen Datentyp, der einfach auf die Zeichenfläche gezeichnet werden kann. Nichts interessantes hier. Wir nehmen einfach ein Array und übergeben es an den Konstruktor. Auf die gleiche Weise zeichnen wir unser Bild auf die Leinwand, jedoch mit einer anderen Methode unter ImageData. Außerdem ist alles so wie vorher.

Kommen wir zu den Schlussfolgerungen. Heute habe ich Ihnen von einer Aufgabe erzählt, die ich vor nicht allzu langer Zeit erledigt habe. ?



. - - , - , , UX. -. Safari .. , , .

- , . Web Workers. , , , - , . , Web Workers, .

? . . . , 200 , .

Web Workers . , , .

:


Vielen Dank.

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


All Articles