Mobile Performance Testing and Optimization: A Developer's Guide
A practical Fusebox guide to mobile performance testing and optimization.
Mobile Performance Testing and Optimization: A Developer's Guide
Mobile users are unforgiving—53% abandon sites that take over 3 seconds to load. Yet most developers test on high-end devices with fast WiFi, missing the reality of budget phones on spotty 3G. This guide provides JavaScript tools to test real mobile performance, simulate network conditions, and optimize for the constraints that actually matter.
Why Mobile Performance Is Different
In 2023, an e-commerce giant discovered their mobile conversion rate was 70% lower than desktop. The culprit? Their site consumed 6MB of JavaScript and required 45 seconds of CPU time on budget Android phones. After optimization, they reduced mobile load time by 80% and doubled mobile revenue. Mobile isn't just a smaller screen—it's an entirely different performance profile.
Mobile Performance Analyzer
Comprehensive mobile performance testing in the browser:
// Mobile Performance Analyzer
class MobilePerformanceAnalyzer {
constructor() {
this.metrics = {
device: {},
network: {},
resources: [],
interactions: [],
memory: {}
};
}
// Run comprehensive mobile analysis
async analyzeMobilePerformance() {
const analysis = {
device: this.detectDevice(),
viewport: this.analyzeViewport(),
network: await this.estimateNetworkSpeed(),
cpu: await this.measureCPUPerformance(),
memory: this.analyzeMemoryUsage(),
touch: this.analyzeTouchPerformance(),
resources: await this.analyzeResourceLoading(),
rendering: this.analyzeRenderingPerformance(),
battery: await this.analyzeBatteryImpact(),
recommendations: []
};
analysis.recommendations = this.generateRecommendations(analysis);
return analysis;
}
// Detect device capabilities
detectDevice() {
const ua = navigator.userAgent;
const device = {
type: this.getDeviceType(),
os: this.getOS(),
browser: this.getBrowser(),
// Hardware capabilities
cores: navigator.hardwareConcurrency || 1,
memory: navigator.deviceMemory || 'unknown',
connection: navigator.connection || {},
// Feature support
touchPoints: navigator.maxTouchPoints || 0,
hasTouch: 'ontouchstart' in window,
hasPointer: 'PointerEvent' in window,
// Performance hints
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
saveData: navigator.connection?.saveData || false,
// Screen
pixelRatio: window.devicePixelRatio || 1,
screen: {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight
}
};
// Estimate device tier
device.tier = this.estimateDeviceTier(device);
return device;
}
// Get device type
getDeviceType() {
const ua = navigator.userAgent;
if (/tablet|ipad/i.test(ua)) return 'tablet';
if (/mobile|android|iphone/i.test(ua)) return 'mobile';
return 'desktop';
}
// Get operating system
getOS() {
const ua = navigator.userAgent;
if (/android/i.test(ua)) return 'Android';
if (/iphone|ipad|ipod/i.test(ua)) return 'iOS';
if (/windows phone/i.test(ua)) return 'Windows Phone';
return 'Unknown';
}
// Get browser
getBrowser() {
const ua = navigator.userAgent;
if (/chrome/i.test(ua) && !/edge/i.test(ua)) return 'Chrome';
if (/safari/i.test(ua) && !/chrome/i.test(ua)) return 'Safari';
if (/firefox/i.test(ua)) return 'Firefox';
if (/edge/i.test(ua)) return 'Edge';
return 'Unknown';
}
// Estimate device tier (low, mid, high)
estimateDeviceTier(device) {
let score = 0;
// CPU cores
if (device.cores >= 8) score += 3;
else if (device.cores >= 4) score += 2;
else score += 1;
// RAM
if (device.memory >= 8) score += 3;
else if (device.memory >= 4) score += 2;
else if (device.memory >= 2) score += 1;
// Connection
if (device.connection.effectiveType === '4g') score += 2;
else if (device.connection.effectiveType === '3g') score += 1;
// Pixel ratio (retina displays)
if (device.pixelRatio >= 3) score += 1;
// Determine tier
if (score >= 8) return 'high';
if (score >= 5) return 'mid';
return 'low';
}
// Analyze viewport and responsive behavior
analyzeViewport() {
const viewport = {
width: window.innerWidth,
height: window.innerHeight,
orientation: screen.orientation?.type || 'unknown',
isPortrait: window.innerHeight > window.innerWidth,
// Visual viewport (accounts for keyboard, etc)
visualViewport: {
width: window.visualViewport?.width || window.innerWidth,
height: window.visualViewport?.height || window.innerHeight,
scale: window.visualViewport?.scale || 1
},
// Check for viewport meta tag
hasViewportMeta: !!document.querySelector('meta[name="viewport"]'),
viewportContent: document.querySelector('meta[name="viewport"]')?.content
};
// Test responsive breakpoints
viewport.breakpoints = this.testBreakpoints();
return viewport;
}
// Test responsive breakpoints
testBreakpoints() {
const breakpoints = {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
};
const current = window.innerWidth;
let activeBreakpoint = 'xs';
for (const [name, width] of Object.entries(breakpoints)) {
if (current >= width) activeBreakpoint = name;
}
return {
current: activeBreakpoint,
width: current,
breakpoints
};
}
// Estimate network speed
async estimateNetworkSpeed() {
const connection = navigator.connection || {};
const network = {
effectiveType: connection.effectiveType || 'unknown',
downlink: connection.downlink || 'unknown',
rtt: connection.rtt || 'unknown',
saveData: connection.saveData || false,
type: connection.type || 'unknown'
};
// Measure actual download speed
try {
const testUrl = '/favicon.ico?' + Date.now();
const startTime = performance.now();
const response = await fetch(testUrl);
const blob = await response.blob();
const endTime = performance.now();
const duration = endTime - startTime;
const sizeInBits = blob.size * 8;
network.measuredSpeed = {
bps: sizeInBits / (duration / 1000),
mbps: (sizeInBits / (duration / 1000)) / 1000000,
latency: duration,
testSize: blob.size
};
} catch (e) {
network.measuredSpeed = null;
}
// Classify network quality
network.quality = this.classifyNetworkQuality(network);
return network;
}
// Classify network quality
classifyNetworkQuality(network) {
if (network.saveData) return 'save-data';
const effectiveType = network.effectiveType;
if (effectiveType === 'slow-2g') return 'very-poor';
if (effectiveType === '2g') return 'poor';
if (effectiveType === '3g') return 'moderate';
if (effectiveType === '4g') return 'good';
// Fallback to measured speed
if (network.measuredSpeed) {
const mbps = network.measuredSpeed.mbps;
if (mbps < 0.5) return 'very-poor';
if (mbps < 1) return 'poor';
if (mbps < 5) return 'moderate';
return 'good';
}
return 'unknown';
}
// Measure CPU performance
async measureCPUPerformance() {
const measurements = {
jsExecutionSpeed: 0,
domManipulationSpeed: 0,
calculationSpeed: 0,
renderingSpeed: 0
};
// Test 1: JavaScript execution speed
const jsStart = performance.now();
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.sqrt(i);
}
measurements.jsExecutionSpeed = performance.now() - jsStart;
// Test 2: DOM manipulation speed
const domStart = performance.now();
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Test ${i}`;
fragment.appendChild(div);
}
measurements.domManipulationSpeed = performance.now() - domStart;
// Test 3: Complex calculation
const calcStart = performance.now();
const fibonacci = (n) => n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
fibonacci(30);
measurements.calculationSpeed = performance.now() - calcStart;
// Test 4: Rendering performance
const renderStart = performance.now();
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
measurements.renderingSpeed = performance.now() - renderStart;
resolve();
});
});
});
// Calculate CPU score
const cpuScore = this.calculateCPUScore(measurements);
return {
measurements,
score: cpuScore,
tier: cpuScore > 80 ? 'high' : cpuScore > 50 ? 'mid' : 'low'
};
}
// Calculate CPU score
calculateCPUScore(measurements) {
// Baseline times (ms) for "good" performance
const baselines = {
jsExecutionSpeed: 50,
domManipulationSpeed: 5,
calculationSpeed: 100,
renderingSpeed: 32
};
let score = 100;
for (const [test, time] of Object.entries(measurements)) {
const baseline = baselines[test];
if (time > baseline) {
// Reduce score based on how much slower than baseline
const penalty = ((time - baseline) / baseline) * 20;
score -= Math.min(penalty, 25); // Max 25 points penalty per test
}
}
return Math.max(0, score);
}
// Analyze memory usage
analyzeMemoryUsage() {
const memory = {
available: navigator.deviceMemory || 'unknown',
usage: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
percentUsed: (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100
} : null,
pressure: 'normal' // Would need Memory Pressure API
};
// Estimate memory pressure
if (memory.usage) {
if (memory.usage.percentUsed > 90) memory.pressure = 'critical';
else if (memory.usage.percentUsed > 70) memory.pressure = 'high';
else if (memory.usage.percentUsed > 50) memory.pressure = 'moderate';
}
return memory;
}
// Analyze touch performance
analyzeTouchPerformance() {
const touch = {
supported: 'ontouchstart' in window,
maxTouchPoints: navigator.maxTouchPoints || 0,
// Check for touch event listeners
hasPassiveListeners: false,
hasTouchOptimizations: false,
tapDelay: 300, // Default iOS tap delay
issues: []
};
// Check for passive event listeners
try {
const opts = Object.defineProperty({}, 'passive', {
get: () => { touch.hasPassiveListeners = true; }
});
window.addEventListener('test', null, opts);
window.removeEventListener('test', null, opts);
} catch (e) {}
// Check for touch optimizations
const styles = getComputedStyle(document.body);
if (styles.touchAction && styles.touchAction !== 'auto') {
touch.hasTouchOptimizations = true;
}
// Check for tap delay removal
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport && viewport.content.includes('user-scalable=no')) {
touch.tapDelay = 0;
}
// Identify issues
if (!touch.hasPassiveListeners) {
touch.issues.push({
type: 'passive-listeners',
message: 'Touch listeners not using passive option',
impact: 'Scrolling may feel janky'
});
}
if (touch.tapDelay > 0) {
touch.issues.push({
type: 'tap-delay',
message: '300ms tap delay present',
impact: 'UI feels sluggish'
});
}
return touch;
}
// Analyze resource loading for mobile
async analyzeResourceLoading() {
const resources = performance.getEntriesByType('resource');
const analysis = {
total: resources.length,
totalSize: 0,
byType: {},
critical: [],
nonCritical: [],
opportunities: []
};
// Group by type and analyze
resources.forEach(resource => {
const type = this.getResourceType(resource.name);
if (!analysis.byType[type]) {
analysis.byType[type] = {
count: 0,
size: 0,
avgLoadTime: 0
};
}
analysis.byType[type].count++;
analysis.byType[type].size += resource.transferSize || 0;
analysis.byType[type].avgLoadTime += resource.duration;
analysis.totalSize += resource.transferSize || 0;
// Identify critical resources
if (resource.startTime < 1000) { // First second
analysis.critical.push({
url: resource.name,
type,
size: resource.transferSize,
duration: resource.duration
});
} else {
analysis.nonCritical.push({
url: resource.name,
type,
size: resource.transferSize
});
}
});
// Calculate averages
Object.values(analysis.byType).forEach(type => {
type.avgLoadTime = type.avgLoadTime / type.count;
});
// Identify optimization opportunities
if (analysis.byType.image && analysis.byType.image.size > 1000000) {
analysis.opportunities.push({
type: 'images',
message: 'Images exceed 1MB total',
savings: analysis.byType.image.size * 0.5
});
}
if (analysis.byType.script && analysis.byType.script.size > 500000) {
analysis.opportunities.push({
type: 'javascript',
message: 'JavaScript exceeds 500KB',
savings: analysis.byType.script.size * 0.3
});
}
return analysis;
}
// Get resource type
getResourceType(url) {
if (/\.(jpg|jpeg|png|gif|webp|svg|ico)/i.test(url)) return 'image';
if (/\.(js|mjs)$/i.test(url)) return 'script';
if (/\.(css)$/i.test(url)) return 'style';
if (/\.(woff|woff2|ttf|otf|eot)/i.test(url)) return 'font';
if (/\.(mp4|webm|ogg|mp3|wav)/i.test(url)) return 'media';
return 'other';
}
// Analyze rendering performance
analyzeRenderingPerformance() {
const rendering = {
fps: this.measureFPS(),
paintMetrics: this.getPaintMetrics(),
layoutMetrics: this.getLayoutMetrics(),
animations: this.analyzeAnimations(),
scrollPerformance: this.analyzeScrollPerformance()
};
return rendering;
}
// Measure FPS
measureFPS() {
// This would need to run over time
// Simplified version
let fps = 60;
let lastTime = performance.now();
let frames = 0;
const measure = () => {
frames++;
const currentTime = performance.now();
if (currentTime >= lastTime + 1000) {
fps = Math.round((frames * 1000) / (currentTime - lastTime));
frames = 0;
lastTime = currentTime;
}
};
// Would need to call measure() in RAF loop
return fps;
}
// Get paint metrics
getPaintMetrics() {
const entries = performance.getEntriesByType('paint');
const metrics = {};
entries.forEach(entry => {
metrics[entry.name] = entry.startTime;
});
// Get LCP
const lcpEntries = performance.getEntriesByType('largest-contentful-paint');
if (lcpEntries.length > 0) {
metrics['largest-contentful-paint'] = lcpEntries[lcpEntries.length - 1].startTime;
}
return metrics;
}
// Get layout metrics
getLayoutMetrics() {
const observer = performance.getEntriesByType('layout-shift');
let cls = 0;
observer.forEach(entry => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
return {
cumulativeLayoutShift: cls,
layoutShifts: observer.length
};
}
// Analyze animations
analyzeAnimations() {
const animations = {
cssAnimations: 0,
cssTransitions: 0,
jsAnimations: 0,
willChange: 0,
transform3d: 0
};
// Check all elements
const elements = document.querySelectorAll('*');
elements.forEach(element => {
const styles = getComputedStyle(element);
if (styles.animation && styles.animation !== 'none') {
animations.cssAnimations++;
}
if (styles.transition && styles.transition !== 'none') {
animations.cssTransitions++;
}
if (styles.willChange && styles.willChange !== 'auto') {
animations.willChange++;
}
if (styles.transform && styles.transform.includes('3d')) {
animations.transform3d++;
}
});
return animations;
}
// Analyze scroll performance
analyzeScrollPerformance() {
return {
hasPassiveScroll: this.checkPassiveScroll(),
hasSmoothScroll: getComputedStyle(document.documentElement).scrollBehavior === 'smooth',
hasOverflowScrolling: this.checkOverflowScrolling(),
scrollHeight: document.documentElement.scrollHeight,
viewportHeight: window.innerHeight
};
}
// Check passive scroll listeners
checkPassiveScroll() {
// Would need to check actual event listeners
// This is a simplified check
return true;
}
// Check overflow scrolling
checkOverflowScrolling() {
const elements = document.querySelectorAll('[style*="overflow"]');
let hasWebkitOverflowScrolling = false;
elements.forEach(element => {
const style = getComputedStyle(element);
if (style.webkitOverflowScrolling === 'touch') {
hasWebkitOverflowScrolling = true;
}
});
return hasWebkitOverflowScrolling;
}
// Analyze battery impact
async analyzeBatteryImpact() {
if (!navigator.getBattery) {
return { supported: false };
}
try {
const battery = await navigator.getBattery();
return {
supported: true,
level: battery.level,
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
// Estimate impact based on resource usage
estimatedImpact: this.estimateBatteryImpact()
};
} catch (e) {
return { supported: false };
}
}
// Estimate battery impact
estimateBatteryImpact() {
// Factors that impact battery
const factors = {
animations: document.querySelectorAll('[style*="animation"]').length,
videos: document.querySelectorAll('video').length,
highFrequencyTimers: 0, // Would need to track
wakeLock: 'wakeLock' in navigator,
gps: 'geolocation' in navigator
};
let impact = 'low';
let score = 0;
if (factors.animations > 10) score += 2;
if (factors.videos > 0) score += 3;
if (factors.wakeLock) score += 2;
if (factors.gps) score += 1;
if (score >= 5) impact = 'high';
else if (score >= 3) impact = 'medium';
return impact;
}
// Generate recommendations
generateRecommendations(analysis) {
const recommendations = [];
// Device tier recommendations
if (analysis.device.tier === 'low') {
recommendations.push({
priority: 'critical',
category: 'performance',
title: 'Low-end device detected',
suggestions: [
'Reduce JavaScript bundle size below 100KB',
'Implement aggressive code splitting',
'Use CSS containment for complex layouts',
'Avoid complex animations'
]
});
}
// Network recommendations
if (analysis.network.quality === 'poor' || analysis.network.quality === 'very-poor') {
recommendations.push({
priority: 'high',
category: 'network',
title: 'Poor network conditions',
suggestions: [
'Implement offline-first strategies',
'Use service workers for caching',
'Lazy load all non-critical resources',
'Reduce image quality for slow connections'
],
code: `// Adaptive loading based on network
if (navigator.connection?.effectiveType === '2g' ||
navigator.connection?.saveData) {
// Load low-quality images
img.src = img.dataset.lowSrc;
// Skip non-essential features
return;
}`
});
}
// Memory recommendations
if (analysis.memory.pressure === 'high' || analysis.memory.pressure === 'critical') {
recommendations.push({
priority: 'high',
category: 'memory',
title: 'High memory usage detected',
suggestions: [
'Implement virtual scrolling for long lists',
'Remove DOM nodes when not visible',
'Clear unused variables and event listeners',
'Avoid memory leaks in SPAs'
]
});
}
// Touch performance
if (analysis.touch.issues.length > 0) {
recommendations.push({
priority: 'medium',
category: 'interaction',
title: 'Touch performance issues',
issues: analysis.touch.issues,
code: `// Use passive touch listeners
element.addEventListener('touchstart', handler, { passive: true });
// Remove tap delay
<meta name="viewport" content="width=device-width, initial-scale=1">
// CSS touch optimizations
.scrollable {
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}`
});
}
// Resource loading
if (analysis.resources.totalSize > 2000000) { // 2MB
recommendations.push({
priority: 'critical',
category: 'resources',
title: `Page size is ${(analysis.resources.totalSize / 1024 / 1024).toFixed(1)}MB`,
impact: 'Extremely slow on mobile networks',
suggestions: [
'Reduce image sizes and use modern formats',
'Implement code splitting',
'Remove unused CSS and JavaScript',
'Use resource hints wisely'
]
});
}
// CPU performance
if (analysis.cpu.tier === 'low') {
recommendations.push({
priority: 'high',
category: 'cpu',
title: 'CPU performance is limited',
suggestions: [
'Use CSS transforms instead of JavaScript animations',
'Debounce scroll and resize handlers',
'Use Web Workers for heavy computations',
'Implement virtual scrolling'
],
code: `// Debounce expensive operations
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
// Use CSS for animations
.animate {
transform: translateX(0);
transition: transform 0.3s ease;
}`
});
}
return recommendations;
}
}
// Run mobile performance analysis
const analyzer = new MobilePerformanceAnalyzer();
analyzer.analyzeMobilePerformance().then(analysis => {
console.log('Mobile Performance Analysis:', analysis);
});
Network Condition Simulator
Simulate various mobile network conditions:
// Network Condition Simulator
class NetworkSimulator {
constructor() {
this.conditions = {
'4g': { rtt: 50, downlink: 12, saveData: false },
'3g': { rtt: 100, downlink: 1.5, saveData: false },
'2g': { rtt: 300, downlink: 0.25, saveData: true },
'slow-2g': { rtt: 400, downlink: 0.05, saveData: true },
'offline': { rtt: Infinity, downlink: 0, saveData: true }
};
this.interceptors = new Map();
}
// Simulate network conditions
async simulateNetwork(condition) {
const config = this.conditions[condition];
if (!config) {
throw new Error(`Unknown network condition: ${condition}`);
}
console.log(`Simulating ${condition} network:`, config);
// Override fetch to simulate network
this.overrideFetch(config);
// Simulate resource loading
const results = await this.testResourceLoading(config);
// Restore original fetch
this.restoreFetch();
return results;
}
// Override fetch to add delays
overrideFetch(config) {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const [url, options] = args;
// Simulate RTT
await this.delay(config.rtt);
// Get response
const response = await originalFetch(...args);
const blob = await response.blob();
// Simulate download time
const downloadTime = this.calculateDownloadTime(
blob.size,
config.downlink
);
await this.delay(downloadTime);
// Return new response with blob
return new Response(blob, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
};
this.interceptors.set('fetch', originalFetch);
}
// Restore original fetch
restoreFetch() {
if (this.interceptors.has('fetch')) {
window.fetch = this.interceptors.get('fetch');
this.interceptors.delete('fetch');
}
}
// Calculate download time
calculateDownloadTime(bytes, mbps) {
const bits = bytes * 8;
const bps = mbps * 1000000;
return (bits / bps) * 1000; // Convert to milliseconds
}
// Delay helper
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Test resource loading under conditions
async testResourceLoading(config) {
const resources = [
{ url: '/api/data', size: 50000 }, // 50KB API response
{ url: '/image.jpg', size: 200000 }, // 200KB image
{ url: '/script.js', size: 100000 }, // 100KB script
{ url: '/style.css', size: 30000 } // 30KB stylesheet
];
const results = {
condition: config,
resources: [],
totalTime: 0,
recommendations: []
};
for (const resource of resources) {
const startTime = performance.now();
// Simulate loading
const downloadTime = this.calculateDownloadTime(
resource.size,
config.downlink
);
const totalTime = config.rtt + downloadTime;
await this.delay(totalTime);
const endTime = performance.now();
const actualTime = endTime - startTime;
results.resources.push({
url: resource.url,
size: resource.size,
estimatedTime: totalTime,
actualTime: actualTime,
wouldTimeout: totalTime > 30000 // 30s timeout
});
results.totalTime += actualTime;
}
// Generate recommendations
results.recommendations = this.generateNetworkRecommendations(results);
return results;
}
// Generate network-specific recommendations
generateNetworkRecommendations(results) {
const recommendations = [];
const totalSize = results.resources.reduce((sum, r) => sum + r.size, 0);
if (results.totalTime > 10000) { // 10 seconds
recommendations.push({
type: 'critical-resources',
message: 'Page takes over 10 seconds to load key resources',
solution: 'Inline critical CSS, reduce JavaScript, optimize images'
});
}
const timeouts = results.resources.filter(r => r.wouldTimeout);
if (timeouts.length > 0) {
recommendations.push({
type: 'timeout-risk',
message: `${timeouts.length} resources at risk of timeout`,
resources: timeouts.map(r => r.url),
solution: 'Implement progressive loading and timeouts'
});
}
if (totalSize > 500000 && results.condition.saveData) {
recommendations.push({
type: 'save-data',
message: 'User has Save-Data enabled but page is large',
solution: 'Respect Save-Data header and serve minimal resources',
code: `if (navigator.connection?.saveData) {
// Skip non-essential resources
document.querySelectorAll('img[data-lazy]').forEach(img => {
img.removeAttribute('data-lazy');
});
}`
});
}
return recommendations;
}
}
// Test different network conditions
const simulator = new NetworkSimulator();
// Test 3G network
simulator.simulateNetwork('3g').then(results => {
console.log('3G Network Simulation:', results);
});
Mobile-Specific Optimization Tool
Generate mobile-optimized code:
// Mobile Optimization Generator
class MobileOptimizer {
constructor() {
this.optimizations = [];
}
// Generate comprehensive mobile optimizations
generateOptimizations() {
return {
viewport: this.generateViewportOptimizations(),
touch: this.generateTouchOptimizations(),
performance: this.generatePerformanceOptimizations(),
images: this.generateImageOptimizations(),
fonts: this.generateFontOptimizations(),
offline: this.generateOfflineStrategy(),
adaptive: this.generateAdaptiveLoading()
};
}
// Viewport optimizations
generateViewportOptimizations() {
return {
meta: `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`,
css: `/* Safe area insets for notched devices */
:root {
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
.app-header {
padding-top: calc(1rem + var(--safe-area-inset-top));
}
.app-footer {
padding-bottom: calc(1rem + var(--safe-area-inset-bottom));
}
/* Prevent horizontal scroll */
html, body {
overflow-x: hidden;
max-width: 100%;
}
/* Responsive utilities */
.mobile-only {
display: block;
}
.desktop-only {
display: none;
}
@media (min-width: 768px) {
.mobile-only {
display: none;
}
.desktop-only {
display: block;
}
}`,
javascript: `// Handle viewport changes
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', \`\${vh}px\`);
window.addEventListener('resize', () => {
vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', \`\${vh}px\`);
});
// Handle orientation changes
window.addEventListener('orientationchange', () => {
setTimeout(() => {
window.scrollTo(0, 1);
}, 500);
});`
};
}
// Touch optimizations
generateTouchOptimizations() {
return {
css: `/* Remove tap highlight */
* {
-webkit-tap-highlight-color: transparent;
}
/* Smooth scrolling */
.scrollable {
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
/* Prevent callout on long press */
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
/* Touch-friendly buttons */
.touch-target {
min-height: 44px;
min-width: 44px;
padding: 12px;
touch-action: manipulation;
}
/* Improve touch responsiveness */
.touchable {
touch-action: manipulation;
cursor: pointer;
}`,
javascript: `// Fast click implementation
class FastClick {
constructor(element) {
this.element = element;
this.moved = false;
element.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: true });
element.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: true });
element.addEventListener('touchend', this.onTouchEnd.bind(this));
}
onTouchStart(e) {
this.moved = false;
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
}
onTouchMove(e) {
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
// Moved more than 10px
if (Math.abs(x - this.startX) > 10 || Math.abs(y - this.startY) > 10) {
this.moved = true;
}
}
onTouchEnd(e) {
if (!this.moved) {
e.preventDefault();
// Trigger click
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
this.element.dispatchEvent(clickEvent);
}
}
}
// Apply to all buttons
document.querySelectorAll('button, .btn').forEach(btn => {
new FastClick(btn);
});
// Passive event listeners for scroll performance
document.addEventListener('touchstart', handler, { passive: true });
document.addEventListener('touchmove', handler, { passive: true });
document.addEventListener('wheel', handler, { passive: true });`
};
}
// Performance optimizations
generatePerformanceOptimizations() {
return {
css: `/* Use GPU acceleration wisely */
.gpu-accelerated {
transform: translateZ(0);
will-change: transform;
}
/* Contain layout calculations */
.container {
contain: layout style paint;
}
/* Reduce paint areas */
.static-content {
contain: strict;
}
/* Font loading optimization */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}`,
javascript: `// Intersection Observer for lazy loading
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// Request Idle Callback for non-critical tasks
const deferredTasks = [];
function runDeferredTasks(deadline) {
while (deadline.timeRemaining() > 0 && deferredTasks.length > 0) {
const task = deferredTasks.shift();
task();
}
if (deferredTasks.length > 0) {
requestIdleCallback(runDeferredTasks);
}
}
if ('requestIdleCallback' in window) {
requestIdleCallback(runDeferredTasks);
}
// Debounce expensive operations
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
};
// Throttle scroll events
const throttle = (func, limit) => {
let inThrottle;
return (...args) => {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};
window.addEventListener('scroll', throttle(handleScroll, 100));
window.addEventListener('resize', debounce(handleResize, 300));`
};
}
// Image optimizations
generateImageOptimizations() {
return {
html: `<!-- Responsive images with WebP -->
<picture>
<source
media="(max-width: 640px)"
srcset="image-mobile.webp 1x, image-mobile@2x.webp 2x"
type="image/webp"
>
<source
media="(max-width: 640px)"
srcset="image-mobile.jpg 1x, image-mobile@2x.jpg 2x"
>
<source
srcset="image-desktop.webp"
type="image/webp"
>
<img
src="image-desktop.jpg"
alt="Description"
loading="lazy"
decoding="async"
width="800"
height="600"
>
</picture>`,
javascript: `// Adaptive image loading based on network
class AdaptiveImageLoader {
constructor() {
this.connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
}
getImageSrc(element) {
const sources = {
high: element.dataset.srcHigh,
medium: element.dataset.srcMedium,
low: element.dataset.srcLow
};
// Check save data
if (this.connection?.saveData) {
return sources.low;
}
// Check connection type
const effectiveType = this.connection?.effectiveType;
switch(effectiveType) {
case '4g':
return sources.high;
case '3g':
return sources.medium;
case '2g':
case 'slow-2g':
return sources.low;
default:
// Check device pixel ratio
if (window.devicePixelRatio > 2) {
return sources.high;
}
return sources.medium;
}
}
loadImage(img) {
const src = this.getImageSrc(img);
if (src) {
img.src = src;
}
}
}
const imageLoader = new AdaptiveImageLoader();
document.querySelectorAll('img[data-adaptive]').forEach(img => {
imageLoader.loadImage(img);
});`
};
}
// Font optimizations
generateFontOptimizations() {
return {
css: `/* System font stack for instant rendering */
.system-font {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans",
"Droid Sans", "Helvetica Neue", Arial, sans-serif;
}
/* Variable fonts for smaller file size */
@font-face {
font-family: 'VariableFont';
src: url('font-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
/* Subset fonts for faster loading */
@font-face {
font-family: 'CustomFont';
src: url('font-subset.woff2') format('woff2');
unicode-range: U+0020-007E; /* Basic Latin */
font-display: fallback;
}`,
javascript: `// Font loading strategy
if ('fonts' in document) {
// Modern browsers
document.fonts.load('1rem CustomFont').then(() => {
document.documentElement.classList.add('fonts-loaded');
}).catch(() => {
// Fallback to system fonts
document.documentElement.classList.add('fonts-failed');
});
} else {
// Older browsers - use Font Face Observer or similar
const font = new FontFaceObserver('CustomFont');
font.load().then(() => {
document.documentElement.classList.add('fonts-loaded');
});
}`
};
}
// Offline strategy
generateOfflineStrategy() {
return {
serviceWorker: `// Service Worker for offline support
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/offline.html'
];
// Install event
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Fetch event with network-first strategy
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
// Clone the response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
return caches.match(event.request)
.then(response => {
if (response) {
return response;
}
// Return offline page for navigation requests
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});`,
registration: `// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration);
})
.catch(error => {
console.log('SW registration failed:', error);
});
});
}
// Offline detection
window.addEventListener('online', () => {
document.body.classList.remove('offline');
});
window.addEventListener('offline', () => {
document.body.classList.add('offline');
});`
};
}
// Adaptive loading strategy
generateAdaptiveLoading() {
return `// Adaptive Loading Manager
class AdaptiveLoader {
constructor() {
this.deviceMemory = navigator.deviceMemory || 4;
this.connection = navigator.connection;
this.saveData = this.connection?.saveData || false;
this.cores = navigator.hardwareConcurrency || 4;
}
getDeviceCapabilities() {
let score = 0;
// Memory score
if (this.deviceMemory >= 8) score += 3;
else if (this.deviceMemory >= 4) score += 2;
else score += 1;
// CPU score
if (this.cores >= 8) score += 3;
else if (this.cores >= 4) score += 2;
else score += 1;
// Network score
const effectiveType = this.connection?.effectiveType;
if (effectiveType === '4g') score += 3;
else if (effectiveType === '3g') score += 2;
else score += 1;
return {
score,
tier: score >= 7 ? 'high' : score >= 4 ? 'medium' : 'low',
memory: this.deviceMemory,
cores: this.cores,
network: effectiveType,
saveData: this.saveData
};
}
async loadFeature(feature) {
const capabilities = this.getDeviceCapabilities();
// Don't load heavy features on low-end devices
if (capabilities.tier === 'low' && feature.heavy) {
console.log(\`Skipping \${feature.name} on low-end device\`);
return null;
}
// Use lighter versions when appropriate
if (capabilities.saveData || capabilities.network === '2g') {
if (feature.lite) {
return import(feature.lite);
}
}
// Load the full feature
return import(feature.full);
}
// Example usage
async initializeApp() {
const capabilities = this.getDeviceCapabilities();
// Core features - always load
await import('./core.js');
// Conditional features based on device
if (capabilities.tier !== 'low') {
// Animation library
this.loadFeature({
name: 'animations',
full: './animations-full.js',
lite: './animations-lite.js',
heavy: true
});
// Rich text editor
this.loadFeature({
name: 'editor',
full: './editor-full.js',
lite: './editor-lite.js',
heavy: true
});
}
// Analytics - load in idle time
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./analytics.js');
});
}
}
}
const loader = new AdaptiveLoader();
loader.initializeApp();`;
}
}
// Generate mobile optimizations
const optimizer = new MobileOptimizer();
const optimizations = optimizer.generateOptimizations();
console.log('Mobile Optimizations:', optimizations);
Best Practices for Mobile Performance
- Respect Data Saver: Check navigator.connection.saveData
- Adaptive Loading: Serve different assets based on device capability
- Touch-Friendly: Minimum 44x44px touch targets
- Avoid Jank: Use passive listeners and will-change carefully
- Lazy Everything: Images, fonts, non-critical JavaScript
- Offline First: Service workers for resilient apps
- Battery Conscious: Minimize animations and wake locks
- Test on Real Devices: Emulators miss real-world constraints
- Monitor Field Data: Use RUM to track actual performance
- Progressive Enhancement: Core functionality works everywhere
Common Mobile Performance Mistakes
- Desktop-First Development: Mobile as an afterthought
- Ignoring Network Variability: Testing only on WiFi
- Heavy Frameworks: 200KB+ of JavaScript for simple sites
- No Touch Optimization: 300ms tap delays, non-passive listeners
- Memory Leaks: Not cleaning up in SPAs
- Viewport Issues: Horizontal scroll, text too small
- Battery Drain: Continuous animations, wake locks
- Large Images: Serving desktop images to mobile
- Blocking Resources: Render-blocking CSS/JS
- No Offline Support: White screen when connection drops
Remember: Mobile performance isn't about making a desktop site smaller—it's about building for intermittent connectivity, limited CPU, and battery constraints from the ground up.