Adaptive Loading: Optimize for User's Device and Network

Medium•

Adaptive loading ensures all users get an optimal experience regardless of their device or network conditions.

Quick Decision Guide

Quick Implementation Guide:

Network detection: Use navigator.connection.effectiveType to detect 4G/3G/2G and serve appropriate content.

Save Data mode: Check navigator.connection.saveData to respect user preference for reduced data usage.

Device capabilities: Use navigator.deviceMemory and navigator.hardwareConcurrency to detect device performance.

Adaptive images: Serve low-res on slow networks, high-res on fast. Use srcset or dynamic loading.

Best Practice: Default to low-quality, progressively enhance for better connections. Always respect save-data preference.

Network Information API

Detecting Connection Type

// Get connection info
const connection = navigator.connection 
  || navigator.mozConnection 
  || navigator.webkitConnection;

if (connection) {
  console.log('Effective type:', connection.effectiveType);
  // Returns: '4g', '3g', '2g', 'slow-2g'
  
  console.log('Downlink speed:', connection.downlink);
  // Returns: Mbps estimate (e.g., 10.5)
  
  console.log('RTT:', connection.rtt);
  // Returns: Round-trip time in ms (e.g., 50)
  
  console.log('Save data:', connection.saveData);
  // Returns: true if user enabled data saver
}

React Hook

function useNetworkStatus() {
  const [networkStatus, setNetworkStatus] = useState({
    effectiveType: '4g',
    downlink: 10,
    saveData: false
  });
  
  useEffect(() => {
    const connection = navigator.connection;
    if (!connection) return;
    
    const updateNetworkStatus = () => {
      setNetworkStatus({
        effectiveType: connection.effectiveType,
        downlink: connection.downlink,
        saveData: connection.saveData
      });
    };
    
    updateNetworkStatus();
    connection.addEventListener('change', updateNetworkStatus);
    
    return () => {
      connection.removeEventListener('change', updateNetworkStatus);
    };
  }, []);
  
  return networkStatus;
}

Usage

function App() {
  const { effectiveType, saveData } = useNetworkStatus();
  
  // Adapt based on network
  const shouldLoadHighQuality = 
    effectiveType === '4g' && !saveData;
  
  return (
    <Image 
      src={shouldLoadHighQuality ? 'high-res.jpg' : 'low-res.jpg'} 
    />
  );
}

Device Capabilities Detection

Memory Detection

// Device RAM (GB)
const memory = navigator.deviceMemory || 4;

console.log(`Device has ${memory}GB RAM`);

// Categorize device
function getDeviceTier() {
  if (memory >= 8) return 'high-end';
  if (memory >= 4) return 'mid-range';
  return 'low-end';
}

const tier = getDeviceTier();

CPU Detection

// Number of logical processors
const cores = navigator.hardwareConcurrency || 2;

console.log(`Device has ${cores} CPU cores`);

// Adjust thread pool
const maxWorkers = Math.max(1, cores - 1);

Device Tier Hook

function useDeviceCapabilities() {
  const memory = navigator.deviceMemory || 4;
  const cores = navigator.hardwareConcurrency || 2;
  
  const tier = useMemo(() => {
    if (memory >= 8 && cores >= 4) return 'high';
    if (memory >= 4 && cores >= 2) return 'medium';
    return 'low';
  }, [memory, cores]);
  
  return {
    memory,
    cores,
    tier,
    isLowEnd: tier === 'low',
    isHighEnd: tier === 'high'
  };
}

Adapt Features

function Dashboard() {
  const { isLowEnd } = useDeviceCapabilities();
  
  return (
    <div>
      <DataTable 
        enableVirtualization={!isLowEnd}
        enableAnimations={!isLowEnd}
      />
      
      {!isLowEnd && <HeavyChart />}
    </div>
  );
}

Adaptive Image Loading

Network-Based Quality

function AdaptiveImage({ src, alt }) {
  const { effectiveType, saveData } = useNetworkStatus();
  
  const getImageQuality = () => {
    if (saveData) return 'low';
    
    switch (effectiveType) {
      case '4g': return 'high';
      case '3g': return 'medium';
      case '2g':
      case 'slow-2g':
      default:
        return 'low';
    }
  };
  
  const quality = getImageQuality();
  const imageSrc = `${src}?quality=${quality}`;
  
  return <img src={imageSrc} alt={alt} loading="lazy" />;
}

Progressive Image Loading

function ProgressiveImage({ lowRes, highRes, alt }) {
  const [currentSrc, setCurrentSrc] = useState(lowRes);
  const { effectiveType } = useNetworkStatus();
  
  useEffect(() => {
    // Only load high-res on good connection
    if (effectiveType === '4g') {
      const img = new Image();
      img.src = highRes;
      img.onload = () => setCurrentSrc(highRes);
    }
  }, [effectiveType, highRes]);
  
  return (
    <img 
      src={currentSrc} 
      alt={alt}
      style={{ 
        filter: currentSrc === lowRes ? 'blur(5px)' : 'none',
        transition: 'filter 0.3s'
      }}
    />
  );
}

Responsive Images with Network Hints

function ResponsiveImage() {
  const { effectiveType } = useNetworkStatus();
  
  // Skip high DPI on slow networks
  const multiplier = effectiveType === '4g' ? 2 : 1;
  
  return (
    <img
      srcSet={`
        image-400w.jpg ${400 * multiplier}w,
        image-800w.jpg ${800 * multiplier}w,
        image-1200w.jpg ${1200 * multiplier}w
      `}
      sizes="(max-width: 600px) 400px, 800px"
      src="image-800w.jpg"
      alt="Responsive image"
    />
  );
}

Adaptive Code Splitting

Load Based on Connection

function AdaptiveComponent() {
  const { effectiveType } = useNetworkStatus();
  const { isLowEnd } = useDeviceCapabilities();
  
  // Only load heavy features on good devices + fast network
  if (effectiveType === '4g' && !isLowEnd) {
    const HeavyFeature = lazy(() => import('./HeavyFeature'));
    return (
      <Suspense fallback={<Loading />}>
        <HeavyFeature />
      </Suspense>
    );
  }
  
  // Lightweight alternative
  return <LightweightFeature />;
}

Adaptive Prefetching

function useAdaptivePrefetch() {
  const { effectiveType, saveData } = useNetworkStatus();
  
  const shouldPrefetch = useMemo(() => {
    // Don't prefetch on slow networks or save-data mode
    return effectiveType === '4g' && !saveData;
  }, [effectiveType, saveData]);
  
  const prefetchRoute = useCallback((route) => {
    if (shouldPrefetch) {
      import('./pages/' + route);
    }
  }, [shouldPrefetch]);
  
  return prefetchRoute;
}

// Usage
function Navigation() {
  const prefetch = useAdaptivePrefetch();
  
  return (
    <Link 
      to="/dashboard" 
      onMouseEnter={() => prefetch('dashboard')}
    >
      Dashboard
    </Link>
  );
}

Adaptive Bundle Loading

async function loadAppropriateBundle() {
  const { memory, cores } = await getDeviceCapabilities();
  
  if (memory >= 8 && cores >= 4) {
    // High-end: Load full features
    return import('./bundles/full');
  } else if (memory >= 4) {
    // Mid-range: Load core features
    return import('./bundles/core');
  } else {
    // Low-end: Load minimal features
    return import('./bundles/minimal');
  }
}

Save Data Mode

Respecting User Preference

function useSaveData() {
  const [saveData, setSaveData] = useState(false);
  
  useEffect(() => {
    const connection = navigator.connection;
    if (!connection) return;
    
    setSaveData(connection.saveData);
    
    const handleChange = () => {
      setSaveData(connection.saveData);
    };
    
    connection.addEventListener('change', handleChange);
    return () => {
      connection.removeEventListener('change', handleChange);
    };
  }, []);
  
  return saveData;
}

Adapt Content

function VideoPlayer({ videoUrl }) {
  const saveData = useSaveData();
  
  if (saveData) {
    // Show thumbnail with play button
    return (
      <div onClick={playVideo}>
        <img src="thumbnail.jpg" alt="Video thumbnail" />
        <PlayIcon />
        <p>Tap to play (Save Data is on)</p>
      </div>
    );
  }
  
  // Auto-play on good connection
  return <video src={videoUrl} autoPlay />;
}

Server-Side Hints

// Express middleware
app.use((req, res, next) => {
  const saveData = req.headers['save-data'] === 'on';
  req.saveData = saveData;
  next();
});

// Serve appropriate images
app.get('/image', (req, res) => {
  if (req.saveData) {
    res.sendFile('image-low-quality.jpg');
  } else {
    res.sendFile('image-high-quality.jpg');
  }
});

Best Practices

Progressive Enhancement

// Start with baseline experience
function App() {
  const [experience, setExperience] = useState('baseline');
  const { effectiveType } = useNetworkStatus();
  const { tier } = useDeviceCapabilities();
  
  useEffect(() => {
    // Progressively enhance based on capabilities
    if (effectiveType === '4g' && tier === 'high') {
      setExperience('premium');
    } else if (effectiveType === '3g' && tier === 'medium') {
      setExperience('standard');
    }
    // else stay at 'baseline'
  }, [effectiveType, tier]);
  
  return (
    <div>
      {experience === 'premium' && <PremiumFeatures />}
      {experience === 'standard' && <StandardFeatures />}
      <BaselineFeatures />
    </div>
  );
}

Adaptive Loading Strategy

class AdaptiveLoader {
  constructor() {
    this.connection = navigator.connection;
    this.memory = navigator.deviceMemory || 4;
    this.cores = navigator.hardwareConcurrency || 2;
  }
  
  shouldLoadFeature(feature) {
    const { effectiveType, saveData } = this.connection;
    
    // Critical features: always load
    if (feature.critical) return true;
    
    // Respect save-data mode
    if (saveData && !feature.essential) return false;
    
    // Check network
    if (effectiveType === 'slow-2g' || effectiveType === '2g') {
      return feature.essential;
    }
    
    // Check device capability
    if (this.memory < 4 && feature.memoryIntensive) {
      return false;
    }
    
    return true;
  }
  
  getImageQuality() {
    const { effectiveType, saveData } = this.connection;
    
    if (saveData) return 0.3;
    
    const qualityMap = {
      '4g': 0.9,
      '3g': 0.6,
      '2g': 0.3,
      'slow-2g': 0.2
    };
    
    return qualityMap[effectiveType] || 0.5;
  }
}

Testing

// Chrome DevTools: Throttle network
// Settings → Network → Add custom profile

// Simulate low memory
Object.defineProperty(navigator, 'deviceMemory', {
  value: 2,
  writable: false
});

// Simulate save-data
Object.defineProperty(navigator.connection, 'saveData', {
  value: true,
  writable: false
});