Why Mobile Performance Is Different
Mobile devices face fundamentally different constraints than desktops: slower CPUs (4-6x slower than desktop), limited and variable network connections, smaller memory, and battery constraints. Over 60% of web traffic is mobile, yet most developers test primarily on powerful desktop machines. Optimizing for mobile means designing for the weakest link in the chain.
Mobile vs Desktop Constraints
- CPU: Mobile CPUs are 4-6x slower. JavaScript parsing and execution take proportionally longer.
- Network: Mobile connections are variable, high-latency (50-300ms RTT), and bandwidth-limited.
- Memory: Limited RAM causes aggressive tab eviction and garbage collection pauses.
- Battery: Heavy computation and network requests drain battery, degrading the device over time.
- Thermal throttling: Sustained CPU usage causes the device to heat up and throttle performance.
Reducing JavaScript Execution Time
// JavaScript is the most expensive resource on mobile
// Every KB of JS costs more on mobile due to slower CPUs
// 1. Break up long tasks to prevent jank
async function processLargeList<T>(
items: T[],
process: (item: T) => void,
chunkSize: number = 25 // Smaller chunks for mobile
): Promise<void> {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach(process);
// Yield to browser between chunks
await new Promise(resolve => requestAnimationFrame(resolve));
}
}
// 2. Use requestIdleCallback for non-urgent work
function scheduleNonUrgent(work: () => void): void {
if ('requestIdleCallback' in window) {
requestIdleCallback(work, { timeout: 5000 });
} else {
setTimeout(work, 100);
}
}
// 3. Web Workers for CPU-intensive operations
// worker.ts
self.onmessage = (e: MessageEvent) => {
const { type, payload } = e.data;
switch (type) {
case 'sort':
const sorted = payload.sort((a: number, b: number) => a - b);
self.postMessage({ type: 'sorted', payload: sorted });
break;
case 'search':
const results = payload.items.filter((item: any) =>
item.name.toLowerCase().includes(payload.query.toLowerCase())
);
self.postMessage({ type: 'searchResults', payload: results });
break;
}
};
// Using the worker from React
function useWorker() {
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = new Worker(new URL('./worker.ts', import.meta.url));
return () => workerRef.current?.terminate();
}, []);
const sortData = useCallback((data: number[]): Promise<number[]> => {
return new Promise((resolve) => {
const worker = workerRef.current!;
worker.onmessage = (e) => resolve(e.data.payload);
worker.postMessage({ type: 'sort', payload: data });
});
}, []);
return { sortData };
}Touch Performance
// Optimizing touch interactions for better INP
// 1. Use CSS touch-action to eliminate 300ms tap delay
// In your global CSS:
// * { touch-action: manipulation; } /* Removes 300ms delay */
// 2. Passive event listeners for scroll performance
function usePassiveScroll(callback: (scrollY: number) => void) {
useEffect(() => {
const handler = () => callback(window.scrollY);
// Passive listener allows browser to scroll without waiting for JS
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}, [callback]);
}
// 3. Hardware-accelerated animations
// CSS approach — always prefer transform over position/size changes:
// .animated-element {
// will-change: transform;
// transform: translateX(0);
// transition: transform 0.3s ease;
// }
// .animated-element.active {
// transform: translateX(100px);
// }
// 4. Debounce expensive scroll/resize handlers
function useThrottledCallback(
callback: (...args: any[]) => void,
fps: number = 30
): (...args: any[]) => void {
const lastCall = useRef(0);
const frameId = useRef<number>(0);
return useCallback((...args: any[]) => {
const now = performance.now();
const interval = 1000 / fps;
if (now - lastCall.current >= interval) {
lastCall.current = now;
callback(...args);
} else {
cancelAnimationFrame(frameId.current);
frameId.current = requestAnimationFrame(() => {
lastCall.current = performance.now();
callback(...args);
});
}
}, [callback, fps]);
}Network-Aware Loading
// Adapt behavior based on network conditions
function getNetworkInfo() {
const conn = (navigator as any).connection;
return {
effectiveType: conn?.effectiveType || '4g', // 'slow-2g', '2g', '3g', '4g'
downlink: conn?.downlink || 10, // Mbps
rtt: conn?.rtt || 50, // Round trip time in ms
saveData: conn?.saveData || false, // Data saver mode
};
}
// Adaptive image loading
function AdaptiveImage({ src, alt }: { src: string; alt: string }) {
const network = getNetworkInfo();
// On slow connections, load lower quality
const quality = network.effectiveType === '4g' ? 85 :
network.effectiveType === '3g' ? 60 : 40;
// Skip loading non-essential images on 2G or data saver
if ((network.effectiveType === '2g' || network.saveData) && !isPriority) {
return <div className="bg-gray-200 h-48 flex items-center justify-center text-gray-500">
Image hidden to save data
</div>;
}
return <Image src={src} alt={alt} quality={quality} loading="lazy" />;
}
// Adaptive prefetching
function AdaptivePrefetch({ href, children }: { href: string; children: React.ReactNode }) {
const network = getNetworkInfo();
// Only prefetch on fast connections
const shouldPrefetch = network.effectiveType === '4g' && !network.saveData;
return (
<Link href={href} prefetch={shouldPrefetch}>
{children}
</Link>
);
}
// Adaptive video — don't autoplay on slow connections
function AdaptiveVideo({ src }: { src: string }) {
const network = getNetworkInfo();
const isFastConnection = network.effectiveType === '4g' && network.downlink > 5;
return (
<video
src={src}
autoPlay={isFastConnection}
preload={isFastConnection ? 'auto' : 'none'}
muted
loop
playsInline
/>
);
}Mobile Testing Strategy
// Lighthouse configuration for mobile testing
// Test with realistic mobile constraints
const mobileConfig = {
extends: 'lighthouse:default',
settings: {
formFactor: 'mobile',
throttling: {
rttMs: 150, // 150ms round-trip (typical 4G)
throughputKbps: 1638, // 1.6 Mbps download (slow 4G)
cpuSlowdownMultiplier: 4, // 4x CPU slowdown
requestLatencyMs: 0,
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
screenEmulation: {
mobile: true,
width: 412,
height: 823,
deviceScaleFactor: 1.75,
disabled: false,
},
},
};
// k6 mobile-specific load test
// mobile-load-test.js
// import http from 'k6/http';
// export const options = {
// stages: [
// { duration: '2m', target: 100 },
// { duration: '5m', target: 100 },
// { duration: '1m', target: 0 },
// ],
// thresholds: {
// // Mobile-specific thresholds (more lenient)
// http_req_duration: ['p(95)<3000'],
// http_req_failed: ['rate<0.05'],
// },
// };
// export default function () {
// http.get('https://example.com/', {
// headers: {
// 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)',
// },
// });
// }Mobile Performance Checklist
- Minimize JavaScript: Every KB costs 4-6x more on mobile CPUs
- Use CSS touch-action: Eliminate the 300ms tap delay on all interactive elements
- Prefer CSS animations: GPU-accelerated transforms over JavaScript animations
- Adapt to network: Use the Network Information API to adjust behavior
- Test on real devices: Emulators do not replicate thermal throttling or real network conditions
- Offload to workers: Move CPU-intensive work off the main thread with Web Workers
- Use passive listeners: Mark scroll/touch listeners as passive to allow smooth scrolling