Gerenciamento da complexidade

Manter um aplicativo da web simples pode ser surpreendentemente complicado. Neste módulo, você vai aprender como as APIs da Web funcionam com linhas de execução e como usar isso para padrões comuns de PWA, como gerenciamento de estado.

Simplicidade e complexidade

Na palestra Simple Made Easy, Rich Hickey discute as qualidades das coisas simples e complexas. Ele descreve coisas simples como focar em:

“Uma função, uma tarefa, um conceito ou uma dimensão.”

Mas enfatiza que a simplicidade não tem a ver com:

"Ter uma instância ou executar uma operação."

Se algo é simples ou não, é uma questão de estar interconectado.

A complexidade vem da vinculação, tecelagem ou, para usar o termo de Rich, concluir as coisas juntas. É possível calcular a complexidade contando o número de funções, tarefas, conceitos ou dimensões que algo gerencia.

A simplicidade é essencial no desenvolvimento de softwares porque códigos simples são mais fáceis de entender e manter. A simplicidade também é necessária para aplicativos da web, pois pode ajudar a tornar nosso aplicativo rápido e acessível em todos os contextos possíveis.

Como gerenciar a complexidade dos PWAs

Todo JavaScript que escrevemos para a Web toca no thread principal em um determinado ponto. No entanto, a linha de execução principal tem muita complexidade pronta para uso, sobre a qual você, como desenvolvedor, não tem controle.

A linha de execução principal é:

  • Responsáveis por desenhar a página, que é um processo complexo de várias etapas que envolve o cálculo de estilos, a atualização e a composição de camadas e a pintura na tela.
  • Responsáveis por ouvir e reagir a eventos, incluindo eventos como rolagem.
  • responsável por carregar e descarregar a página.
  • Como gerenciar mídia, segurança e identidade. Isso é tudo antes de qualquer código que você escrever possa ser executado nessa linha de execução, como:
  • Manipular o DOM.
  • O acesso a APIs confidenciais, como recursos do dispositivo ou mídia/segurança/identidade.

Como Surma disse na palestra da Conferência de Desenvolvedores do Chrome 2019 (em inglês), a linha de execução principal é sobrecarregada e paga pouco paga.

Ainda assim, a maior parte do código do aplicativo também reside na linha de execução principal.

Todo esse código aumenta a complexidade da linha de execução principal. A linha de execução principal é a única que o navegador pode usar para posicionar e renderizar o conteúdo na tela. Portanto, quando seu código exige cada vez mais capacidade de processamento para ser concluído, precisamos executá-lo rapidamente, porque cada segundo necessário para executar a lógica do aplicativo é um segundo para que o navegador não consiga responder à entrada do usuário ou reexibir a página.

Quando as interações não se conectam à entrada, quando os frames caem ou quando leva muito tempo para usar um site, os usuários ficam frustrados, sentem que o aplicativo não está funcionando e a confiança nele diminui.

A má notícia? Adicionar complexidade à linha de execução principal é uma maneira quase infalível de dificultar o cumprimento dessas metas. A boa notícia? Como o que o thread principal precisa fazer é claro, ele pode ser usado como um guia para ajudar a reduzir a dependência dele para o restante do aplicativo.

Separações de conceitos

Existem muitos tipos diferentes de trabalho que os aplicativos da Web realizam, mas, em termos gerais, é possível dividi-lo em trabalho que afeta diretamente a IU e trabalho que não tem. O trabalho da interface é um trabalho que:

  • Toca diretamente no DOM.
  • Usa APIs que tocam nos recursos do dispositivo, como notificações ou acesso ao sistema de arquivos.
  • Aborda a identidade, por exemplo, cookies do usuário, armazenamento local ou por sessão.
  • Gerencia mídia, como imagens, áudio ou vídeo.
  • Tem implicações de segurança que exigiriam intervenção do usuário para aprovação, como a API serial da Web.

O trabalho que não é da interface pode incluir itens como:

  • Cálculos puros.
  • Acesso a dados (busca, IndexedDB etc.).
  • Cripto.
  • Mensagens
  • Criação ou manipulação de blobs ou streams.

O trabalho que não é da interface geralmente é reservado pelo trabalho da interface: um usuário clica em um botão que aciona uma solicitação de rede para uma API que retorna resultados analisados que são usados para atualizar o DOM. Ao escrever o código, essa experiência de ponta a ponta costuma ser considerada, mas onde cada parte desse fluxo normalmente não é. Os limites entre o trabalho da interface e o que não é da interface são tão importantes a serem considerados quanto as experiências de ponta a ponta, porque são o primeiro lugar em que você pode reduzir a complexidade da linha de execução principal.

Concentrar-se em uma única tarefa

Uma das maneiras mais diretas de simplificar o código é dividir as funções para que cada uma se concentre em uma única tarefa. As tarefas podem ser determinadas pelos limites identificados ao percorrer a experiência de ponta a ponta:

  • Primeiro, responda à entrada do usuário. Este é o trabalho da interface.
  • Em seguida, faça uma solicitação de API. Esse trabalho não é da interface.
  • Em seguida, analise a solicitação de API. Novamente, o trabalho não é da interface.
  • Em seguida, determine as mudanças no DOM. Pode ser um trabalho de interface ou, se você estiver usando algo como uma implementação de DOM virtual, pode não ser o trabalho de interface.
  • Por fim, faça alterações no DOM. Este é o trabalho da interface.

Os primeiros limites claros são entre o trabalho da interface e o trabalho não relacionado à interface. Há também chamadas de julgamento que precisam ser feitas: fazer e analisar uma solicitação de API é uma tarefa ou duas? Se as mudanças do DOM não forem trabalhos da interface do usuário, elas serão agrupadas com o trabalho da API? Na mesma conversa? Em outra conversa? O nível adequado de separação é fundamental para simplificar a base de código e conseguir remover partes dela da linha de execução principal.

Composição

Para dividir grandes fluxos de trabalho de ponta a ponta em partes menores, é preciso pensar na composição da base de código. Considerando as dicas da programação funcional, considere:

  • Categorizar os tipos de trabalho que seu aplicativo faz.
  • Criar interfaces de entrada e saída comuns para elas.

Por exemplo, todas as tarefas de recuperação de API usam o endpoint da API e retornam uma matriz de objetos padrão, e todas as funções de processamento de dados recebem e retornam uma matriz de objetos padrão.

O JavaScript tem um algoritmo de clone estruturado destinado à cópia de objetos JavaScript complexos. Os Web workers o utilizam ao enviar mensagens, e o IndexedDB o utiliza para armazenar objetos. Se você escolher interfaces que possam ser usadas com o algoritmo de clonagem estruturada, a execução delas ficará ainda mais flexível.

Com isso em mente, é possível criar uma biblioteca de funcionalidades combináveis categorizando seu código e criando interfaces de E/S comuns para essas categorias. O código combináveis é uma marca registrada de bases de código simples: partes intercambiáveis e acopladas com flexibilidade que podem ficar "próximas" entre si, ao contrário de códigos complexos que são profundamente interconectados e, portanto, não podem ser facilmente separados. E, na Web, o código combinável pode ser a diferença entre sobrecarregar a linha de execução principal ou não.

Com o código combinável em mãos, é hora de remover parte dele da linha de execução principal.

Usar workers da Web para reduzir a complexidade

Os Web workers, um recurso da Web muitas vezes subutilizado, mas amplamente disponível, permitem que você mova o trabalho para fora da linha de execução principal.

Os Web workers permitem que um PWA execute (alguns) JavaScript fora da linha de execução principal.

Há três tipos de workers.

Os workers dedicados, que geralmente são considerados na descrição de workers da Web, podem ser usados por um único script em uma única instância em execução de um PWA. Sempre que possível, o trabalho que não interage diretamente com o DOM deve ser movido para um web worker para melhorar o desempenho.

Os Workers compartilhados são semelhantes aos dedicados. A diferença é que vários scripts podem compartilhá-los em várias janelas abertas. Isso proporciona os benefícios de um worker dedicado, mas com um estado compartilhado e contexto interno entre janelas e scripts.

Um worker compartilhado pode, por exemplo, gerenciar o acesso e as transações para o IndexedDB de um PWA e transmitir os resultados das transações em todos os scripts de chamada para permitir que eles reajam às mudanças.

O último worker da Web é um dos mais abordados neste curso: os service workers, que atuam como um proxy para solicitações de rede e são compartilhados entre todas as instâncias de um PWA.

Sua vez de tentar

É hora do código! Crie um PWA do zero com base em tudo o que você aprendeu neste módulo.

Recursos