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
- Progressive enhancement: Start with basics
- Respect Save Data: Honor user preference
- Graceful degradation: Work on any network
- Clear feedback: Show network status
- Adjust dynamically: React to changes
- Test thoroughly: All network conditions
- Preload smartly: Only on fast connections
- Retry intelligently: Backoff on failures
- Monitor metrics: Track by network type
- 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!