Front-end Engineering Lab
PatternsMobile & PWA

Mobile Performance Optimization

Optimize for mobile devices with limited resources

Mobile devices have slower CPUs, less memory, and variable networks compared to desktop. Optimizing for mobile is critical—50% of users abandon sites that take >3 seconds to load.

The Mobile Performance Gap

Desktop:
- 8-16 GB RAM
- Fast multi-core CPU
- Stable WiFi
- Large battery

Mobile:
- 2-6 GB RAM
- Slower CPU (esp. mid-range)
- Variable network (2G-5G)
- Battery constraints

JavaScript Optimization

Reduce Bundle Size

// ❌ BAD: Import entire library
import _ from 'lodash';
import moment from 'moment';

// ✅ GOOD: Import only what you need
import debounce from 'lodash/debounce';
import { format } from 'date-fns';

// Or use tree-shaking friendly libraries
import { useState } from 'react'; // Tree-shakes automatically

Code Splitting

// Split by route
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));

export function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Defer Non-Critical JS

<!-- Critical: Load immediately -->
<script src="/app.js"></script>

<!-- Non-critical: Defer -->
<script src="/analytics.js" defer></script>
<script src="/chat-widget.js" defer></script>

<!-- Low priority: Load on idle -->
<script>
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      import('./non-critical.js');
    });
  }
</script>

CSS Optimization

Inline Critical CSS

<!DOCTYPE html>
<html>
<head>
  <!-- Inline critical CSS for above-the-fold content -->
  <style>
    /* Critical styles */
    body { margin: 0; font-family: sans-serif; }
    .header { height: 60px; background: #000; }
    .hero { height: 400px; }
  </style>
  
  <!-- Load full CSS async -->
  <link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">
</head>
</html>

Remove Unused CSS

# Use PurgeCSS
npm install -D @fullhuman/postcss-purgecss

# postcss.config.js
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.{js,jsx,ts,tsx}'],
      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
    }),
  ],
};

Image Optimization

Responsive Images

// components/ResponsiveImage.tsx
interface Props {
  src: string;
  alt: string;
  width: number;
  height: number;
}

export function ResponsiveImage({ src, alt, width, height }: Props) {
  // Generate srcset for different screen sizes
  const srcset = [
    `${src}?w=320 320w`,
    `${src}?w=640 640w`,
    `${src}?w=960 960w`,
    `${src}?w=1280 1280w`,
  ].join(', ');

  return (
    <img
      src={`${src}?w=640`}
      srcSet={srcset}
      sizes="(max-width: 640px) 100vw, 640px"
      alt={alt}
      width={width}
      height={height}
      loading="lazy"
    />
  );
}

Modern Formats

<picture>
  <!-- AVIF: Best compression -->
  <source srcset="/image.avif" type="image/avif" />
  
  <!-- WebP: Good compression -->
  <source srcset="/image.webp" type="image/webp" />
  
  <!-- Fallback: JPEG -->
  <img src="/image.jpg" alt="Description" loading="lazy" />
</picture>

Lazy Loading

// Lazy load images below the fold
export function LazyImage({ src, alt }: Props) {
  const [isVisible, setIsVisible] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' } // Load 100px before visible
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <img
      ref={imgRef}
      src={isVisible ? src : '/placeholder.jpg'}
      alt={alt}
      loading="lazy"
    />
  );
}

Reduce Main Thread Work

Web Workers for Heavy Tasks

// worker.ts
self.addEventListener('message', (e) => {
  const { type, data } = e.data;

  if (type === 'PROCESS_DATA') {
    // Heavy computation off main thread
    const result = processLargeDataset(data);
    self.postMessage({ type: 'RESULT', result });
  }
});

function processLargeDataset(data: any[]) {
  // CPU-intensive work
  return data.map(item => /* ... */);
}
// main.ts
const worker = new Worker('/worker.js');

worker.postMessage({ type: 'PROCESS_DATA', data: largeDataset });

worker.addEventListener('message', (e) => {
  if (e.data.type === 'RESULT') {
    console.log('Processed:', e.data.result);
  }
});

Debounce Expensive Operations

import { debounce } from 'lodash';

// ❌ BAD: Runs on every keystroke
function handleSearch(query: string) {
  fetchSearchResults(query); // Expensive!
}

// ✅ GOOD: Debounced
const handleSearch = debounce((query: string) => {
  fetchSearchResults(query);
}, 300);

Virtual Scrolling for Long Lists

// Use react-window for long lists
import { FixedSizeList } from 'react-window';

export function VirtualList({ items }: Props) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

Network Optimization

Prefetch Critical Resources

<!-- DNS prefetch -->
<link rel="dns-prefetch" href="https://api.example.com" />

<!-- Preconnect (DNS + TCP + TLS) -->
<link rel="preconnect" href="https://cdn.example.com" />

<!-- Prefetch resources -->
<link rel="prefetch" href="/next-page.js" />

<!-- Preload critical resources -->
<link rel="preload" href="/critical.js" as="script" />
<link rel="preload" href="/hero.jpg" as="image" />

Compress Assets

// next.config.js
module.exports = {
  compress: true, // Enable gzip
  
  webpack(config) {
    // Enable compression
    if (process.env.NODE_ENV === 'production') {
      config.plugins.push(
        new CompressionPlugin({
          algorithm: 'brotliCompress', // Better than gzip
          test: /\.(js|css|html|svg)$/,
          threshold: 10240,
        })
      );
    }
    return config;
  },
};

Cache Aggressively

// sw.js - Service Worker caching
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
  '/',
  '/app.js',
  '/app.css',
  '/logo.svg',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

Touch Interactions

Passive Event Listeners

// ❌ BAD: Blocks scrolling
document.addEventListener('touchstart', handleTouch);

// ✅ GOOD: Non-blocking
document.addEventListener('touchstart', handleTouch, { passive: true });

Optimize Touch Targets

/* Minimum 48x48px touch targets */
button {
  min-width: 48px;
  min-height: 48px;
  padding: 12px 16px;
}

/* Adequate spacing between targets */
.button-group button {
  margin: 8px;
}

Battery Optimization

Reduce Animations on Low Battery

export function useLowBattery() {
  const [isLowBattery, setIsLowBattery] = useState(false);

  useEffect(() => {
    const checkBattery = async () => {
      if ('getBattery' in navigator) {
        const battery = await (navigator as any).getBattery();
        
        const updateBatteryStatus = () => {
          setIsLowBattery(battery.level < 0.2 && !battery.charging);
        };

        battery.addEventListener('levelchange', updateBatteryStatus);
        battery.addEventListener('chargingchange', updateBatteryStatus);
        
        updateBatteryStatus();
      }
    };

    checkBattery();
  }, []);

  return isLowBattery;
}

// Use in component
export function AnimatedComponent() {
  const isLowBattery = useLowBattery();

  return (
    <div className={isLowBattery ? 'no-animation' : 'animated'}>
      Content
    </div>
  );
}

Performance Monitoring

// Track mobile performance metrics
export function trackMobilePerformance() {
  if ('PerformanceObserver' in window) {
    // Largest Contentful Paint
    const lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
    });
    lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });

    // First Input Delay
    const fidObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        console.log('FID:', entry.processingStart - entry.startTime);
      });
    });
    fidObserver.observe({ entryTypes: ['first-input'] });

    // Cumulative Layout Shift
    let clsScore = 0;
    const clsObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry: any) => {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
          console.log('CLS:', clsScore);
        }
      });
    });
    clsObserver.observe({ entryTypes: ['layout-shift'] });
  }

  // Device info
  console.log('Device:', {
    memory: (navigator as any).deviceMemory || 'unknown',
    cores: navigator.hardwareConcurrency || 'unknown',
    connection: (navigator as any).connection?.effectiveType || 'unknown',
  });
}

Best Practices Checklist

  • Bundle size < 200 KB (gzipped)
  • Time to Interactive < 3s on 3G
  • Images lazy loaded and optimized
  • Critical CSS inlined
  • JavaScript deferred or async
  • Service Worker for offline
  • Touch targets ≥ 48x48px
  • Passive event listeners
  • Virtual scrolling for lists
  • Compress assets (Brotli/gzip)
  • Prefetch critical resources
  • Monitor Core Web Vitals

Testing on Real Devices

# Chrome Remote Debugging
1. Enable USB debugging on Android
2. Connect device via USB
3. Open chrome://inspect in Chrome
4. Click "Inspect" on your device

# Network throttling
1. DevTools Network tab
2. Select "Slow 3G" or "Fast 3G"
3. Test performance

# Lighthouse on mobile
lighthouse https://example.com --preset=mobile --view

Common Pitfalls

Only testing on desktop: Misses mobile issues
Test on real mid-range devices

Large JavaScript bundles: Slow parsing on mobile
Code split, defer non-critical

Unoptimized images: Slow load, wastes data
Responsive, lazy, modern formats

Blocking scroll: Poor UX
Passive listeners, optimize touch

No offline support: Fails on poor networks
Service worker, caching

Mobile performance is critical—optimize aggressively for the devices most of your users have!

On this page