IntersectionObserver na prática: lazy loading, scroll infinito e detecção de viewport
性能优化
IntersectionObserver: detecção de viewport de alto desempenho
O IntersectionObserver é uma API nativa assíncrona de interseção com o viewport que substitui os listeners de eventos scroll tradicionais:
| Aspecto | Listener de scroll | IntersectionObserver |
|---|---|---|
| Execução | Thread principal, síncrono | Interno do navegador, assíncrono |
| Frequência de disparo | Possível a cada frame | Apenas na mudança de interseção |
| Custo de desempenho | Alto (throttle manual) | Baixo (otimizado pelo navegador) |
| Cálculo | getBoundingClientRect() |
Thread do compositor |
| Suporte | Todos os navegadores | Cobertura global 98%+ |
Análise profunda da API central
Uso básico
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
console.log(entry.isIntersecting);
console.log(entry.intersectionRatio);
console.log(entry.target);
console.log(entry.boundingClientRect);
console.log(entry.rootBounds);
console.log(entry.time);
});
},
{
root: null,
rootMargin: '0px',
threshold: [0, 0.5, 1.0]
}
);
observer.observe(document.querySelector('.target'));
observer.unobserve(element);
observer.disconnect();
rootMargin: disparo antecipado/atrasado
// Trigger 200px before entering viewport (preload)
const lazyObserver = new IntersectionObserver(callback, {
rootMargin: '200px 0px'
});
// Trigger only when fully inside viewport
const fullObserver = new IntersectionObserver(callback, {
rootMargin: '-100px 0px'
});
threshold: controle preciso do disparo
// Trigger at every 10% intersection
const observer = new IntersectionObserver(callback, {
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
});
// Only when fully visible
const observer2 = new IntersectionObserver(callback, {
threshold: 1.0
});
Prática 1: Lazy loading de imagens
Implementação básica
<img data-src="photo.webp" alt="description" class="lazy-img" />
class LazyImageLoader {
private observer: IntersectionObserver;
constructor() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
img.removeAttribute('data-src');
this.observer.unobserve(img);
}
});
},
{ rootMargin: '200px 0px' }
);
}
observe(container: HTMLElement) {
container.querySelectorAll<HTMLImageElement>('.lazy-img')
.forEach((img) => this.observer.observe(img));
}
}
Carregamento progressivo (desfocado → nítido)
class ProgressiveImageLoader {
private observer: IntersectionObserver;
constructor() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target as HTMLImageElement;
const thumb = img.dataset.thumb!;
img.src = thumb;
img.classList.add('blurred');
const fullSrc = img.dataset.full!;
const fullImg = new Image();
fullImg.onload = () => {
img.src = fullSrc;
img.classList.remove('blurred');
};
fullImg.src = fullSrc;
this.observer.unobserve(img);
});
},
{ rootMargin: '300px 0px' }
);
}
}
.blurred {
filter: blur(20px);
transition: filter 0.3s ease;
}
Prática 2: Scroll infinito
class InfiniteScroll {
private observer: IntersectionObserver;
private loading = false;
private page = 1;
constructor(
private container: HTMLElement,
private loadMore: (page: number) => Promise<HTMLElement[]>
) {
const sentinel = document.createElement('div');
sentinel.className = 'scroll-sentinel';
container.appendChild(sentinel);
this.observer = new IntersectionObserver(
async (entries) => {
if (!entries[0].isIntersecting || this.loading) return;
this.loading = true;
this.page++;
const items = await this.loadMore(this.page);
items.forEach((item) => container.insertBefore(item, sentinel));
this.loading = false;
},
{ rootMargin: '500px' }
);
this.observer.observe(sentinel);
}
destroy() {
this.observer.disconnect();
}
}
Prática 3: Scroll virtual
class VirtualScroller<T> {
private observer: IntersectionObserver;
private visibleRange = { start: 0, end: 0 };
private itemHeight = 48;
constructor(
private container: HTMLElement,
private items: T[],
private renderItem: (item: T, index: number) => HTMLElement
) {
this.container.style.height = `${this.items.length * this.itemHeight}px`;
this.container.style.position = 'relative';
this.setupObserver();
}
private setupObserver() {
const sentinelTop = document.createElement('div');
const sentinelBottom = document.createElement('div');
this.container.prepend(sentinelTop);
this.container.appendChild(sentinelBottom);
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
this.updateVisibleItems();
});
});
this.observer.observe(sentinelTop);
this.observer.observe(sentinelBottom);
}
private updateVisibleItems() {
const scrollTop = this.container.scrollTop;
const viewHeight = this.container.clientHeight;
const start = Math.floor(scrollTop / this.itemHeight);
const end = Math.min(
start + Math.ceil(viewHeight / this.itemHeight) + 2,
this.items.length
);
if (start === this.visibleRange.start && end === this.visibleRange.end) return;
this.visibleRange = { start, end };
this.render();
}
private render() {
const fragment = document.createDocumentFragment();
for (let i = this.visibleRange.start; i < this.visibleRange.end; i++) {
const el = this.renderItem(this.items[i], i);
el.style.position = 'absolute';
el.style.top = `${i * this.itemHeight}px`;
fragment.appendChild(el);
}
this.container.innerHTML = '';
this.container.appendChild(fragment);
}
}
Prática 4: Rastreamento de exposição
class ExposureTracker {
private observer: IntersectionObserver;
private tracked = new WeakSet<Element>();
constructor(private onExpose: (element: Element) => void) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
if (this.tracked.has(entry.target)) return;
this.tracked.add(entry.target);
this.onExpose(entry.target);
});
},
{ threshold: 0.5 }
);
}
track(elements: Element[]) {
elements.forEach((el) => this.observer.observe(el));
}
}
const tracker = new ExposureTracker((el) => {
const id = el.getAttribute('data-track-id');
analytics.track('element_exposed', { id });
});
Comparação de desempenho
| Cenário | Listener de scroll | IntersectionObserver | Ganho |
|---|---|---|---|
| 1000 imagens lazy | ~60fps (throttle 200ms) | ~60fps (sem travamentos) | Suavidade ↑ |
| Eventos de scroll/seg | ~300 | ~5 | 60× ↓ |
| getBoundingClientRect | 1000 por disparo | 0 (calculado pelo navegador) | 100% ↓ |
| Tempo de thread principal | 15-30ms/frame | <1ms/callback | 30× ↓ |
Compatibilidade de navegadores
| Navegador | Versão | Notas |
|---|---|---|
| Chrome | 51+ | Suporte completo |
| Firefox | 55+ | Suporte completo |
| Safari | 12.1+ | Suporte completo |
| Edge | 15+ | Suporte completo |
| IE | Nenhuma | Requer polyfill |
if ('IntersectionObserver' in window) {
// Use IntersectionObserver
} else {
// Fallback: load all images immediately
document.querySelectorAll('.lazy-img').forEach((img) => {
(img as HTMLImageElement).src = (img as HTMLImageElement).dataset.src!;
});
}
Boas práticas
- Unobserve rapidamente: pare de observar após o carregamento para reduzir callbacks
- rootMargin sensato:
200-500pxpara lazy load,0pxpara rastreamento de exposição - Observações em lote: um Observer para muitos elementos, não um por elemento
- Escolha do threshold:
[0]para lazy load,[0, 0.25, 0.5, 0.75, 1]para progresso - Disconnect na limpeza: chame
disconnect()ao desmontar para evitar vazamentos de memória
Resumo
O IntersectionObserver é a API padrão de detecção de viewport para frontends modernos, oferecendo ganhos de desempenho de ordens de magnitude em relação aos listeners de scroll. Lazy loading, scroll infinito, rastreamento de exposição e gatilhos de animação são seus quatro casos de uso centrais, e rootMargin e threshold permitem controle fino.
Use Image Compress para otimizar o tamanho das imagens lazy, Image Resize para normalizar dimensões e Image Convert para converter ao formato WebP.
Experimente estas ferramentas executadas localmente no navegador — nenhum cadastro necessário →
#IntersectionObserver#懒加载#虚拟滚动#性能#视口检测