Content loaders (também conhecidos como placeholder loaders, ou skeleton loaders) além de serem legais são muito úteis para UX, como por exemplo:
- O loading mostra uma prévia do que será o conteúdo.
- O usuário tem a percepção que o conteúdo carrega mais rápido.
- Cria uma expectativa no usuário e evita surpresas.
Problemas
Fato é que se você já tentou implementar alguma vez, provavelmente já se pegou com algum desses problemas:
- Fidelidade: É usado uma abstração genérica de content loaders já prontos, mas que não representa fielmente o que é o componente fazendo com que ocorram glitchs no carregamento. Também pode ser considerado um problema de UX a partir do momento que o usuário espera uma coisa e aparece outra. Diferença de tamanhos pode fazer com que os elementos “dancem” na tela. As vezes é importante que o loader seja fiel ao componente.
- Manutenibilidade: A princípio é criado um content loader fiél ao componente, mas diante da necessidade de alterações no componente por novas demandas, torna-se necessário também alterar o content loader para que sempre fiquem ‘sincronizados’. Um problema de manutenibilidade. Uma simples alteração pode ter um custo caro de tempo por conta disso, e uma alteração feita às pressas pode fazer com que os dois fiquem dessincronizados, trazendo de volta o problema da falta de fidelidade.
Quando fugimos de um problema acabamos encontrando o outro. Por querer fugir destes dois problemas encontrei uma abordagem que solucionou minhas necessidades, e é isto que irei falar aqui. Antes de mais nada queria mencionar que a inspiração veio do superplayer.fm (se você entrar no site logo na home verá os content loaders). Talvez a implementação não seja a mesma, mas o princípio por trás é o mesmo que foi usado no superplayer.
Conceito
Essencialmente um content loader não é muito diferente do que uma variação do próprio componente.
Se você tem um componente de Button que, por padrão, tem uma aparência e um formato específicos, e quer criar botões que tenham aparências diferentes, você cria propriedades como style, size, theme, de maneira que seja simples aplicar vários tipos de variações no botão. O conceito geral aqui é o mesmo. Usar o próprio componente como loader, e não separar. Pois se você quer manter duas coisas iguais, não há nada mais confiável do que usar um só, com variação.
Pra ser mais claro no exemplo, o componente terá uma propriedade que diz que ele terá que se comportar como um loader, e depois simplesmente mockar o conteúdo dele.
Preenchimento
A maioria dos content loaders são barras cinzas que passam a percepção de que ali é o lugar de algo. Nestes casos uma variação de loading com um fundo cinza ja é suficiente. Quando um elemento já tem uma altura/largura definidos, é só aplicar o background-color em uma área sem conteúdo, sem segredo.
Existem muitos casos, entretanto, que quem molda a altura e/ou largura é o próprio conteúdo. Como por exemplo, texto corrido. Você nunca sabe a área de um parágrafo sem antes saber o conteúdo que vai dentro. Existe uma técnica em que você mocka o conteúdo e esconde ele. Dessa maneira, mesmo sendo impossível ser sempre 100% fiél, você consegue resultados muito próximos.
Você pode usar da mesma ideia com botões adicionando a propriedade pointer-events: none; evitando que o botão seja clicável. Já para imagens, é preciso que tenha uma dimensão especificada, caso contrário o bloco terá 0px de altura. Depois só adicionar um fundo.
Shimmer
Shimmer, em tradução livre, significa brilho. No contexto dos content loaders, ele é aquele gradiente que ‘passa’ pelas barras cinzas, fazendo-as brilhar. O shimmer aumenta a percepção do usuário de que aquela área está sendo carregada. O motivo não é muito diferente de um spinner, onde o importante é que o usuário veja algo se mexendo para não pensar que a interface travou ou algo deu errado.
A técnica mais comum consiste em criar um background gradient em que seu background-position é rotacionado por um animation keyframes. Isso cria a falsa ilusão de que o gradiente está andando, mas na verdade é só a posição do background. Aqui tem um exemplo de um Animated Gradient Background. E aqui está um exemplo que criei para demonstrar o shimmer funcionando.
Tradeoffs e Conclusão
Como tudo no mundo, há desvantagens que devem ser considerados. Isso NÃO É silver bullet, isto é, não resolve todos os problemas de uma vez só. Cada tipo de abordagem faz com que você ganhe em determinados pontos e perca em outros, e com esta não é diferente. Esta abordagem resolve os dois problemas citados no começo deste artigo, porém traz consigo um outro: ele acopla o estado de loading dentro do componente. Em alguns casos pode não ser um problema, como o exemplo que dei do Button, que devido a baixa complexidade não se torna realmente um problema. Mas se for um componente relativamente grande, talvez você tenha problemas de grande complexidade, o que afetaria diretamente a manutenibilidade também. Por isso deve-se sempre considerar que pode se tornar um problema e se realmente compensa para cada caso.
A minha dica é: quanto mais granular for sua UI, mais fácil se torna aplicar loaders. Mesmo que no final você use-os para criar algo maior, eles ainda estão isolados em uma baixa complexidade.
Ainda não existe uma solução silver-bullet para content loaders. Mas um content loader handcrafted e bem feito pode trazer bons benefícios. Criei um exemplo completo usando React que você pode ver clicando aqui. No exemplo, usando styled-components, eu criei um factory em que aplica um css bem genérico quando contém a prop contentLoading, assim eu consigo reusá-lo em vário cenários. Ainda assim, seria necessário adaptações para vários outros edge cases.