¿Cómo trabajar con imágenes en el cliente, manteniendo una interfaz de usuario fluida? El desarrollador de interfaces Pavel Smirnov habló sobre esto sobre la base de la experiencia de desarrollar la búsqueda de fotografías en el mercado. A partir del informe, puede aprender a usar Web Workers y OffscreenCanvas correctamente.

- Durante esta media hora hablaremos de aventuras. Te contaré sobre mi aventura y realmente espero que mi informe te inspire y que tomes y hagas lo mismo en casa.
Al principio, quería hablar sobre algunas tecnologías nuevas o no muy nuevas que nuestros navegadores nos brindan y que nos permiten hacer cosas geniales. Pero me parece que no sería muy divertido, porque todos pueden ir a MDN y leer algo. Por lo tanto, contaré la historia de una característica que hice con el equipo de Market.
Vamos a presentarme de nuevo primero. Mi nombre es Pasha, soy un desarrollador de interfaces en el equipo de Market.

Principalmente trato con interfaces móviles: búsqueda de mapas, tarjeta de oferta. También reescribo el código de la pila anterior a la nueva, y luego de la nueva a una pila aún más nueva. Y trato de hacer que mis interfaces sean buenas. Aquí vale la pena decir qué es una buena interfaz.
Las buenas interfaces tienen diferentes características. En primer lugar, es conveniente; en segundo lugar, es hermoso; en tercer lugar, es asequible. Pero una de las características de las que quiero hablar hoy es la velocidad. Y la velocidad a menudo se manifiesta en la suavidad de su trabajo. Incluso los frisos pequeños pueden cambiar en gran medida la experiencia del usuario de nuestras interfaces.

Pasemos al plan para mi conversación de hoy. Primero hablaremos sobre la tarea que hice: encontrar una imagen en el mercado. A continuación, te diré qué problemas tuve que resolver para implementar esta funcionalidad. Aquí recordamos un poco cómo funciona su script en el navegador, y veamos las tecnologías que me ayudaron. Pequeño spoiler: estos son Web Workers y OffscreenCanvas.
Volvamos a la tarea. Hace unos meses, Luba, nuestro gerente de producto, se me acercó. Lyuba aborda los problemas de elegir un producto en el mercado. Ahora tenemos varias opciones para encontrar productos. Una de ellas es ingresar algo en la barra de búsqueda.

Por ejemplo, "compre un iPhone X rojo en Samara". Y encontraremos algo. O podemos usar el árbol de catálogo. En este catálogo tenemos categorías y subcategorías.
Pero, ¿qué pasa si quiero encontrar algo en el mercado, sin saber cómo se llama, pero o tengo una foto de esta cosa o la veo en la fiesta de alguien?

Te diré un caso real. Una vez fui con mis amigos a un café. Pedimos limonada allí, ya sabes, en una jarra así, y esta jarra tenía algo muy extraño. Incluso guardé una foto. Fue diseñado para que cuando vierta limonada en un vaso, el hielo no entre en él. Pensamos que era algo genial, pero teníamos opiniones diferentes sobre cómo se llamaba y, en general, para qué estaba destinado. Por lo tanto, lo encontramos en Yandex.Pictures.
Pero pensé: sería genial si no solo pudiera buscar esta cosa, sino también comprarla de inmediato o al menos averiguar el precio, leer reseñas, especificaciones, etc. En este punto, nuestros sueños coincidieron con Any, y decidimos hacer tal funcionalidad en el mercado.
¿Cómo es esta funcionalidad? Permite al usuario cargar una foto o una foto, incluso puede tomar una foto de inmediato y enviarla al mercado. Analizamos esta foto usando las tecnologías de búsqueda de Yandex, encontramos un producto en ella y mostramos al usuario los resultados con estos productos. Parece simple, pero si fuera así de simple, no haría mi informe. Para asegurarme de qué tipo de característica es esta, déjame mostrarte.
Mira la primera demoLo mostraré en producción. Primero, cargue lo que estábamos buscando y veamos qué sucede.
Encontramos algunos bienes y específicamente esta cosa. Esta cosa se llama colador. Para encontrar algo más, ayer fotografié un libro en el escritorio de un colega, vamos a buscarlo. Aquí hay un libro así, tal vez alguien lo lea. Se llama "Código perfecto". También lo encuentra de alguna manera, y por alguna razón con un límite de 18+. Esto es probablemente un poco extraño.
Volvamos a nuestro informe. ¿Qué problemas he encontrado? El primer problema es que el usuario comienza a descargar cualquier cosa, incluidas imágenes enormes. Por ejemplo, mi teléfono toma fotos de tres a cuatro megabytes de tamaño, lo cual es bastante. Enviar tales fotos al backend es ineficiente. Toma mucho tiempo, toma mucho tiempo analizarlos, por lo que debe hacer algo al respecto. Pero aquí todo es simple: recortaremos, comprimiremos, redimensionaremos esta foto en el cliente.

¿Cómo haremos esto? Tenemos un archivo Y de alguna manera leeremos este archivo. Leeremos usando la API FileReader. Te diré brevemente de qué se trata.

Esta es una API de navegador que nos permite leer el archivo descargado y hacer algo con él. Puedes leer de diferentes maneras, lo veremos ahora. Estas son sus características, y tenemos algún tipo de objeto que nos devolvió de la entrada del evento de cambio. Tratemos de leerlo.

El código se verá así. No hay nada complicado aquí todavía. Tenemos un objeto Reader creado a partir del constructor FileReader, en el que colgamos al desarrollador del evento de carga. A continuación, leeremos este archivo como DataURL. DataURL: una cadena que representa el contenido del archivo codificado a través de Base64. Como leemos, necesitamos cortarlo de alguna manera. Primero, carguemos todo en una imagen. Tenemos una etiqueta o elemento img, y lo cargamos allí mismo.

El código se verá más o menos así. Creamos un elemento img, mediante el evento load Reader cargamos nuestra línea en el atributo src y haremos todo más cuando nuestra línea termine de cargarse en img.
Haremos lo que quisiéramos: recortar la imagen. Lo comprimiremos, y aquí algo como Canvas nos ayudará, una herramienta muy poderosa. Te permite hacer mucho. Pero aquí solo dibujamos nuestra imagen en este lienzo, y si los tamaños de la imagen exceden el máximo permitido, los ajustaremos un poco. Además, podemos recoger esta imagen con Canvas de la relación de compresión deseada.

Algo asi. Otro pequeño descargo de responsabilidad: el código aquí está muy simplificado, no especifico todo. Tenemos manejo de errores y otras cosas, pero para que todo encaje en la diapositiva y quede claro en el informe, omití algunos detalles.
Tenemos tamaños de imagen, solo los miramos. Hay algunas constantes permitidas para nosotros. Si el tamaño de las imágenes excede nuestras constantes, simplemente las recortamos debajo de ellas y configuramos nuestro Canvas en estos mismos tamaños.
A continuación dibujaremos nuestra imagen en este lienzo.

Tome el contexto 2d, necesitamos una imagen 2d e intente dibujar usando el método drawImage. DrawImage es un método interesante que acepta, si no me equivoco, nueve parámetros. Pero no todos son obligatorios, usaremos solo cinco. Tomamos Imagen y esos dos ceros, esto es desplazamiento o sangría de la imagen. Necesitamos el punto superior izquierdo. Dibuja con las dimensiones que necesitamos.
Además, a partir de este lienzo, tomaremos nuestra cadena Base64 codificada por DataURL exactamente de la misma manera y la convertiremos en blob, un objeto especial que nos conviene enviar al servidor. Parece ser todo. Todo funciona La imagen se recorta, se envía la imagen, se reconoce la imagen.
Pero luego comencé a notar algo. Cuando probé esta solución, cuando cargué una imagen, especialmente en dispositivos débiles, mi interfaz se ralentizó un poco. O no se presionó el botón, entonces el elemento no se desplazó así. ¿Tuviste la sensación de que tu código funciona en el 99% de los casos y funciona bien, pero a veces simplemente no funciona? Y puede dárselo para probar, y probablemente nadie lo notará. Y los usuarios, probablemente, no lo notarán, especialmente en dispositivos débiles.
Esto nunca me ha pasado, y decidí arreglarlo. Esto resultó ser un problema. Si la imagen es grande, entonces, durante las manipulaciones con recorte, compresión, nos tomó algo de tiempo, y en este pequeño, pequeño tiempo, nuestra interfaz no respondió.
Al principio descubrí por qué sucede esto. Aquí vale la pena recordar un poco cómo funciona JavaScript en el navegador. No entraré en detalles, este es un tema para un gran informe. Solo recuerda algunos puntos.

Tenemos JavaScript ejecutándose en un solo hilo, llamémoslo principal. Y tenemos tal cosa en el navegador como un bucle de eventos. Aquí inmediatamente decimos que este es un modelo. En algunos navegadores, el bucle de eventos está organizado de manera diferente, pero como su nombre lo indica, en general es un bucle. Procesa ciertas tareas en la cola en orden.
Un momento desagradable: hasta que procese una tarea, no pasará a la siguiente. Mostraré la demostración que vi, ella lo muestra. Ella es un clasico.
Mira la segunda demostraciónTengo una imagen GIF y una animación CSS realizada de diferentes maneras: una que usa translatex, la otra que usa posición: relativa a la izquierda, la tercera que usa JavaScript, es decir, requestAnimationFrame. Aquí es donde gira el erizo. Que haré
Bloquearé el hilo principal durante cinco segundos. Ya sabes, por lo general, los tipos duros calculan el enésimo número de Fibonacci, pero escribí un bucle sin fin con un descanso en cinco segundos.
Que va a pasar Inmediatamente notó que el erizo dejó de girar, y el gato inferior, que está animado usando translatex, también dejó de montar. Pero veamos la misma demostración en otro navegador, por ejemplo Safari. El gato GIF dejó de correr.
¿Por qué estoy mostrando todo esto? En primer lugar, los navegadores son diferentes, debes tener esto en cuenta. En segundo lugar, cuando algo bloquea nuestro flujo, algunas cosas dejarán de funcionar. Por ejemplo, animación JavaScript. O demostremos que el texto ya no se destacará, los botones ya no se presionarán.
Este es un ejemplo muy abstracto. No bloqueemos el flujo durante cinco segundos, tome nuestra tarea, cargue una foto, recórtela, exprímala y dibuje aquí. No lo enviaremos a ningún lado, no será muy revelador.
Mira la tercera demostraciónTengo una MacBook poderosa aquí, y para que todo parezca más convincente, ralentizaremos el procesador seis veces. Esto le permite hacer DevTools. Sube nuestra foto. El Código Perfecto nos ayudará nuevamente. Como vemos, sucede lo mismo que cuando se bloquea el hilo principal.
Regresemos a nuestra tarea y pensemos cómo trataremos esto.

Por cierto, si nos fijamos en el generador de perfiles, veremos esto. En el marco rojo está nuestra microtask, que bloquea el hilo principal. Vemos que lo bloquea por casi cinco segundos. Está en una computadora bastante poderosa, y en dispositivos más débiles será aún más notable.
Pasemos a la solución. Diré de inmediato lo que usé y lo que hice, y luego analizaremos todas estas cosas. Primero, utilicé Web Workers. Nos permiten poner algunas tareas en un hilo separado. Y en segundo lugar, en el contexto de Web Workers, el DOM no está disponible para nosotros. Para hacer frente a esta situación, utilizaremos otras herramientas. La imagen no estará disponible para nosotros, el Canvas clásico está disponible y, por lo tanto, usamos Canvas y algunos otros trucos.

Recordemos rápidamente qué son los trabajadores, para qué sirven. Le permiten ejecutar JavaScript en un hilo separado, no principalmente. Y el flujo de trabajo no interfiere con el flujo de representación de la interfaz principal. Por lo tanto, podemos realizar algunas tareas computacionales complejas sin ralentizar nuestra interfaz.
Tenemos una herramienta que le permite transferir algo a los trabajadores y devolver algo de los trabajadores. Veamos un ejemplo.

Entonces creamos nuestro Worker usando el constructor. Allí debe transferir la ruta al archivo. Incluso podemos pasar blob. Y tenemos un controlador de eventos de mensajes. En este caso, simplemente mostrará algo en la pantalla. Entonces podemos enviar algunos datos a nuestro trabajador.

¿Cuál es el soporte? Todo está bien aquí. Trabajadores es una herramienta bien conocida, no nueva, pero muchos de mis amigos piensan que no siempre son compatibles. Esto no es asi.

Ahora echemos un vistazo a OffscreenCanvas. Como ya hemos visto, Canvas es una herramienta muy poderosa, pero, desafortunadamente, no está disponible para nosotros en el contexto de Web Workers, por lo que utilizaremos una alternativa. Esta es una cosa bastante nueva llamada OffscreenCanvas. Le permite hacer casi las mismas cosas que Canvas, solo fuera de la pantalla, es decir, en el contexto de Web Workers. Por supuesto, podemos hacer esto también en el contexto de la ventana, pero ahora no lo haremos.

¿Qué hay con el apoyo? Como puede ver, hay mucho rojo. OffscreenCanvas normalmente solo es compatible con Chrome. También hay una opción con Firefox, pero hasta ahora hay una bandera, y Canvas solo funciona con contexto WebGL. Aquí puedes preguntar: ¿por qué estoy hablando de algo tan genial como OffscreenCanvas, que no funciona en ningún lado?

Una pequeña digresión. Tenemos algunos niveles de soporte de navegador en el mercado. Y tenemos dos cantidades. Un valor caracteriza el navegador, que no admitimos en absoluto. Esto es aproximadamente la mitad del porcentaje de popularidad del navegador.
Y hay una segunda cantidad. Incluye los navegadores que admitimos, pero solo la funcionalidad crítica. Aquí, sin Trabajadores, toda la funcionalidad de búsqueda funciona, pero con pequeños frisos. Creo que está bien, y nuestro equipo cree que está bien. Veamos cómo implementaremos esto.

Aquí hay un diagrama de lo que haremos. Incluso tenemos archivos que leeremos a través de FileReader. Pero en la transmisión principal, lo enviaremos a Web Workers, donde se cortará, comprimirá y volverá a nosotros, y ya lo enviaremos al servidor.

Veamos el código de nuestro trabajador. Primero, creamos una instancia OffscreenCanvas con el ancho y la altura que necesitamos.
Además, como dije, el elemento Imagen no está disponible para nosotros en el contexto Trabajadores, por lo que aquí usamos el método createImageBitmap, que nos hará la estructura de datos que caracteriza nuestra imagen.
De lo interesante: nos vemos aquí a uno mismo. Aquellos que no están familiarizados con Web Workers, esto apunta al contexto de ejecución. No nos importa aquí, ventana o esto, usamos self. Este método es asíncrono, lo he usado aquí para ser compacto y conveniente, ¿por qué no?
Luego, obtenemos la misma imagen y hacemos lo mismo que hicimos antes. Dibuja en el lienzo y vuelve.
De lo simple. Solíamos tomar DataURL y convertir todo a blob. Pero aquí el método convertToBlob está disponible de inmediato para nosotros. ¿Por qué no lo he usado antes? Porque el apoyo fue peor. Pero dado que fuimos hasta aquí y usamos OffscreenCanvas, ¿qué nos impide usar convertToBlob?

Devolveremos este blob básicamente una secuencia, desde donde lo enviaremos al servidor. O, como en las demos, dibujarlo.
Entonces creamos un Trabajador en el hilo principal, escuchamos algunos mensajes y dibujaremos o enviaremos al servidor. No hay nada importante aquí. El trabajador aceptará nuestros archivos.
Volvamos a nuestra demo.
Mira la cuarta demostraciónLa misma demostración, los mismos tres gatos y un erizo. Encenderé el acelerador nuevamente, ralentizando el procesador seis veces. Subiré la misma foto. Como vemos, en el momento en que se dibujó la imagen, las animaciones no se detuvieron, el erizo continuó girando, la interfaz permaneció y logramos lo que queríamos.
¿Pero se puede mejorar esta decisión?

Aquí, por cierto, el perfilador. Aquí no vemos las enormes Microtasks durante los cinco segundos que vimos antes.
La mejora es posible. Utilizando objetos transferibles. Aquí vale la pena volver de nuevo. Cuando pasamos nuestro DataURL o blob a través del mecanismo postMessage, copiamos estos datos. Esto probablemente no sea muy efectivo. Sería genial evitarlo. Por lo tanto, tenemos un mecanismo que le permite transferir datos a Web Workers como si estuviera en un paquete.
¿Por qué digo "me gusta"? Cuando transferimos estos datos a los trabajadores, perdemos el control sobre ellos en la transmisión principal; no podemos interactuar con ellos de ninguna manera. Hay una segunda limitación aquí. No podemos transferir todos los tipos de datos a Web Workers. No podemos hacer esto con una cadena; lo haremos de manera diferente.

Miremos el código. En primer lugar, transmitimos los datos de manera un poco diferente. Aquí está nuestro postMessage. Verá, hay una matriz con loadEvent.target.result. Dicha interfaz nos permite transferir nuestros datos como objetos transferibles, perdiendo el control sobre ellos.
Por cierto, cualquiera que escriba en Rust probablemente escuchará algo familiar. Y leeremos nuestro archivo no como una cadena, sino como un ArrayBuffer. Esta es una secuencia de datos binarios lidar a los que no hay acceso directo. Por lo tanto, tendremos que hacer algo más con ellos.

De vuelta a nuestros ImageWorkers. Aquí se volvió mucho más interesante. Primero, tomamos nuestro búfer y hacemos algo tan terrible como Uint8ClampedArray. Esta es una matriz con tipo. Como su nombre lo indica, los datos que contiene son los números de signos, es decir, números del cero al 255 que representarán el píxel de nuestra imagen.
El tercer argumento, pasamos algo tan extraño, como el ancho, multiplicado por la altura, multiplicado por cuatro. ¿Por qué exactamente cuatro? Exactamente, RGBA. Hay tres valores por color y uno por canal alfa.
A continuación, crearemos ImageData a partir de esta matriz, un tipo de datos especial que se puede dibujar fácilmente en el lienzo. Nada interesante aquí Simplemente tomamos una matriz y la pasamos al constructor. Además, de la misma manera dibujamos nuestra imagen en el lienzo, pero usando un método diferente, en ImageData. Además, todo sigue igual que antes.
Pasemos a las conclusiones. Hoy te conté sobre una tarea que no hice hace mucho tiempo. ¿Qué noté en él?

La suavidad de la interfaz es muy importante. Cuando el usuario se retrasa un poco, se congela un poco, no se presiona el botón, esto puede conducir a un deterioro severo en UX. Los navegadores funcionan de manera diferente. Observamos un ejemplo esférico con Safari y Yandex.Browser. Vemos que si verificó la suavidad de su interfaz en un navegador, debería mirar los otros.
Debe hacer algo con los scripts de bloqueo si continúan durante mucho tiempo. En mi caso, lo puse en Web Workers. Pero probablemente hay otros enfoques, de alguna manera puedes dividirlos en otros más pequeños, aquí tienes que pensar. , Web Workers, .
? . . . , 200 , .
Web Workers . , , .
:
.