DEV Community

Cover image for Como fazer um lazyload compatível com qualquer browser + Placeholder
deMenezes
deMenezes

Posted on • Originally published at demenezes.dev

1 1

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.

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.

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" />
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

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');
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Veja nesse teste, usando a aba Network, que as imagens são baixadas apenas quando entram na viewport:

Efeito lazyload em açã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;
Enter fullscreen mode Exit fullscreen mode

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]'));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • 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);
  })
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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

function showImage(image) {
  image.src = image.dataset.src; // a magia novamente
  observer.unobserve(image);
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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

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

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

@keyframes loading {
  from { left: -400%; }
  to   { left: 0;     }
}
Enter fullscreen mode Exit fullscreen mode

Skeleton placeholder 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode
@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;
}
Enter fullscreen mode Exit fullscreen mode
/* 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));
Enter fullscreen mode Exit fullscreen mode

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!

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (3)

Collapse
 
raulferreirasilva profile image
Raul Ferreira

Cara que conteúdo fenomenal, estou adorando aprender com seus artigos de vdd, não conhecia a Intersection Observer API, achei fenomenal, me deu altas ideias para projetos futuros, pretendo fazer uma pokebola, aquelas padrão, vou aplicar esse lazyload junto com o placeholder. Muito obrigado por compartilhar seu conhecimento 🦤.

Collapse
 
demenezes profile image
deMenezes

Obrigado cara :D fico muito feliz

Além da Intersection Observer, que observa quando um elemento entra em intersecção com outro, existe a Intersection Mutation, que observa quando um elemento HTML é alterado.

Já precisei usar essa em situações onde tinha Javascript que não era feito por mim rodando, daí após ele mudar algo na DOM, tu executa uma função callback em cima.

É legal que evita aquelas gambiarras com setTimeout e setInterval haha

Collapse
 
raulferreirasilva profile image
Raul Ferreira

Acho que tu acabou de me salvar, estou com um projeto, código legado, que as coisas estão começando a ficar complicadas de mexer, por conta de ser estruturado em AJAX e Jquery. Vou tentar implementar para ver se consigo corrigir um bug que já está um tempo em produção, muito obrigado, estava a dias procurando algo que eu pudesse usar, acho que vai servir bem KKKKKK 🦤.

Cloudinary image

Video API: manage, encode, and optimize for any device, channel or network condition. Deliver branded video experiences in minutes and get deep engagement insights.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay