Como fazer um lazyload compatível com qualquer browser + Placeholder

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.

  • lazyload
  • imagem
  • performance
  • tempo de carregamento
  • otimização
  • javascript
  • scroll event
  • intersection observer
  • crossbrowser
Front-end

Shortcut

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", 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.

Saber o que fazer com imagens sem texto alternativo é outra forma de melhorar o SEO do site através delas.

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?

<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:

<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.

<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 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:

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:

window.addEventListener('scroll', checkNotLoadedImages);

Veja o que faz essa função callback:

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:

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
Animação mostrando que as imagens são baixadas apenas quando se aproximam da área de visualização

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:

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) 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:

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:

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:

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.

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

Ah, claro, a função showImage():

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:

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
Skeleton placeholder

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 <picture>:

<!-- 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:

.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:

.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 <picture> possui uma cor de fundo cinza

  • A imagem possui zero de opacidade

  • O ::after e o ::before do elemento <picture> 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
Skeleton placeholder sem animação

O código da animação dos pseudoelementos é bem simples:

@keyframes loading {
  from { left: -400%; }
  to   { left: 0;     }
}
Skeleton placeholder com animação
Skeleton placecholder com animação

E por último, quando a imagem finalmente for baixada, ela surge com uma transição no opacity:

.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():

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:

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
Skeleton placeholder sendo substituído pela imagem

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.

<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>
@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;
}
/* 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 . 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!

Veja outros posts sobre Front-end