Don't Debounce Lazy Loading: Benchmark Loaded Only 4 of 120 Images
Fast-scrolling users on pages that rely on scroll + debounce lazy loading see unfilled images until they stop scrolling, a silent failure in long lists, anchor jumps, and End-key jumps. Native loading=lazy is more aggressive than most handwritten IO and costs one line, so the benchmark gives a data-backed reason to switch or at least widen rootMargin to 1000px.
A systematic comparison of nine lazy loading approaches on 120 images (67KB each, local server, cache disabled) produced a clear winner and one catastrophic loser. Scroll + debounce 250ms, a pattern still common in tutorials, loaded only 4 of 120 images when the page scrolled to the bottom in one continuous motion. The median image-ready delay was 615ms, and the mechanism is the problem: every scroll event resets the timer, so the load check never runs until scrolling stops. When it finally does, only the last few images are in view. The same setup with scroll + requestAnimationFrame had a 14ms delay, a 44x difference.
Native loading=lazy preloads at roughly 1250px and got images ready 851ms earlier on average. Handwritten IntersectionObserver code with the widely taught rootMargin of 200px only managed 119ms of early readiness; matching native required rootMargin of 1000px, which reached 685ms early. The benchmark also debunked two myths: fetchpriority=low had zero effect on lazy-loaded images, and bare scroll listeners didn't cause jank (median frame time stayed at 17ms with zero long tasks) but were slower than IO. Lazy loading's real payoff is bandwidth: it cuts initial requests from 120 to 1-7, not frame rate.
Debounce is fundamentally mismatched with scroll events because it resets on every trigger, so any continuous scroll postpones the load check indefinitely. This is a mechanism flaw, not a tuning issue.
The browser's native lazy loading is more aggressive than what most handwritten IO tutorials recommend (200px rootMargin). Following common advice makes users wait half a second longer, so the 'handwritten is better' assumption doesn't hold for typical configurations.
The perception that bare scroll listeners cause jank is overstated at this scale; the real cost is latency in ready time, not frame drops. Performance profiling should separate these two concerns.
Lazy loading's primary win is network savings, not rendering performance. Optimizing for frame rate with a scroll listener misses the point.
The discussion centers on whether handwritten scroll-based lazy loading can rival native loading=lazy. Debounce causes rapid scrolling to load only a few images, while throttle keeps checks regular. Native lazy's prefetch distance varies with network speed, so custom implementations must adjust rootMargin accordingly. A call to benchmark round-trip scrolling and a version upgrade issue also surface.
Can add the loading completion rate for back-and-forth scrolling.
For the version that only loads a subset of images, I locked it at 3.2; after upgrading to 4.0, some behavior changed. Have any of you encountered this?
Switching debounce to throttle makes it work: during scrolling, at least one check every 250ms, so it won't get pushed back indefinitely. Also, Chrome's prefetch distance for native lazy loading depends on network speed: 4G 1250px, 3G or below 2500px. Measurements from a local server reflect the fast-network tier; to match native under weak networks with a custom IO, rootMargin needs to be increased further.