TechLead
Lesson 22 of 22
6 min read
Performance Engineering

Mobile Performance

Optimize for mobile devices with constrained CPUs, limited bandwidth, touch responsiveness, and battery awareness

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

Continue Learning