Front-end Engineering Lab
PatternsMobile & PWA

Network-Aware Components

Adapt UI and behavior based on network conditions

Network-Aware Components adapt to connection quality automatically—reducing data on slow networks, preloading on fast networks, and working offline. Used by YouTube, Netflix, and Google Maps.

Detecting Network Status

Network Information API

// utils/network.ts
export interface NetworkInfo {
  online: boolean;
  effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined;
  downlink: number; // Mbps
  rtt: number; // ms (round-trip time)
  saveData: boolean;
}

export function getNetworkInfo(): NetworkInfo {
  const connection = (navigator as any).connection || 
                     (navigator as any).mozConnection || 
                     (navigator as any).webkitConnection;

  return {
    online: navigator.onLine,
    effectiveType: connection?.effectiveType,
    downlink: connection?.downlink || 0,
    rtt: connection?.rtt || 0,
    saveData: connection?.saveData || false,
  };
}

export function isSlowNetwork(): boolean {
  const { effectiveType, saveData } = getNetworkInfo();
  return saveData || effectiveType === 'slow-2g' || effectiveType === '2g';
}

export function isFastNetwork(): boolean {
  const { effectiveType } = getNetworkInfo();
  return effectiveType === '4g';
}

export function isOffline(): boolean {
  return !navigator.onLine;
}

React Hook

// hooks/useNetworkInfo.ts
import { useState, useEffect } from 'react';
import { getNetworkInfo, type NetworkInfo } from '@/utils/network';

export function useNetworkInfo() {
  const [networkInfo, setNetworkInfo] = useState<NetworkInfo>(getNetworkInfo());

  useEffect(() => {
    const updateNetworkInfo = () => {
      setNetworkInfo(getNetworkInfo());
    };

    // Listen for online/offline events
    window.addEventListener('online', updateNetworkInfo);
    window.addEventListener('offline', updateNetworkInfo);

    // Listen for connection changes
    const connection = (navigator as any).connection;
    connection?.addEventListener('change', updateNetworkInfo);

    return () => {
      window.removeEventListener('online', updateNetworkInfo);
      window.removeEventListener('offline', updateNetworkInfo);
      connection?.removeEventListener('change', updateNetworkInfo);
    };
  }, []);

  return networkInfo;
}

// Simplified hooks
export function useOnlineStatus() {
  const { online } = useNetworkInfo();
  return online;
}

export function useSlowNetwork() {
  const { effectiveType, saveData } = useNetworkInfo();
  return saveData || effectiveType === 'slow-2g' || effectiveType === '2g';
}

Adaptive Image Loading

// components/AdaptiveImage.tsx
import { useState, useEffect } from 'react';
import { useNetworkInfo } from '@/hooks/useNetworkInfo';

interface Props {
  src: string;
  alt: string;
  lowQualitySrc?: string;
  placeholder?: string;
}

export function AdaptiveImage({ src, alt, lowQualitySrc, placeholder }: Props) {
  const { effectiveType, saveData } = useNetworkInfo();
  const [imageSrc, setImageSrc] = useState(placeholder || '');
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    // Determine which image to load
    let imageToLoad = src;

    if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') {
      // Use low quality on slow connections
      imageToLoad = lowQualitySrc || src;
    }

    // Load image
    const img = new Image();
    img.src = imageToLoad;
    img.onload = () => {
      setImageSrc(imageToLoad);
      setLoaded(true);
    };
  }, [src, lowQualitySrc, effectiveType, saveData]);

  return (
    <img
      src={imageSrc}
      alt={alt}
      className={loaded ? 'loaded' : 'loading'}
      style={{
        filter: loaded ? 'none' : 'blur(10px)',
        transition: 'filter 0.3s',
      }}
    />
  );
}

Adaptive Video Quality

// components/AdaptiveVideo.tsx
import { useEffect, useRef } from 'react';
import { useNetworkInfo } from '@/hooks/useNetworkInfo';

interface Props {
  sources: {
    quality: '360p' | '720p' | '1080p';
    src: string;
    bitrate: number; // kbps
  }[];
}

export function AdaptiveVideo({ sources }: Props) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const { downlink, effectiveType } = useNetworkInfo();

  useEffect(() => {
    if (!videoRef.current) return;

    // Select quality based on network
    let selectedSource = sources[0]; // Default to lowest

    if (effectiveType === '4g' && downlink > 5) {
      // Fast network: highest quality
      selectedSource = sources[sources.length - 1];
    } else if (effectiveType === '3g' || (downlink > 1.5 && downlink <= 5)) {
      // Medium network: 720p
      selectedSource = sources.find(s => s.quality === '720p') || sources[1];
    }

    // Update video source
    videoRef.current.src = selectedSource.src;
  }, [sources, downlink, effectiveType]);

  return (
    <video
      ref={videoRef}
      controls
      preload="metadata"
      style={{ width: '100%', maxWidth: '800px' }}
    />
  );
}

Data Saver Mode

// components/DataSaverNotice.tsx
import { useNetworkInfo } from '@/hooks/useNetworkInfo';

export function DataSaverNotice({ children }: Props) {
  const { saveData, effectiveType } = useNetworkInfo();

  if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') {
    return (
      <div className="data-saver-notice">
        <p>
          📶 Data Saver Mode is on. Some features are limited to save data.
        </p>
        <button onClick={() => disableDataSaver()}>
          Load Full Experience
        </button>
      </div>
    );
  }

  return <>{children}</>;
}

Adaptive Preloading

// utils/adaptive-preload.ts
import { getNetworkInfo } from './network';

export function shouldPreload(): boolean {
  const { effectiveType, saveData, online } = getNetworkInfo();
  
  if (!online || saveData) return false;
  
  // Only preload on fast networks
  return effectiveType === '4g';
}

export async function preloadResources(urls: string[]) {
  if (!shouldPreload()) {
    console.log('Skipping preload on slow network');
    return;
  }

  urls.forEach((url) => {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = url;
    document.head.appendChild(link);
  });
}

// Preload next page on hover (only if fast network)
export function preloadOnHover(url: string) {
  if (!shouldPreload()) return;

  return (e: React.MouseEvent) => {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = url;
    document.head.appendChild(link);
  };
}

Adaptive Polling

// hooks/useAdaptivePolling.ts
import { useEffect, useState } from 'react';
import { useNetworkInfo } from './useNetworkInfo';

export function useAdaptivePolling(fetchFn: () => Promise<any>) {
  const [data, setData] = useState<any>(null);
  const { effectiveType, saveData, online } = useNetworkInfo();

  useEffect(() => {
    if (!online) return;

    // Adjust polling interval based on network
    let interval = 60000; // 1 minute (slow)

    if (!saveData) {
      if (effectiveType === '4g') {
        interval = 5000; // 5 seconds (fast)
      } else if (effectiveType === '3g') {
        interval = 15000; // 15 seconds (medium)
      }
    }

    const poll = async () => {
      try {
        const result = await fetchFn();
        setData(result);
      } catch (error) {
        console.error('Polling failed:', error);
      }
    };

    poll(); // Initial fetch
    const intervalId = setInterval(poll, interval);

    return () => clearInterval(intervalId);
  }, [fetchFn, effectiveType, saveData, online]);

  return data;
}

Network Status Banner

// components/NetworkBanner.tsx
import { useNetworkInfo } from '@/hooks/useNetworkInfo';

export function NetworkBanner() {
  const { online, effectiveType, saveData } = useNetworkInfo();

  if (!online) {
    return (
      <div className="network-banner offline">
        📡 You're offline. Some features are unavailable.
      </div>
    );
  }

  if (saveData) {
    return (
      <div className="network-banner data-saver">
        💾 Data Saver is on. Images and videos are limited.
      </div>
    );
  }

  if (effectiveType === 'slow-2g' || effectiveType === '2g') {
    return (
      <div className="network-banner slow">
        🐌 Slow connection detected. Using lite version.
      </div>
    );
  }

  return null;
}

Adaptive Component Loading

// components/AdaptiveContent.tsx
import { lazy, Suspense } from 'react';
import { useSlowNetwork } from '@/hooks/useNetworkInfo';

const HeavyComponent = lazy(() => import('./HeavyComponent'));
const LiteComponent = lazy(() => import('./LiteComponent'));

export function AdaptiveContent() {
  const isSlowNetwork = useSlowNetwork();

  return (
    <Suspense fallback={<Loading />}>
      {isSlowNetwork ? <LiteComponent /> : <HeavyComponent />}
    </Suspense>
  );
}

Retry with Backoff

// utils/retry.ts
import { getNetworkInfo } from './network';

export async function fetchWithRetry<T>(
  url: string,
  options: RequestInit = {},
  maxRetries = 3
): Promise<T> {
  const { effectiveType } = getNetworkInfo();
  
  // Adjust timeout based on network
  let timeout = 5000; // Default
  if (effectiveType === '2g' || effectiveType === 'slow-2g') {
    timeout = 15000; // Longer timeout for slow networks
  }

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), timeout);

      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      
      return await response.json();
    } catch (error) {
      console.error(`Attempt ${attempt + 1} failed:`, error);

      if (attempt === maxRetries) throw error;

      // Exponential backoff
      const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error('Max retries exceeded');
}

Progressive Enhancement Example

// pages/Feed.tsx
import { useState, useEffect } from 'react';
import { useNetworkInfo } from '@/hooks/useNetworkInfo';
import { AdaptiveImage } from '@/components/AdaptiveImage';

export function Feed() {
  const [posts, setPosts] = useState([]);
  const { effectiveType, saveData } = useNetworkInfo();

  // Adjust page size based on network
  const pageSize = 
    saveData || effectiveType === '2g' ? 5 :
    effectiveType === '3g' ? 10 :
    20;

  useEffect(() => {
    fetch(`/api/posts?limit=${pageSize}`).then(/* ... */);
  }, [pageSize]);

  return (
    <div className="feed">
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          
          {/* Only load images on decent connections */}
          {effectiveType !== 'slow-2g' && (
            <AdaptiveImage
              src={post.image}
              lowQualitySrc={post.thumbnail}
              alt={post.title}
            />
          )}
          
          <p>{post.excerpt}</p>
          
          {/* Simplify UI on slow networks */}
          {effectiveType !== '2g' && (
            <div className="post-actions">
              <button>Like</button>
              <button>Comment</button>
              <button>Share</button>
            </div>
          )}
        </article>
      ))}
    </div>
  );
}

Testing Network Conditions

Chrome DevTools

1. Open DevTools
2. Network tab
3. Throttling dropdown
4. Select "Slow 3G", "Fast 3G", or "Offline"

Programmatic Testing

// Test different network conditions
describe('Network-Aware Component', () => {
  it('loads low quality on slow network', async () => {
    // Mock slow network
    Object.defineProperty(navigator, 'connection', {
      value: { effectiveType: '2g', saveData: false },
      writable: true,
    });

    render(<AdaptiveImage src="/high.jpg" lowQualitySrc="/low.jpg" />);
    
    // Should load low quality
    await waitFor(() => {
      expect(screen.getByRole('img')).toHaveAttribute('src', '/low.jpg');
    });
  });

  it('loads high quality on fast network', async () => {
    Object.defineProperty(navigator, 'connection', {
      value: { effectiveType: '4g', saveData: false },
    });

    render(<AdaptiveImage src="/high.jpg" lowQualitySrc="/low.jpg" />);
    
    // Should load high quality
    await waitFor(() => {
      expect(screen.getByRole('img')).toHaveAttribute('src', '/high.jpg');
    });
  });
});

Best Practices

  1. Progressive enhancement: Start with basics
  2. Respect Save Data: Honor user preference
  3. Graceful degradation: Work on any network
  4. Clear feedback: Show network status
  5. Adjust dynamically: React to changes
  6. Test thoroughly: All network conditions
  7. Preload smartly: Only on fast connections
  8. Retry intelligently: Backoff on failures
  9. Monitor metrics: Track by network type
  10. User control: Allow manual overrides

Common Pitfalls

Assuming fast network: Breaks on slow connections
Adapt to actual conditions

Ignoring Save Data: Wastes user's data
Respect saveData flag

No offline support: App unusable
Cache critical resources

Same experience everywhere: Poor UX
Optimize for each network type

No feedback: Users don't understand behavior
Show clear network status

Network-aware components respect users' connections—adapt quality and behavior for the best experience on any network!

On this page