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

  1. Rechtzeitig unobserve aufrufen: Beobachtung nach dem Laden stoppen, um Callbacks zu reduzieren
  2. Sinnvolles rootMargin: 200-500px für Lazy Load, 0px für Expositionstracking
  3. Beobachtungen bündeln: Ein Observer für viele Elemente, nicht einer pro Element
  4. Threshold-Wahl: [0] für Lazy Load, [0, 0.25, 0.5, 0.75, 1] für Fortschritt
  5. 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#懒加载#虚拟滚动#性能#视口检测