Tutorial · Mobile · Performance · WebGL
📅 July 2026 ⏱ ≈ 25 min 🎯 Intermediate

Mobile WebGL Optimization: Steps & Pitfalls

A simulation that hits 60fps on a desktop RTX card can drop to single digits — or thermal-throttle itself into a slideshow after two minutes — on a mid-range phone. Mobile GPUs aren't smaller desktop GPUs; they're a genuinely different architecture with different bottlenecks. This tutorial covers the concrete steps that actually move the needle, and the pitfalls that only show up once real users open your simulation on real phones.

1. A different regime, not a smaller desktop GPU

Nearly every mobile GPU (Apple's, Arm Mali, Qualcomm Adreno, Imagination PowerVR) uses tile-based deferred rendering (TBDR): the screen is split into small tiles, and each tile's geometry and shading is resolved entirely in fast on-chip memory before writing to main memory once. This is why some desktop-honed habits actively hurt mobile performance:

Consequence for shader design: anything that benefits from "render to a texture, then sample it in another pass" (bloom, blur, most post-processing) is real work on TBDR hardware, not close-to-free — budget render targets and passes deliberately rather than stacking effects the way you might on desktop.

2. Cap devicePixelRatio

A modern phone reports devicePixelRatio of 2.6-3+. Rendering a WebGL canvas at that full ratio means every fragment shader runs roughly 9x as many times as at 1x — for sharpness improvements that are barely perceptible on a small screen held at arm's length.

const pixelRatio = Math.min(window.devicePixelRatio, 1.5); // 1.5-2 is a good mobile default
renderer.setPixelRatio(pixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

For fill-rate-heavy simulations (particle systems, fluid sims, anything with large full-screen shader passes), consider going further: render internally at a fraction of the CSS size and let the canvas upscale via CSS, which costs almost nothing compared to shading every pixel at native resolution.

// Internal render resolution at 0.75x, upscaled by the browser via CSS
const renderScale = 0.75;
renderer.setSize(
  window.innerWidth * renderScale,
  window.innerHeight * renderScale,
  false // false = don't touch the CSS size, only the drawing buffer
);
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';

3. Shader precision: avoid highp overflow

GLSL ES precision qualifiers (lowp, mediump, highp) aren't just suggestions on mobile — many mobile GPUs execute mediump fragment shader math on genuinely narrower hardware paths than highp, and some older mobile GPUs don't support fragment-shader highp at all (the spec allows falling back to mediump silently).

Qualifier Typical range Use for
lowp ~256 distinct values, ±2 Normalised colours, simple 0-1 factors
mediump ~16-bit float range UVs, most lighting math, default fragment precision on many GPUs
highp ~32-bit float range World-space positions far from origin, precise time accumulation

The most common silent mobile-only bug: a simulation that accumulates elapsed time in a mediump (or fallback-from-highp) uniform starts showing visible jitter or banding after a few minutes, once the accumulated value exceeds mediump's precision at that magnitude — it runs fine on desktop's full highp support and only breaks on the phone in your users' hands.

// Reset time-based uniforms periodically instead of accumulating forever
const t = (performance.now() % 100000) / 1000; // wraps every ~100s
material.uniforms.uTime.value = t;
Test the actual fallback: query gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT) — if .precision is 0, the GPU has no true highp fragment support and is silently running your "highp" math at mediump.

4. Draw call & texture memory budgets

Rough, conservative starting budgets for a mid-range phone (2-3 year old Android mid-tier or an older iPhone) versus a typical desktop:

Budget Desktop (typical) Mid-range mobile
Draw calls / frame at 60fps ~1,000-3,000+ ~150-400
Triangles / frame Several million ~200k-500k
Total texture memory Several GB ~150-400 MB (shared with the OS and browser)

5. Thermal throttling

A phone has no active cooling. Sustained high GPU utilisation heats the SoC until the OS's thermal management clocks the GPU (and often the CPU) down — sometimes 30-50% below its peak clock — to stay within safe temperatures. This means:

Design implication: target and test against your expected sustained frame rate after several minutes of play, not the frame rate in the first few seconds. A frame-rate governor that lowers particle count, shadow resolution, or post-processing quality when the delta-time between frames creeps up is a more robust fix than assuming a fixed device tier from a one-time capability check.
// A minimal adaptive-quality governor
let quality = 1.0;
function animate(now) {
  const dt = (now - lastTime) / 1000;
  lastTime = now;
  if (dt > 1 / 45) quality = Math.max(0.5, quality - 0.02); // falling behind → cut back
  else if (dt < 1 / 58) quality = Math.min(1.0, quality + 0.005); // headroom → ease back up
  particleSystem.setActiveCount(Math.floor(maxParticles * quality));
  requestAnimationFrame(animate);
}

6. Touch input instead of mouse/keyboard

Every desktop-first interaction pattern needs a touch-friendly equivalent — there is no hover, no right-click, and often no keyboard at all:

Desktop pattern Mobile-friendly equivalent
Hover to preview/highlight Tap to select, with a visible "selected" state (no hover exists)
Right-click context menu Long-press gesture, or a persistent on-screen button
Mouse drag to orbit camera Single-finger drag to orbit, two-finger pinch to zoom, two-finger drag to pan
Scroll wheel to zoom Pinch gesture (touchmove with two active touch points)
Keyboard WASD/arrow controls On-screen virtual joystick or directional buttons
// Minimal pinch-to-zoom distance tracking
let lastPinchDist = null;
canvas.addEventListener('touchmove', (e) => {
  if (e.touches.length === 2) {
    const [a, b] = e.touches;
    const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
    if (lastPinchDist !== null) {
      const delta = dist - lastPinchDist;
      camera.position.multiplyScalar(1 - delta * 0.003); // zoom by scaling distance
    }
    lastPinchDist = dist;
    e.preventDefault(); // stop the page from scrolling/zooming
  }
}, { passive: false });

canvas.addEventListener('touchend', () => { lastPinchDist = null; });
Set touch-action: none on the canvas element in CSS alongside { passive: false } listeners — otherwise the browser's own pinch-zoom/scroll gestures fight with your own touch handling.

7. Testing on real devices

Chrome DevTools' emulated "mobile" mode (device toolbar) only simulates screen size and touch events — it still runs your shaders on the desktop GPU driver, which gives no useful signal about real mobile GPU performance, precision fallbacks, or thermal behaviour.

  1. Connect an Android phone via USB with USB debugging enabled, then open chrome://inspect on the desktop — it lists the phone's open tabs with a live "inspect" link into real DevTools running against the actual device.
  2. On iOS, connect an iPhone/iPad via cable, enable Web Inspector in Settings → Safari → Advanced, then open Safari's Develop menu on a Mac to attach to the page running on-device.
  3. Test on the oldest device your analytics show meaningful traffic from, not your own newest phone — a 60fps result on a current flagship says nothing about the 3-year-old mid-range Android devices that are usually the real long tail.
Next steps: pair this with the Debugging WebGL tutorial for reading chrome://gpu on the connected device itself, or the 3D optimisation tutorial for LOD and culling techniques that reduce the same draw-call and triangle budgets discussed in section 4.

Frequently Asked Questions

What will I learn in this tutorial?

Ship a WebGL simulation that actually runs well on phones: pixel ratio capping, shader precision, draw call and texture memory budgets, thermal throttling, touch input, and the mobile-specific bugs that don't show up on desktop.

What topics are covered in this tutorial?

This tutorial covers: A different regime, not a smaller desktop GPU, Cap devicePixelRatio, Shader precision: avoid highp overflow, Draw call & texture memory budgets, Thermal throttling, Touch input instead of mouse/keyboard, Testing on real devices.

How long does this tutorial take?

This tutorial takes approximately 25 minutes to complete.

What prerequisites do I need before starting?

This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.