Aventuras em um fluxo separado. Relatório Yandex

Como trabalhar com imagens no cliente, mantendo uma interface de usuário suave? O desenvolvedor de interface Pavel Smirnov falou sobre isso com base na experiência de desenvolver pesquisa de fotografias no mercado. No relatório, você pode aprender como usar Web Workers e OffscreenCanvas corretamente.



- Por meia hora, falaremos sobre aventuras. Vou falar sobre a minha aventura e realmente espero que meu relatório o inspire e você faça o mesmo em casa.

Primeiro, eu queria falar sobre algumas tecnologias novas ou não muito novas que nossos navegadores nos fornecem e que nos permitem fazer coisas legais. Mas parece-me que não seria muito divertido, porque todos podem ir ao MDN e ler alguma coisa. Portanto, vou contar a história de um recurso que fiz com a equipe do Market.

Vamos me apresentar novamente primeiro. Meu nome é Pasha, sou desenvolvedor de interfaces na equipe do Market.



Eu trato principalmente de interfaces móveis - pesquisa de mapa, cartão de oferta. Também reescrevi o código da pilha antiga para a nova e, em seguida, da nova para uma pilha ainda mais nova. E tento melhorar minhas interfaces. Aqui vale a pena dizer o que é uma boa interface.

Boas interfaces têm características diferentes. Em primeiro lugar, é conveniente; em segundo lugar, é bonito; em terceiro lugar, é acessível. Mas uma das características que eu quero falar hoje é a velocidade. E a velocidade geralmente se manifesta na suavidade de seu trabalho. Mesmo pequenos frisos podem mudar bastante a experiência do usuário de nossas interfaces.



Vamos seguir para o plano da minha conversa hoje. Primeiro, falaremos sobre a tarefa que eu fiz: encontrar uma foto no mercado. Em seguida, mostrarei quais problemas tive que resolver para implementar essa funcionalidade. Aqui, lembramos um pouco de como seu script funciona no navegador e analisamos as tecnologias que me ajudaram. Pequeno spoiler: são Web Workers e OffscreenCanvas.

Vamos voltar à tarefa. Alguns meses atrás, Luba, nosso gerente de produtos, se aproximou de mim. Lyuba lida com os problemas de escolher um produto no mercado. Agora, temos várias opções para encontrar mercadorias. Uma delas é inserir algo na barra de pesquisa.



Por exemplo, "compre um iPhone X vermelho em Samara". E nós vamos encontrar algo. Ou podemos usar a árvore de catálogos. Neste catálogo, temos categorias e subcategorias.

Mas e se eu quiser encontrar algo no mercado, sem saber como é chamado, mas ou tenho uma foto dessa coisa ou a vejo na festa de alguém?



Vou contar um caso real. Uma vez fui com meus amigos a um café. Pedimos limonada lá, você sabe, em tal jarro, e esse jarro tinha uma coisa tão estranha. Eu até guardei uma foto. Ele foi projetado para que, quando você derramar limonada em um copo, o gelo não entre nele. Nós pensamos que era uma coisa legal, mas tínhamos opiniões diferentes sobre como essa coisa era chamada e, em geral, para que ela era destinada. Portanto, encontramos no Yandex.Pictures.

Mas eu pensei - seria legal se eu não apenas pudesse procurar por essa coisa, mas também comprá-la imediatamente, ou pelo menos descobrir o preço, ler comentários, recursos etc. Nesse momento, nossos sonhos coincidiram com Qualquer, e decidimos fazer essa funcionalidade no mercado.

Como é essa funcionalidade? Ele permite que o usuário faça upload de uma foto ou foto; você pode tirar uma foto imediatamente e enviá-la ao mercado. Analisamos esta foto usando as tecnologias de pesquisa Yandex, localizamos um produto e mostramos ao usuário os resultados com esses produtos. Parece simples, mas se fosse assim tão simples, não faria o meu relatório. Para garantir que tipo de recurso é esse, deixe-me mostrá-lo.

Assista à primeira demonstração

Eu vou mostrar na produção. Vamos primeiro fazer upload da mesma coisa que estávamos procurando e ver o que acontece.

Encontramos alguns produtos e, especificamente, essa coisa. Essa coisa é chamada de filtro. Para encontrar outra coisa, fotografei um livro na mesa de um colega ontem, vamos procurá-lo. Aqui está um livro desses, talvez alguém o tenha lido. É chamado "Código Perfeito". Ele também o encontra de alguma forma, e por algum motivo com um limite de 18 anos ou mais. Provavelmente isso é um pouco estranho.

Vamos voltar ao nosso relatório. Que problemas eu encontrei? O primeiro problema é que o usuário começa a baixar qualquer coisa, incluindo imagens enormes. Por exemplo, meu telefone tira fotos de três a quatro megabytes, o que é bastante. Enviar essas fotos para o back-end é ineficiente. Leva muito tempo, leva muito tempo para analisá-los, então você precisa fazer algo sobre isso. Mas aqui tudo é simples - vamos cortar, compactar, redimensionar esta foto no cliente.



Como vamos fazer isso? Nós temos um arquivo. E, de alguma forma, vamos ler este arquivo. Leremos usando a API FileReader. Vou dizer brevemente o que é.



Essa é uma API do navegador que nos permite ler o arquivo baixado e fazer algo com ele. Você pode ler de diferentes maneiras, veremos agora. Aqui estão seus recursos, e temos algum tipo de objeto que nos retornou da entrada pelo evento de mudança. Vamos tentar ler.



O código ficará assim. Não há nada complicado aqui ainda. Temos um objeto Reader criado a partir do construtor FileReader, no qual travamos o desenvolvedor do evento load. Em seguida, leremos esse arquivo como DataURL. DataURL - uma sequência que representa o conteúdo do arquivo codificado através do Base64. Como lemos, precisamos cortá-lo de alguma forma. Primeiro, vamos carregar tudo em uma imagem. Temos um elemento tag ou img e o carregamos ali mesmo.



O código será algo parecido com isto. Criamos um elemento img, pelo evento load Reader, carregamos nossa linha no atributo src e faremos tudo mais quando nossa linha terminar de carregar no img.

Faremos o que quisemos - cortar a imagem. Vamos compactá-lo e, aqui, o Canvas nos ajudará, uma ferramenta muito poderosa. Ele permite que você faça muito. Mas aqui apenas desenhamos nossa imagem nesta tela e, se o tamanho da imagem exceder o máximo permitido, vamos ajustá-los um pouco. Além disso, podemos capturar esta imagem com o Canvas da taxa de compressão desejada.



Algo assim. Outro pequeno aviso: o código aqui é bastante simplificado, não especifico tudo. Temos manipulação de erros e outras coisas, mas para que tudo caiba no slide e fique claro no relatório, omiti alguns detalhes.

Temos tamanhos de imagem, apenas olhamos para eles. Existem algumas constantes permitidas para nós. Se os tamanhos das figuras excederem nossas constantes, apenas as recortamos sob elas e ajustamos nossa tela para os mesmos tamanhos.

A seguir, desenharemos nossa imagem nesta tela.



Pegue o contexto 2D, precisamos de uma imagem 2D e tente desenhar usando o método drawImage. DrawImage é um método interessante que aceita, se não me engano, nove parâmetros. Mas eles não são todos obrigatórios, usaremos apenas cinco. Tomamos Image e esses dois zeros, isso é deslocamento ou recuo da imagem. Precisamos do ponto superior esquerdo. Desenhe com as dimensões que precisamos.

Além disso, a partir deste Canvas, pegaremos nossa string Base64 codificada em DataURL exatamente da mesma maneira e a transformaremos em blob - um objeto especial que é conveniente enviar ao servidor. Parece ser tudo. Tudo funciona. A imagem é cortada, a imagem é enviada, a imagem é reconhecida.

Mas então eu comecei a perceber algo. Quando testei esta solução, quando carreguei uma imagem, especialmente em dispositivos fracos, minha interface ficou um pouco mais lenta. O botão não foi pressionado e o elemento não foi rolado. Você sentiu que seu código funciona em 99% dos casos e funciona bem, mas às vezes simplesmente não funciona? E você pode testá-lo e provavelmente ninguém notará. E os usuários, provavelmente, não notarão, especialmente em dispositivos fracos.

Isso nunca aconteceu comigo, e eu decidi consertar. Isso acabou sendo um problema. Se a imagem é grande, durante as manipulações com corte, compactação, levou algum tempo e, nesse pequeno e pequeno tempo, nossa interface não respondeu.

No começo, descobri por que isso está acontecendo. Aqui, vale a pena lembrar um pouco como o JavaScript funciona no navegador. Não vou entrar em detalhes, este é um tópico para um grande relatório. Apenas lembre-se de alguns pontos.



Temos o JavaScript em execução em um único segmento, vamos chamá-lo de principal. E temos algo no navegador como um loop de eventos. Aqui dizemos imediatamente que este é um modelo. Em alguns navegadores, o loop de eventos é organizado de maneira diferente, mas, como o nome indica, geralmente é um loop. Ele processa determinadas tarefas na fila em ordem.

Um momento desagradável: até que ele processe uma tarefa, ele não passará para a próxima. Vou mostrar a demo que vi, ela mostra. Ela é um clássico.

Assista à segunda demo

Eu tenho uma imagem GIF e animação CSS executada de diferentes maneiras: uma usando o translatex, a outra usando a posição: relativa à esquerda, a terceira usando o JavaScript, a saber requestAnimationFrame. É aqui que o ouriço está girando. O que eu farei?

Vou bloquear o segmento principal por cinco segundos. Você sabe, geralmente os caras durões calculam o enésimo número de Fibonacci, mas escrevi um loop sem fim com uma pausa em cinco segundos.

O que vai acontecer? Você notou imediatamente que o ouriço parou de girar e o gato inferior, que é animado usando o translatex, também parou de andar. Mas vamos ver a mesma demonstração em outro navegador, por exemplo, Safari. O gato GIF parou de correr.

Por que estou mostrando tudo isso? Em primeiro lugar, os navegadores são diferentes, você deve considerar isso. Em segundo lugar, quando nosso fluxo é bloqueado por algo, algumas coisas param de funcionar. Por exemplo - animação JavaScript. Ou vamos mostrar que o texto não se destacará mais, que os botões não serão mais pressionados.

Este é um exemplo muito abstrato. Não vamos bloquear o fluxo por cinco segundos, mas assuma nossa tarefa, faça upload de uma foto, corte-a, aperte-a e desenhe-a aqui. Não o enviaremos a lugar algum, não será muito revelador.

Assista à terceira demonstração

Eu tenho um poderoso MacBook aqui e, para tornar tudo mais convincente, desaceleraremos o processador em seis vezes. Isso permite que você faça o DevTools. Envie nossa foto. O Código Perfeito nos ajudará novamente. Como vemos, o mesmo acontece com o bloqueio do thread principal.

Vamos voltar à nossa tarefa e pensar em como vamos lidar com isso.



A propósito, se você olhar para o criador de perfil, veremos isso. No quadro vermelho está o nosso microtask, que bloqueia o segmento principal. Vemos que ele bloqueia por quase cinco segundos. Está em um computador bastante poderoso e, em dispositivos mais fracos, será ainda mais perceptível.

Vamos para a solução. Direi imediatamente o que usei e o que fiz e depois analisaremos todas essas coisas. Primeiro, usei Web Workers. Eles nos permitem colocar algumas tarefas em um thread separado. E segundo, no contexto de Web Workers, o DOM não está disponível para nós. Para lidar com essa situação, usaremos outras ferramentas. A imagem não estará disponível para nós, o Canvas clássico está disponível e, portanto, usamos o Canvas e alguns outros truques.



Vamos rapidamente lembrar o que são os Trabalhadores, para que servem. Eles permitem que você execute o JavaScript em um thread separado, não principalmente. E o fluxo de Trabalhadores não interfere no fluxo de renderização da interface principal. Portanto, podemos executar algumas tarefas computacionais complexas sem diminuir a velocidade da nossa interface.

Temos uma ferramenta que permite transferir algo para os trabalhadores e devolver algo dos trabalhadores. Vamos ver um exemplo.



Então criamos nosso Worker usando o construtor. Lá você precisa transferir o caminho para o arquivo. Podemos até passar blob. E nós temos um manipulador de eventos de mensagem. Nesse caso, ele simplesmente exibirá algo na tela. Em seguida, podemos enviar alguns dados para o nosso trabalhador.



Qual é o suporte? Tudo está bem aqui. Trabalhadores é uma ferramenta bem conhecida, não nova, mas muitos dos meus amigos pensam que nem sempre são suportados. Isto não é verdade.



Agora vamos dar uma olhada em OffscreenCanvas. Como já vimos, o Canvas é uma ferramenta muito poderosa, mas, infelizmente, não está disponível para nós no contexto de Web Workers, portanto, usaremos uma alternativa. Essa é uma coisa relativamente nova chamada OffscreenCanvas. Ele permite que você faça as mesmas coisas que o Canvas, apenas fora da tela, ou seja, no contexto de Web Workers. Obviamente, podemos fazer isso também no contexto da janela, mas agora não o faremos.



O que há com suporte? Como você pode ver, há muito vermelho. Normalmente, o OffscreenCanvas é suportado apenas no Chrome. Também existe uma opção no Firefox, mas até agora existe uma flag e o Canvas funciona apenas com o contexto WebGL. Aqui você pode perguntar - por que estou falando de algo tão legal como o OffscreenCanvas, que não funciona em nenhum lugar?



Uma pequena digressão. Temos alguns níveis de suporte ao navegador no mercado. E nós temos duas quantidades. Um valor caracteriza o navegador, ao qual não oferecemos suporte. Isso representa cerca de metade da porcentagem de popularidade do navegador.

E há uma segunda quantidade. Inclui os navegadores que suportamos, mas apenas funcionalidades críticas. Aqui, sem Trabalhadores, toda a funcionalidade de pesquisa funciona, mas com pequenos frisos. Eu acho que está tudo bem, e nossa equipe acredita que está tudo bem. Vamos ver como vamos implementar isso.



Aqui está um diagrama do que faremos. Temos até arquivos que leremos através do FileReader. Porém, no fluxo principal, o enviaremos aos Web Workers, onde será cortado, compactado e retornará para nós, e já o enviaremos ao servidor.



Vamos ver o código para o nosso Worker. Primeiro, criamos uma instância OffscreenCanvas com a largura e altura que precisamos.

Além disso, como eu disse, o elemento Image não está disponível para nós no contexto Workers, então aqui usamos o método createImageBitmap, que nos tornará a estrutura de dados que caracteriza nossa imagem.

Do interessante: vemos aqui o eu. Aqueles que não estão familiarizados com os Trabalhadores da Web, isso aponta para o contexto de execução. Não importa para nós aqui, janela ou isso, usamos a nós mesmos. Este método é assíncrono, usei aqui a espera por compacidade e conveniência, por que não?

Em seguida, obtemos a mesma imagem e fazemos a mesma coisa que fizemos antes. Desenhe na tela e retorne.

Do simples. Costumávamos pegar o DataURL e converter tudo em blob. Mas aqui o método convertToBlob está imediatamente disponível para nós. Por que não o usei antes? Porque o apoio foi pior. Mas desde que percorremos todo o caminho até aqui e usamos o OffscreenCanvas, o que nos impede de usar o convertToBlob?



Retornaremos esse blob basicamente um fluxo, de onde o enviaremos ao servidor. Ou, como nas demos, desenhe-o.

Então, criamos um Worker no thread principal, ouvimos algumas mensagens dele e vamos desenhar ou enviar para o servidor. Não há nada importante aqui. O trabalhador aceitará nossos arquivos.

Vamos voltar à nossa demonstração.

Assista à quarta demo

A mesma demonstração, os mesmos três gatos e um porco-espinho. Vou ativar a regulagem novamente, diminuindo a velocidade do processador seis vezes. Vou fazer upload da mesma foto. Como vemos, no momento em que a imagem foi desenhada, as animações não pararam, o ouriço continuou a girar, a interface permaneceu e conseguimos o que queríamos.

Mas essa decisão pode ser melhorada?



Aqui, a propósito, o criador de perfil. Aqui não vemos os enormes microtasks pelos cinco segundos que vimos antes.

Melhoria é possível. Usando objetos transferíveis. Aqui vale a pena voltar novamente. Quando passamos nosso DataURL ou blob através do mecanismo postMessage, copiamos esses dados. Provavelmente isso não é muito eficaz. Seria legal evitá-lo. Portanto, temos um mecanismo que permite transferir dados para Web Workers como se estivessem em um pacote.

Por que digo "curtir"? Quando transferimos esses dados para os Trabalhadores, perdemos o controle sobre eles no fluxo principal - não podemos interagir com eles de nenhuma maneira. Há uma segunda limitação aqui. Não podemos transferir todos os tipos de dados para Web Workers. Não podemos fazer isso com uma string; faremos de maneira diferente.



Vamos dar uma olhada no código. Em primeiro lugar, transmitimos dados de maneira um pouco diferente. Aqui está o nosso postMessage. Veja bem, existe uma matriz com loadEvent.target.result. Essa interface nos permite transferir nossos dados como objetos transferíveis, perdendo o controle sobre eles.

A propósito, quem escreve em Rust provavelmente ouvirá algo familiar. E leremos nosso arquivo não como uma string, mas como um ArrayBuffer. Este é um fluxo de dados binários do Lidar aos quais não há acesso direto. Portanto, teremos que fazer outra coisa com eles.



Voltar para os nossos ImageWorkers. Aqui ficou muito mais interessante. Primeiro, pegamos nosso buffer e fazemos uma coisa terrível como Uint8ClampedArray. Esta é uma matriz digitada. Como o nome indica, os dados são os números dos sinais, ou seja, números de zero a 255 que representarão o pixel da nossa imagem.

O terceiro argumento, passamos por uma coisa tão estranha, como a largura, multiplicada pela altura, multiplicada por quatro. Por que exatamente quatro? Exatamente, RGBA. Existem três valores por cor e um por canal alfa.

A seguir, criaremos o ImageData a partir dessa matriz, um tipo de dados especial que pode ser facilmente desenhado na tela. Nada interessante aqui. Nós apenas pegamos um array e passamos para o construtor. Além disso, da mesma maneira, desenhamos nossa imagem na tela, mas usando um método diferente, em ImageData. Além disso, tudo é o mesmo de antes.

Vamos passar para as conclusões. Hoje falei sobre uma tarefa que não realizava há muito tempo. O que eu notei nele?



A suavidade da interface é muito importante. Quando o usuário fica um pouco atrasado, congela um pouco, o botão não é pressionado, isso pode levar a uma forte deterioração do UX. Navegadores funcionam de maneira diferente. Vimos um exemplo esférico com o Safari e o Yandex.Browser. Vimos que, se você verificou a suavidade de sua interface em um navegador, deveria olhar para os outros.

Você precisa fazer algo com o bloqueio de scripts se eles continuarem por um longo tempo. No meu caso, eu coloquei na Web Workers. Mas provavelmente existem outras abordagens, você pode de alguma forma dividi-las em outras menores, aqui você tem que pensar. , Web Workers, .

? . . . , 200 , .

Web Workers . , , .

:


.

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


All Articles