Adaptive Loading: Optimize for User's Device and Network

Medium•

Adaptive loading uses imperfect browser signals and product context to reduce unnecessary work. Strong implementations remain correct when capability APIs are missing.

Quick Decision Guide

Quick Implementation Guide:

Network hints: navigator.connection can expose effective connection type and Save-Data, but support is limited. Treat it as a hint, not a source of truth.

Save Data mode: When available, respect navigator.connection.saveData and avoid non-essential transfers.

Device capability hints: navigator.deviceMemory and navigator.hardwareConcurrency can inform coarse decisions, but they are not precise benchmarks and may be unavailable.

Adaptive images: Prefer standards-first responsive images (srcset, sizes, modern formats), then layer in runtime hints where useful.

Best Practice: Start with a solid baseline, progressively enhance, and measure real outcomes.

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). Limited browser support; treat as a hint.
const memory = navigator.deviceMemory || 4;

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

// Categorize device coarsely, not as a benchmark.
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. Useful as a coarse signal only.
const cores = navigator.hardwareConcurrency || 2;

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

// Adjust thread pool conservatively
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={true}
        enableAnimations={!isLowEnd}
      />
      
      {!isLowEnd && <HeavyChart />}
    </div>
  );
}

Virtualization is often more valuable on constrained devices, not less. The trade-off is implementation complexity and dynamic-height correctness, so choose it based on list size and interaction requirements rather than device tier alone.

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 || null;
    this.memory = navigator.deviceMemory || 4;
    this.cores = navigator.hardwareConcurrency || 2;
  }
  
  shouldLoadFeature(feature) {
    // Critical features should not depend on optional browser hints.
    if (feature.critical) return true;

    const connection = this.connection;
    if (!connection) {
      return feature.essential !== false && !feature.memoryIntensive;
    }

    const { effectiveType, saveData } = connection;
    
    // 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 connection = this.connection;
    if (!connection) return 0.6;

    const { effectiveType, saveData } = 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 only in controlled tests
Object.defineProperty(navigator, 'deviceMemory', {
  value: 2,
  configurable: true
});

// Simulate save-data only when navigator.connection exists
if (navigator.connection) {
  Object.defineProperty(navigator.connection, 'saveData', {
    value: true,
    configurable: true
  });
}