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" />
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):
- Evento de scroll
- 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 chamadaimageTop
. 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:
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:
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:
O código da animação dos pseudoelementos é bem simples:
@keyframes loading {
from { left: -400%; }
to { left: 0; }
}
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);
}
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:
- Remover o atributo
src
da imagem para ela não baixar junto com os outros elementos - 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çãoshowImage()
para manipular o atributosrcset
edata-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!
Top comments (3)
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 🦤.
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
esetInterval
hahaAcho 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 🦤.