Tech/Engineering

How We Cut Dashboard API Traffic 90% with Lazy Loading

Piyush Patidar, Sr. Staff Software Engineer

Enterprise SaaS platforms live and die on dashboard performance. When an administrator opens a multi-tenant control plane to check backup compliance or disaster recovery status, every second of lag is a second of eroded trust, and at scale, lag compounds into something worse.

Earlier this year, one of our engineers was profiling the All Organizations page and noticed something that shouldn’t have been possible: a tenant with 80 organizations was generating close to 800 API calls the moment the page opened. Not on scroll. Not on interaction. Immediately, all at once, before the user had moved the mouse.

The page looked fine. Users weren’t filing tickets. But the numbers told a different story, and at larger tenant sizes, that story was going to get expensive fast.

What the page was actually doing

Each organization card on the All Org page surfaces a real-time snapshot of tenant health: backup compliance status, active workload metrics, disaster recovery readiness. That’s ten or more backend calls per card, every time the page initializes.

The problem wasn’t the data — that information is genuinely useful and belongs there. The problem was the timing. Every card fired its requests simultaneously on page load, including cards nowhere near the visible viewport.

On a standard display, users see two or three organization cards at once. Our system was loading data for all of them regardless. The math gets uncomfortable quickly:

  • 20 organizations → ~200 API calls on load

  • 100 organizations → ~1,000 API calls on load

All concurrent. All hitting the load balancer at the same moment. At smaller tenant sizes, this was invisible. At enterprise scale, it created measurable latency spikes, and the problem scaled linearly with tenant size, which meant it was only going to get worse as customers grew.

The problem: Too much happening on page load

Graphic 1: “The Problem: Too Much Happening on Page Load”

The fix: load data when it’s needed, not before

The core change was conceptually simple: stop fetching data for cards the user can’t see yet, and start fetching it when a card is about to enter the viewport.

We implemented this using IntersectionObserver, a browser-native API that monitors element visibility relative to the viewport. Organization cards now initialize as lightweight placeholders. When a card crosses the visibility threshold, meaning it’s about to appear on screen, the observer triggers, and the card fetches its data.

React.useEffect(() => {

    const element = sentinelRef.current;

    if (!element || isVisible) return;


    const observer = new IntersectionObserver(([entry]) => {

        if (entry.isIntersecting) {

            setIsVisible(true); // Load the card only when it's near the viewport

            observer.disconnect(); // fetch once, then stop watching

        }

    }, { rootMargin: "200px" }); // Start loading 200px before card enters view


    observer.observe(element);

    return () => observer.disconnect(); // Stop watching on page change

}, [isVisible]);


if (!isVisible) {

    return (

        <div ref={sentinelRef}>

            <PlaceholderCard /> {/* Lightweight placeholder with no API calls */}

        </div>

    );

}


// Render the actual card and trigger its backend API calls only when needed

return <OrganizationCard organization={organization} />;

The rootMargin value is worth tuning for your use case. We configured it to 200px, allowing data to start loading before a card enters the viewport. This ensures that, during normal scrolling, users can seamlessly view the next 1–2 cards without encountering loading states.

A few implementation notes worth flagging:

  • disconnect() after first load is intentional. You don’t want the observer re-triggering on scroll. If you need periodic data refresh, handle that separately with a polling interval tied to visible state.

  • Polyfill for older environments. IntersectionObserver has strong modern browser support but may need a polyfill depending on your user base.

  • Clean up on unmount. We wrapped the observer setup in a cleanup function tied to component unmount to avoid memory leaks in our React component tree.

Want to learn more about how Druva handles performance at enterprise scale? 

See a technical overview of our cloud-native architecture

Results

The shift from eager to lazy loading flattened our API traffic curve from linear-with-tenant-size to roughly constant. 

The raw numbers tell the story clearly: 

  • Initial API calls dropped from 523 to ~52.

  • Average load time fell from 7,849ms to 809ms.

And P90, the metric that reflects the worst-case experience for large tenants, dropped from 21,920ms to 1,288ms.¹

Before and after: performance

Graphic 2: “Before vs After” performance dashboard

Metric

Improvement

Initial API call volume

Reduced 85–90%

Average page load time

89.6% faster

P90 load time

94.1% faster

Core Web Vitals (TTFB, LCP, FCP, CLS)

Consistent across all tenant sizes

The P90 result is the number that matters most here. Median performance was already acceptable; the tail end, where large tenants were getting hammered, is where users were actually feeling it. Bringing P90 down by 94.1% means the worst-case experience for our largest customers improved more than the average-case experience did.

Beyond the performance metrics, this optimization directly strengthens Druva’s resilience posture. A control plane that degrades under load is a control plane that administrators stop trusting during the moments that matter: incident response, compliance reviews, recovery operations. Fast, consistent dashboard access isn’t just a UX improvement; it’s part of what makes data protection operationally reliable.

The results: A big win

Graphic 3: The benefits extend beyond faster page loads, including improved scalability, reduced infrastructure pressure, and a smoother user experience.

Why this pattern matters at scale

IntersectionObserver-based lazy loading is well-established for paginated lists and media-heavy feeds. What made this instance worth documenting is that the “content” being loaded was API-backed telemetry data, not images. The same pattern applies anywhere you have a list of cards or rows where each item independently triggers backend calls.

If you’re building something similar and the naive implementation works at small scale, profile it at 5–10x your current data size before you hit production with larger customers. The degradation pattern here was invisible until it wasn’t.

See how Druva maintains consistent recovery performance across global, multi-tenant environments.

Request a technical demo →

 

¹ Performance metrics measured across production multi-tenant environments following the IntersectionObserver rollout. Internal Druva engineering measurement.

Druva Blog: Cloud Technology & Data Protection Articles