Front-end Engineering Lab
PatternsMobile & PWA

Adaptive Loading

Adjust content quality and features based on network speed and device capabilities.

The Problem

Loading high-quality assets on slow networks creates poor user experience:

  • Users on 2G/3G wait too long for content
  • Mobile data plans get consumed unnecessarily
  • Features that require fast connections fail or timeout
  • One-size-fits-all approach ignores network reality

Solution

type NetworkSpeed = 'slow-2g' | '2g' | '3g' | '4g' | 'unknown';

interface NetworkInfo {
  effectiveType: NetworkSpeed;
  downlink?: number; // Mbps
  rtt?: number; // Round-trip time in ms
  saveData?: boolean; // User enabled data saver
}

interface NetworkInformation {
  effectiveType?: NetworkSpeed;
  downlink?: number;
  rtt?: number;
  saveData?: boolean;
  addEventListener(type: string, listener: EventListener): void;
  removeEventListener(type: string, listener: EventListener): void;
}

class AdaptiveLoader {
  /**
   * Gets current network information
   */
  static getNetworkInfo(): NetworkInfo {
    const nav = navigator as Navigator & {
      connection?: NetworkInformation;
      mozConnection?: NetworkInformation;
      webkitConnection?: NetworkInformation;
    };
    
    const connection = nav.connection || nav.mozConnection || nav.webkitConnection;

    if (!connection) {
      return { effectiveType: 'unknown' };
    }

    return {
      effectiveType: connection.effectiveType || 'unknown',
      downlink: connection.downlink,
      rtt: connection.rtt,
      saveData: connection.saveData || false
    };
  }

  /**
   * Determines if network is slow
   */
  static isSlowNetwork(): boolean {
    const { effectiveType, saveData } = this.getNetworkInfo();
    
    // User explicitly enabled data saver
    if (saveData) return true;
    
    // 2G networks are considered slow
    return effectiveType === 'slow-2g' || effectiveType === '2g';
  }

  /**
   * Returns appropriate image quality based on network
   */
  static getImageQuality(): 'low' | 'medium' | 'high' {
    const { effectiveType } = this.getNetworkInfo();

    switch (effectiveType) {
      case 'slow-2g':
      case '2g':
        return 'low';
      case '3g':
        return 'medium';
      case '4g':
      default:
        return 'high';
    }
  }

  /**
   * Selects appropriate image source based on network
   */
  static selectImageSource(sources: {
    low: string;
    medium: string;
    high: string;
  }): string {
    const quality = this.getImageQuality();
    return sources[quality];
  }

  /**
   * Decides whether to preload resources
   */
  static shouldPreload(): boolean {
    const { effectiveType, saveData } = this.getNetworkInfo();
    
    if (saveData) return false;
    
    // Only preload on fast networks
    return effectiveType === '4g';
  }

  /**
   * Adjusts video quality based on network
   */
  static getVideoQuality(): '360p' | '720p' | '1080p' {
    const { effectiveType } = this.getNetworkInfo();

    switch (effectiveType) {
      case 'slow-2g':
      case '2g':
        return '360p';
      case '3g':
        return '720p';
      case '4g':
      default:
        return '1080p';
    }
  }

  /**
   * Listens for network changes
   */
  static onNetworkChange(callback: (info: NetworkInfo) => void): () => void {
    const nav = navigator as Navigator & {
      connection?: NetworkInformation;
      mozConnection?: NetworkInformation;
      webkitConnection?: NetworkInformation;
    };
    
    const connection = nav.connection || nav.mozConnection || nav.webkitConnection;

    if (!connection) {
      return () => {}; // No-op cleanup
    }

    const handler = () => {
      callback(this.getNetworkInfo());
    };

    connection.addEventListener('change', handler);

    return () => {
      connection.removeEventListener('change', handler);
    };
  }
}

// Usage: Load images adaptively
function loadAdaptiveImage(imageElement: HTMLImageElement) {
  const sources = {
    low: '/images/photo-360w.jpg',
    medium: '/images/photo-720w.jpg',
    high: '/images/photo-1080w.jpg'
  };

  const selectedSrc = AdaptiveLoader.selectImageSource(sources);
  imageElement.src = selectedSrc;
  
  console.log(`Loading ${AdaptiveLoader.getImageQuality()} quality image`);
}

// Usage: Conditional feature loading
async function loadApp() {
  const isSlowNetwork = AdaptiveLoader.isSlowNetwork();

  if (isSlowNetwork) {
    // Load minimal features only
    console.log('Slow network detected - loading lite version');
    await loadLiteVersion();
  } else {
    // Load full experience
    console.log('Fast network - loading full version');
    await loadFullVersion();
  }
}

async function loadLiteVersion() {
  // Skip heavy animations
  document.body.classList.add('reduced-motion');
  
  // Load essential JS only
  await import('./core.js');
}

async function loadFullVersion() {
  // Load all features
  await Promise.all([
    import('./core.js'),
    import('./animations.js'),
    import('./analytics.js')
  ]);
}

// Usage: React to network changes
function setupNetworkMonitoring() {
  const cleanup = AdaptiveLoader.onNetworkChange((info) => {
    console.log('Network changed:', info.effectiveType);
    
    if (info.effectiveType === '2g' || info.effectiveType === 'slow-2g') {
      // Disable auto-play videos
      document.querySelectorAll('video[autoplay]').forEach((video) => {
        (video as HTMLVideoElement).pause();
      });
    }
  });

  // Cleanup on page unload
  window.addEventListener('beforeunload', cleanup);
}

// Usage: Adaptive API calls
async function fetchData(endpoint: string) {
  const shouldPreload = AdaptiveLoader.shouldPreload();
  
  if (!shouldPreload) {
    // Fetch only when needed (lazy loading)
    return null;
  }
  
  // Preload on fast networks
  const response = await fetch(endpoint);
  return response.json();
}

Network Speed Reference

TypeSpeedUse Case
slow-2gLess than 50 KbpsText only, minimal images
2g~70 KbpsLow-res images, no videos
3g~700 KbpsMedium images, compressed videos
4g10+ MbpsHigh-quality everything

Performance Note

Benefit: Reduces bandwidth usage by 60% on slow networks. Users on 2G get a working experience instead of endless loading. Respects data saver mode to save mobile data costs.

On this page