personal website

lazy load vibes in batches instead of all at once

+31 -7
+31 -7
src/components/sections/Vibes.tsx
··· 1 - import { useEffect, useState } from "react" 1 + import { useEffect, useRef, useState } from "react" 2 + 3 + const BATCH_SIZE = 20 2 4 3 5 export function Vibes() { 4 - const [vibes, setVibes] = useState<string[]>([]) 6 + const [allVibes, setAllVibes] = useState<string[]>([]) 7 + const [visibleCount, setVisibleCount] = useState(BATCH_SIZE) 8 + const sentinelRef = useRef<HTMLDivElement>(null) 5 9 6 10 useEffect(() => { 7 11 fetch("/vibes/manifest.json") 8 12 .then((res) => res.json()) 9 - .then(setVibes) 10 - .catch(() => setVibes([])) 13 + .then(setAllVibes) 14 + .catch(() => setAllVibes([])) 11 15 }, []) 12 16 17 + useEffect(() => { 18 + const sentinel = sentinelRef.current 19 + if (!sentinel) return 20 + 21 + const observer = new IntersectionObserver( 22 + (entries) => { 23 + if (entries[0].isIntersecting) { 24 + setVisibleCount((c) => Math.min(c + BATCH_SIZE, allVibes.length)) 25 + } 26 + }, 27 + { rootMargin: "400px" } 28 + ) 29 + 30 + observer.observe(sentinel) 31 + return () => observer.disconnect() 32 + }, [allVibes.length]) 33 + 34 + const visible = allVibes.slice(0, visibleCount) 35 + 13 36 return ( 14 37 <div className="vibes-container"> 15 - {vibes.map((vibe, index) => { 38 + {visible.map((vibe, index) => { 16 39 const isVideo = vibe.endsWith(".mp4") || vibe.endsWith(".mov") || vibe.endsWith(".webm") 17 40 return isVideo ? ( 18 - <video key={index} src={vibe} className="vibe-item" controls loop muted /> 41 + <video key={vibe} src={vibe} className="vibe-item" controls loop muted preload="none" /> 19 42 ) : ( 20 - <img key={index} src={vibe} alt="" className="vibe-item" loading="lazy" /> 43 + <img key={vibe} src={vibe} alt="" className="vibe-item" loading="lazy" decoding="async" /> 21 44 ) 22 45 })} 46 + {visibleCount < allVibes.length && <div ref={sentinelRef} style={{ height: 1 }} />} 23 47 </div> 24 48 ) 25 49 }