Como fazer um lazyload compatível com qualquer browser + Placeholder
Tags: lazyload, performance, intersectionobserver, imagem
Published: 2022-07-31 15:53:59

## Com algumas linhas de Javascript, você pode fazer esse lazyload compatível com qualquer navegador. Além de melhorar o desempenho e SEO do seu site, crie um skeleton placeholder estiloso.
 
No final da página, tem o código completo de como fazer o efeito de lazyload.
 
Mas depois que conseguir entregar a task do Jira, recomendo muito que volte e leia tudo para entender.
 
Se você está aqui, é porque já sabe o que quer: criar o efeito de lazyload.
 
Mas se ainda tem dúvidas de como o lazyload funciona, indico que você leia a introdução do meu último post. Lá eu falei sobre o [atributo loading="lazy"](https://demenezes.dev/posts/atributo-loading-lazy/), e os cuidados que você deve ter com ele.
 
## O que é Lazyload?
 
Em resumo, o lazyload é atrasar o carregamento de recursos pesados.
 
Geralmente são imagens, vídeos e iframes. E o download desses recursos é feito apenas quando o usuário precisa deles.
 
No post que citei, também comentei as vantagens de fazer o lazyload, como desempenho, SEO e consumo de banda de internet.
 
Mas vamos ao que interessa.
 
## Como fazer o efeito de Lazyload: os atributos src e data-src
 
Entender esses atributos são vitais para criar o lazyload.
 
Como você deve imaginar, o atributo `src` informa para o navegador a fonte do arquivo da imagem. E é através dele que o navegador baixa esse arquivo. Mas e se o atributo `src` não existe?
 
```html
<img alt="Sweet cat" class="image" />
```
 
Acontece exatamente como quero: o navegador não baixa nada e a página carrega mais rápido.
 
De qualquer forma, eu tenho que informar para o navegador de onde baixar a imagem. Para isso, eu crio o `data-src`:
 
```html
<img
  data-src="cat.png"
  alt="Sweet cat"
  class="image"
/>
```
 
Como o `data-src` não possui um comportamento nativo no navegador, ele vai servir apenas como "base de dados". Ou seja, irá armazenar a URL da imagem. Por enquanto.
 
É muito importante que o tamanho da imagem seja declarado através dos atributos `width` e `height`. Caso contrário, a imagem ficará com a altura de 0, e irá causar problemas mais tarde.
 
```html
<img
  data-src="cat.png"
  alt="Sweet cat"
  width="400"
  height="400"
  class="image"
/>
```
 
Dessa forma, você evita que aconteça o temido Cumulative Layout Shift (CLS). Que é quando os elementos da sua página mudam de lugar, conforme outros elementos vão sendo desenhados. O [vídeo no começo dessa página sobre CLS](https://web.dev/cls/) mostra a gravidade desse problema.
 
---
 
Legal, a imagem não carregou de primeira, e a página abriu mais rápido, ótimo. Veja agora como exibir essa imagem assim que ela entra na viewport.
 
## Como fazer o efeito de Lazyload: baixar a imagem
 
Existem duas formas de fazer isso (usarei Javascript nas duas):
 
1. Evento de scroll
2. Intersection Observer
 
### Monitorar imagens pelo evento de scroll
 
Vou criar duas constantes:
 
```javascript
const images = Array.from(document.querySelectorAll('.image[data-src]'));
const screenHeight = window.innerHeight;
```
 
- `images` é uma lista com todas as imagens aptas a usar o lazyload
- `screenHeight` é a altura da tela em pixels
 
Agora vou monitorar o evento de scroll da janela:
 
```javascript
window.addEventListener('scroll', checkNotLoadedImages);
```
 
Veja o que faz essa função callback:
 
```javascript
function checkNotLoadedImages() {
  const notLoadedImages = images.filter(image => !image.src);
  
  notLoadedImages.forEach(image => {
    const imageTop = image.getBoundingClientRect().top;
    
    if (imageTop < screenHeight) {
      image.src = image.dataset.src; // é aqui que a magia acontece
      
      // Você pode até remover o atributo data-src, mas não é tão necessário
      image.removeAttribute('data-src');
    }
  });
}
```
 
Sempre que o usuário usar a barra de rolagem:
 
- Eu filtro a lista de imagens, e pego apenas as que ainda não foram carregadas
- Depois eu itero por essa nova lista com o `forEach`. Para cada imagem não carregada, eu crio uma constante chamada `imageTop`. Ela recebe um número, que é a distância em pixels do topo da imagem para o tipo topo da tela
- Se esse valor for menor que a altura da tela (`screenHeight`), significa que ela está visível. Navegador, eu escolho você: baixe a imagem
 
A imagem é baixada no momento em que o atributo `src` é preenchido com o valor que está em `data-src`:
 
```javascript
image.src = image.dataset.src;
```
 
Veja nesse teste, usando a aba Network, que as imagens são baixadas apenas quando entram na viewport:
 
![Efeito lazyload em ação](https://www.datocms-assets.com/64896/1658110924-3.gif)
 
Sugiro que você insira uma linha assim `checkNotLoadedImages();` no fim do seu código. Isso baixará as imagens que já vêm acima da dobra assim que o site carrega.
 
**Dica de viewport:**
 
Você pode querer baixar uma imagem um pouco antes de ela entrar na viewport.
 
Para isso, aumente a sua área de ativação da imagem com um *offset*. Exemplo:
 
```javascript
const offset = 500;
const screenHeight = window.innerHeight + offset;
```
 
Assim, as imagens serão baixadas quando estiverem a menos de 500px de aparecerem na viewport.
 
Infelizmente, nem tudo são flores. Essa abordagem obriga o navegador a executar uma função a cada scroll. E como citei que uma das vantagens de criar o lazyload é melhorar a performance do site, isso começa a parecer contraditório.
 
Existe outro método mais performático que o evento de scroll para fazer o lazyload.
 
### Monitorar imagens com o Intersection Observer
 
Interoquê?!
 
O [Intersection Observer (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) permite que você observe um elemento, em vez de observar o scroll como antes. Quando ele estiver em intersecção com um elemento pai ou com a viewport, você pode executar uma função callback.
 
Exatamente como fiz no exemplo com o evento de scroll, porém cada imagem é monitorada isoladamente. Isso permite remover aquele `forEach` que fica "procurando" as imagens a cada scroll.
 
Vou começar com o que você já conhece:
 
```javascript
const offset = 500;
const images = Array.from(document.querySelectorAll('.image[data-src]'));
```
 
Não há nada novo sob o sol.
 
Vou criar o `observer` e passar dois parâmetros para ele:
 
```javascript
const observer = new IntersectionObserver(checkEntries, intersectionOptions);
```
 
- `checkEntries`: uma função callback que serve para verificar as entradas. Cada entrada é um elemento a ser observado, no caso, as imagens
- `intersectionOptions`: um objeto de opções, acesse a documentação para entender a sua função
 
Veja agora cada um em detalhes:
 
```javascript
function checkEntries(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) showImage(entry.target);
  })
}
```
 
Cada vez que uma imagem entrar na tela essa função será executada.
 
Se a entrada (`entry`) possui o valor do atributo `isIntersecting` como `true`, então ela está visível na viewport. Nesse momento, executo a função `showImage()` e informo como parâmetro o elemento HTML que fica em `entry.target`.
 
```javascript
const intersectionOptions = {
  root: null,
  rootMargin: offset + 'px',
  threshold: 0
};
```
 
Ah, claro, a função `showImage()`:
 
```javascript
function showImage(image) {
  image.src = image.dataset.src; // a magia novamente
  observer.unobserve(image);
}
```
 
Além de criar o atributo `src` para a imagem ser baixada, vou chamar o método `unobserve`. Isso serve para não observar aquela imagem mais, já que ela já foi baixada.
 
Agora vou de fato "observar" cada imagem:
 
```javascript
images.forEach(image => observer.observe(image));
```
 
Se você refizer o teste da aba network, verá que o efeito de lazyload criado continua funcionando normalmente.
 
---
 
Até aqui, já seria possível dizer que acabou.
 
Mas e se, mesmo com o *offset*, a internet atrasa e a imagem não carrega? O usuário vai ver um pedaço da tela em branco?
 
Para contornar isso de uma forma elegante, vou criar um placeholder.
 
## Como fazer o efeito de Lazyload: placeholder
 
"Aguarde enquanto baixamos a imagem para você".
 
Placeholder (ou marcador de posição) é algo que ocupa o espaço da imagem, até que ela seja carregada.
 
Existem centenas de tipos de placeholders, mas aqui vou focar em um tipo bem conhecido chamado de **Skeleton Placeholder**:
 
![Exemplos de Skeleton placeholder, um fundo cinza com uma animação de loading](https://www.datocms-assets.com/64896/1659057409-p.png)
 
Veja uma explicação geral de como esse placeholder funciona:
 
- A imagem será colocada dentro de uma tag `<picture>`
- Essa tag terá as mesmas medidas da imagem
- Ela também receberá alguns estilos como cor de fundo, e uma animação em CSS
- Assim que a imagem for carregada, ela ocupa o lugar do placeholder
 
### HTML do placeholder
 
Aqui é bem simples.
 
Ajuste seu HTML e coloque as imagens dentro de um `&lt;picture&gt;`:
 
```html
<!-- ANTES -->
<img
  data-src="https://placekitten.com/400/400"
  width="400"
  height="400"
  class="image"
/>

<!-- DEPOIS -->
<picture class="picture lazyload-not-loaded">
  <img
    data-src="https://placekitten.com/400/400"
    width="400"
    height="400"
    class="image"
  />
</picture>
```
 
### CSS do placeholder
 
Esse elemento que envolve a imagem merece receber alguns estilos:
 
```css
.picture {
  display: inline-block;
  overflow: hidden;
}

.picture .image {
  display: block;
  max-width: 100%;
  height: auto;
  transition: .3s;
}
```
 
Isso vai servir para ele ter o mesmo tamanho da imagem, e também adaptar a imagem a ele.
 
Veja agora como vai ficar o placeholder, enquanto a imagem não baixou. Para isso, vou usar a classe `lazyload-not-loaded`:
 
```css
.picture.lazyload-not-loaded {
  position: relative;
  background-color: lightgray;
}

.picture.lazyload-not-loaded .image {
  opacity: 0;
}

.picture.lazyload-not-loaded::before,
.picture.lazyload-not-loaded::after {
  content: '';
  position: absolute;
  top: 0;
  left: -400%;
  width: 400%;
  height: 100%;
  animation-name: loading;
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
  background-image: linear-gradient(135deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0) 30%,
    rgba(255, 255, 255, 1) 50%,
    rgba(255, 255, 255, 0) 70%,
    rgba(255, 255, 255, 0) 100%
  );
}

.picture.lazyload-not-loaded::after {
  animation-delay: 1.5s;
}
```
 
O que foi feito aqui?
 
- O elemento `&lt;picture&gt;` possui uma cor de fundo cinza
- A imagem possui zero de opacidade
- O `::after` e o `::before` do elemento `&lt;picture&gt;` irão compor aquela linha branca que transita sobre o placeholder
- O `::after` possui um delay apenas para não acontecer junto com o `::before` 
 
E o resultado é esse:
 
![Skeleton placeholder sem animação, apenas com fundo cinza estático](https://www.datocms-assets.com/64896/1659057658-pp.png)
 
O código da animação dos pseudoelementos é bem simples:
 
```css
@keyframes loading {
  from { left: -400%; }
  to   { left: 0;     }
}
```
 
![Skeleton placeholder com animação](https://www.datocms-assets.com/64896/1659057943-1.gif)
 
E por último, quando a imagem finalmente for baixada, ela surge com uma transição no `opacity`:
 
```css
.picture.lazyload-loaded .image {
  opacity: 1;
}
```
 
Agora que o CSS está pronto, preciso controlar o comportamento das classes para fazer o lazyload funcionar de verdade.
 
A classe `lazyload-not-loaded` será inserida no `<picture>` diretamente no HTML, pois é assim que a página é carregada. Logo depois que a imagem é baixada, a classe será trocada por `lazyload-loaded` via Javascript.
 
### Javascript do placeholder
 
O ajuste precisa ser feito apenas na função `showImage()`:
 
```javascript
function showImage(image) {
  const picture = image.parentNode; // aqui
  picture.classList.remove('lazyload-not-loaded'); // aqui
  picture.classList.add('lazyload-loaded'); // e aqui
  image.src = image.dataset.src;
  observer.unobserve(image);
}
```
 
Como essa troca é muito rápida, aconselho você adicionar um delay a essa função apenas como teste. Assim será possível ver a transição delas:
 
```javascript
function showImage(image) {
  setTimeout(() => {
    const picture = image.parentNode;
    picture.classList.remove('lazyload-not-loaded');
    picture.classList.add('lazyload-loaded');
    image.src = image.dataset.src;
    observer.unobserve(image);
  }, 2000);
}
```
 
![Skeleton placeholder sendo substituído pela imagem](https://www.datocms-assets.com/64896/1659058158-2.gif)
 
Caso você queira aplicar outro efeito de surgimento da imagem:
 
- Altere o seletor `.picture.lazyload-not-loaded .image` para a imagem antes de ser carregada
- E o seletor `.picture.lazyload-loaded .image` após ela ser carregada
 
Essa explicação toda foi bem extensa, eu sei.
 
Então resolvi aglomerar (mas de máscara) todo o código logo abaixo. Assim fica mais fácil de você adaptar ele no seu projeto.
 
## Exemplo de código Lazyload
 
"Tá aí o que você queria".
 
Se delicie com essa maravilha de código que tenho impresso em um quadro no meu escritório (brincadeira).
 
Mas poderia ser sério.
 
Mas é brincadeira.
 
```html
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/400/400" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/401/401" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/402/402" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/403/403" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/404/404" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/405/405" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/406/406" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/407/407" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/408/408" width="400" height="400" class="image"></picture>
<picture class="picture lazyload-not-loaded"><img data-src="https://placekitten.com/409/409" width="400" height="400" class="image"></picture>

```
 
```css
@keyframes loading {
  from { left: -400%; }
  to   { left: 0;     }
}

.picture {
  display: inline-block;
  overflow: hidden;
}

.picture .image {
  display: block;
  max-width: 100%;
  height: auto;
  transition: .3s;
}

.picture.lazyload-not-loaded {
  position: relative;
  background-color: lightgray;
}

.picture.lazyload-not-loaded .image {
  opacity: 0;
}

.picture.lazyload-not-loaded::before,
.picture.lazyload-not-loaded::after {
  content: '';
  position: absolute;
  top: 0;
  left: -400%;
  width: 400%;
  height: 100%;
  animation-name: loading;
  animation-duration: 3s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
  background-image: linear-gradient(135deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0) 30%,
    rgba(255, 255, 255, 1) 50%,
    rgba(255, 255, 255, 0) 70%,
    rgba(255, 255, 255, 0) 100%
  );
}

.picture.lazyload-not-loaded::after {
  animation-delay: 1.5s;
}

.picture.lazyload-loaded .image {
  opacity: 1;
}
```
 
```javascript
/* SCROLL EVENT */

/*
const images = Array.from(document.querySelectorAll('.image[data-src]'));
const offset = 500;
const screenHeight = window.innerHeight + offset;

window.addEventListener('scroll', checkNotLoadedImages);

function checkNotLoadedImages() {
  const notLoadedImages = images.filter(image => !image.src);
  
  notLoadedImages.forEach(image => {
    const imageTop = image.getBoundingClientRect().top;
    
    if (imageTop < screenHeight) {
      image.src = image.dataset.src; // é aqui que a magia acontece
    }
  });
}
*/

/* INTERSECTION OBSERVER */

const offset = 500;
const images = Array.from(document.querySelectorAll('.image[data-src]'));

const intersectionOptions = {
  root: null,
  rootMargin: offset + 'px',
  threshold: 0
};

function checkEntries(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) showImage(entry.target);
  })
}

const observer = new IntersectionObserver(checkEntries, intersectionOptions);

function showImage(image) {
  const picture = image.parentNode;
  picture.classList.remove('lazyload-not-loaded');
  picture.classList.add('lazyload-loaded');
  image.src = image.dataset.src;
  observer.unobserve(image);
}

images.forEach(image => observer.observe(image));
```
 
## Callback
 
Em resumo, para fazer o efeito de lazyload você precisa de dois passos:
 
1. Remover o atributo `src` da imagem para ela não baixar junto com os outros elementos
2. Definir o momento que deseja baixar ela, e isso pode ser feito com o evento de scroll ou o Intersection Observer
 
E para completar a cereja do bolo, ainda pode colocar um placeholder com estilo.
 
Existem algumas melhorias que podem ser feitas nesse lazyload:
 
- Em alguns casos é usada a tag `<source>` dentro da tag `<picture>`, para [fornecer a imagem adequada para diferentes tamanhos de tela](https://www.w3schools.com/tags/tag_picture.asp). Esses elementos também precisam de lazyload. Então será preciso alterar a função `showImage()` para manipular o atributo `srcset` e `data-srcset` deles
- Outra ideia que gosto muito é usar como placeholder a imagem em baixa qualidade. Para isso, você precisa deixar o atributo `src` na tag `<img>`, porém com o endereço da imagem em baixa qualidade. Essas imagens possuem cerca de 1kb ou menos, então elas carregam muito rápido, e são substituídas pela imagem original assim que ela é baixada
 
Aqui o céu é o limite.
 
Pretendo retomar essas ideias em posts futuros.
 
Te ajudei de alguma forma? Então deixa um valeu aqui nos comentários ;)
 
Obrigado pela sua leitura!