IntersectionObserver in der Praxis: Lazy Loading, Infinite Scroll und Viewport-Erkennung
性能优化
IntersectionObserver: Hochleistungs-Viewport-Erkennung
IntersectionObserver ist eine native asynchrone Viewport-Schnittmengen-API, die herkömmliche Scroll-Event-Listener ersetzt:
| Aspekt | Scroll-Listener | IntersectionObserver |
|---|---|---|
| Ausführung | Hauptthread, synchron | Browser-intern, asynchron |
| Auslösefrequenz | Bei jedem Frame möglich | Nur bei Schnittmengenänderung |
| Leistungskosten | Hoch (manuelles Throttle) | Niedrig (browser-optimiert) |
| Berechnung | getBoundingClientRect() |
Compositor-Thread |
| Unterstützung | Alle Browser | 98%+ globale Abdeckung |
Tiefer Einblick in die Kern-API
Grundlegende Verwendung
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: Frühes/verzögertes Auslösen
// 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: Präzise Auslösekontrolle
// 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
});
Praxis 1: Bild-Lazy-Loading
Grundlegende Implementierung
<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));
}
}
Progressives Laden (Unscharf → Scharf)
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;
}
Praxis 2: Infinite Scroll
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();
}
}
Praxis 3: Virtuelles Scrollen
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);
}
}
Praxis 4: Expositionstracking
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 });
});
Leistungsvergleich
| Szenario | Scroll-Listener | IntersectionObserver | Gewinn |
|---|---|---|---|
| 1000 Lazy-Bilder | ~60fps (Throttle 200ms) | ~60fps (kein Ruckeln) | Flüssigkeit ↑ |
| Scroll-Events/Sek. | ~300 | ~5 | 60× ↓ |
| getBoundingClientRect | 1000 pro Auslöser | 0 (Browser berechnet) | 100% ↓ |
| Hauptthread-Zeit | 15-30ms/Frame | <1ms/Callback | 30× ↓ |
Browser-Kompatibilität
| Browser | Version | Hinweise |
|---|---|---|
| Chrome | 51+ | Volle Unterstützung |
| Firefox | 55+ | Volle Unterstützung |
| Safari | 12.1+ | Volle Unterstützung |
| Edge | 15+ | Volle Unterstützung |
| IE | Keine | Polyfill nötig |
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!;
});
}
Best Practices
- Rechtzeitig unobserve aufrufen: Beobachtung nach dem Laden stoppen, um Callbacks zu reduzieren
- Sinnvolles rootMargin:
200-500pxfür Lazy Load,0pxfür Expositionstracking - Beobachtungen bündeln: Ein Observer für viele Elemente, nicht einer pro Element
- Threshold-Wahl:
[0]für Lazy Load,[0, 0.25, 0.5, 0.75, 1]für Fortschritt - Beim Aufräumen disconnect aufrufen:
disconnect()beim Unmount aufrufen, um Speicherlecks zu vermeiden
Zusammenfassung
IntersectionObserver ist die Standard-API für Viewport-Erkennung für moderne Frontends und bietet Leistungsgewinne um Größenordnungen gegenüber Scroll-Listenern. Lazy Loading, Infinite Scroll, Expositionstracking und Animationstrigger sind ihre vier Kernanwendungsfälle, wobei rootMargin und threshold eine feingranulare Steuerung ermöglichen.
Verwenden Sie Image Compress, um die Größe lazily geladener Bilder zu optimieren, Image Resize zur Normalisierung der Abmessungen und Image Convert zur Konvertierung ins WebP-Format.
Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →
#IntersectionObserver#懒加载#虚拟滚动#性能#视口检测