Guides · 6 min read · Updated

How do you make an animated website fast on phones?

Set a budget before you build, serve small compressed media, start heavy effects after the first paint, and measure frame time at runtime so quality drops on slow devices instead of the whole page stuttering. Give visitors without a real GPU or with reduced motion turned on a still, readable page, and track Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift on real phones.

Start with a budget

A budget turns taste into a number you can fail a build on. The figures below are starting points we suggest for a marketing page with a hero effect. They are not a standard, so tune them to your audience and measure.

ItemSuggested budgetWhy
Hero poster imageUnder 150 KBIt is usually the Largest Contentful Paint element
JavaScript before first interactionUnder 200 KB compressedParse and run time costs more on a phone than on your laptop
Hero video (if any)Under 3 MB for phonesMobile data and decode time
3D modelUnder 2 MBDownload plus GPU upload
Frame timeUnder 16 ms at 60 Hz, with headroomDropped frames are what visitors feel
Effects running at onceOne main effect, a few small onesEach extra effect competes for the same GPU

Every AnimatedWebs template card shows the live demo's page weight, so you can compare a light CSS-and-canvas page with a heavy video one before you pick. A lighter template is the cheapest optimization there is.

Compress media for the phone, not the studio

Media is almost always the largest part of an animated page. Export images in a modern format at the size they display, and make a separate smaller video for small screens.

media.sh
# Poster: WebP at quality 72, a 1280 wide one and a 700 wide one for phones
cwebp -q 72 -resize 1280 0 hero.png -o hero.webp
cwebp -q 72 -resize 700 0 hero.png -o hero-720.webp

# Phone video: 720p, no audio, stronger compression, index first
ffmpeg -i hero-source.mov -an -vf "scale=1280:-2,fps=30" -c:v libx264 -preset slow -crf 28 -pix_fmt yuv420p -movflags +faststart hero-720.mp4
hero.html
<picture>
  <source srcset="/hero-720.webp" media="(max-width: 700px)" type="image/webp" />
  <img src="/hero.webp" alt="" width="1280" height="720"
       fetchpriority="high" decoding="async" />
</picture>

Set width and height on images and reserve space for the canvas, so nothing shifts when they load. Use fetchpriority="high" on the one image that is your Largest Contentful Paint element, and never lazy-load it. For scrubbed video, the video hero guide covers keyframes and faststart in detail.

Paint first, then start the effect

Visitors judge speed by when they see something useful. Render the headline and poster as plain HTML, then load the heavy library after the first paint with a dynamic import. The canvas fades in over the poster when it is ready.

defer.js
const start = async () => {
  const { initHero } = await import("./hero-webgl.js"); // loads three.js here
  await initHero(document.getElementById("bg"));
  document.documentElement.classList.add("hero-ready"); // CSS fades the canvas in
};

if ("requestIdleCallback" in window) {
  requestIdleCallback(start, { timeout: 1500 });
} else {
  setTimeout(start, 200);
}

The timeout means the effect still starts on a busy device. Keep the poster visible until hero-ready, and make sure the poster looks like the first frame of the effect so the swap is not noticeable.

Adapt quality from measured frame time

You cannot know a visitor's phone in advance, but you can watch how it performs. Measure the time between frames, and when the average is bad for a while, lower the cost. Pixel ratio is the best first lever because shading cost grows with its square. The callback you pass to requestAnimationFrame receives a timestamp you can use for this.

adaptive.js
const steps = [2, 1.5, 1.25, 1]; // pixel ratio ladder, best first
let level = steps.findIndex((r) => r <= devicePixelRatio);
if (level < 0) level = steps.length - 1;

let last = performance.now();
let sum = 0;
let n = 0;

export function onFrame(now, renderer) {
  sum += now - last;
  last = now;
  n++;
  if (n < 45) return;

  const avg = sum / n;
  sum = 0;
  n = 0;

  if (avg > 22 && level < steps.length - 1) {
    level++;
    renderer.setPixelRatio(steps[level]);
    renderer.setSize(innerWidth, innerHeight, false);
  }
}

Only step down, never back up. Stepping up when things look good makes the page oscillate, which looks worse than a stable lower quality. After the lowest step, turn off the most expensive extras: bloom, shadows and extra particles. The 22 millisecond threshold is a starting point that tolerates a small amount of variation around 60 Hz; tune it on real hardware.

Software GL and weak GPUs

Some browsers report WebGL but draw it on the CPU with a software renderer. That happens in virtual machines, remote desktops, some older devices and with blocked graphics drivers, and it makes a heavy scene unusable. The context attribute failIfMajorPerformanceCaveat asks the browser to refuse to create a WebGL context when it would fall back to a slow renderer, so you can show a still image instead.

gl-check.js
export function canRunWebGL() {
  const probe = document.createElement("canvas");
  const gl = probe.getContext("webgl2", { failIfMajorPerformanceCaveat: true })
          || probe.getContext("webgl", { failIfMajorPerformanceCaveat: true });
  if (!gl) return false;
  gl.getExtension("WEBGL_lose_context")?.loseContext();
  return true;
}

if (!canRunWebGL()) {
  document.documentElement.classList.add("no-gl"); // CSS shows the poster only
}

The fallback should be a good page, not an apology: the poster, the full text and every button. If you construct your renderer with the same attribute, a refusal can also be caught there. Treat a missing context as a normal state, since tabs can also lose a context while open.

Reduced motion and data saving

Honor prefers-reduced-motion: reduce by replacing movement with a still frame or a short fade. That is both an accessibility need and a performance win, because the visitor no longer pays for the effect. Treat data saving the same way when the browser exposes it.

gate.js
import { canRunWebGL } from "./gl-check.js";

const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
const saveData = navigator.connection && navigator.connection.saveData;
const lowCores = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;

export const richMotion = !reduce && !saveData && canRunWebGL();
// lowCores is a hint, not a verdict: use it to start at a lower pixel ratio.
export const startRatio = lowCores ? 1.25 : 2;

Core Web Vitals on an animated page

Core Web Vitals are three field metrics: Largest Contentful Paint for loading, Interaction to Next Paint for responsiveness and Cumulative Layout Shift for visual stability. Good values are 2.5 seconds or less, 200 milliseconds or less and 0.1 or less, judged at the 75th percentile of real page loads, split by mobile and desktop.

MetricAnimated page riskFix
Largest Contentful PaintThe hero is a canvas or a video that loads lateMake the LCP element a real image or heading, load the effect after it, preload the poster
Interaction to Next PaintA render loop and big scripts block the main threadDefer the library, keep per-frame work small, pause offscreen, avoid long tasks
Cumulative Layout ShiftCanvas, video or fonts change size after loadGive the canvas fixed or aspect-ratio size, set image dimensions, reserve space

Lab tools are useful while building, but they run on your computer and network. Check the field data for your real visitors, and test on at least one modest Android phone and one older iPhone. Browser developer tools can throttle the CPU to approximate a slower device.

Keep the main thread free

Phones have slower processors than laptops, so JavaScript that feels instant on your machine can block a phone for a long time. Interaction to Next Paint reflects this: a tap that waits behind a long task feels broken. The fixes are plain. Ship less script, split code so the effect loads separately from the page, and move work out of the render loop.

  • Allocate once. Create vectors, geometry and arrays outside the loop and reuse them, so the garbage collector has nothing to pause for.
  • Throttle pointer work. A pointermove handler that does heavy math on every event should store the position and let the render loop read it.
  • Use passive scroll listeners and keep them tiny. Store a number, do the work in requestAnimationFrame.
  • Break long tasks up. If a setup step takes more than about 50 milliseconds, split it across frames or idle callbacks.

These habits also help battery life, which matters to visitors who never mention it but leave when their phone gets warm.

Test the way your visitors browse

Your development machine hides most problems. Open the page on a phone you do not use every day, on mobile data, with battery saver on. Watch the first screen load, scroll the whole page, rotate the device and switch tabs and come back. Each of those exposes a different failure: slow first paint, dropped frames, a broken resize and a stopped loop.

Then compare lab numbers with field data. A lab run shows what one device did once. Field data from real visits shows the spread, and the slowest quarter of your visitors is the group the 75th percentile rule is built to protect.

A pre-launch checklist

  1. 1Write the budget down, and fail the build when a file passes it.
  2. 2Make the first screen a real image and heading that works with no JavaScript.
  3. 3Load the effect after first paint, and fade it in over the poster.
  4. 4Add a pixel ratio ladder and step down from measured frame time.
  5. 5Check for software GL and show the still version when it applies.
  6. 6Honor reduced motion and data saving.
  7. 7Pause the loop when the tab is hidden or the canvas is offscreen.
  8. 8Test on real phones, then read the field metrics a week after launch.

Templates that do this

Live demos with a small page weight, shown on each card. Picked from the catalog when the page is built.

Questions people ask

Why is my animated website slow on phones?+

Usually fill rate or download size. A high device pixel ratio makes the GPU shade several times more pixels, and large videos or models cost data and decode time. Cap the pixel ratio, ship smaller media for small screens and defer the effect until after first paint.

What are good Core Web Vitals values?+

Largest Contentful Paint at 2.5 seconds or less, Interaction to Next Paint at 200 milliseconds or less and Cumulative Layout Shift at 0.1 or less, measured at the 75th percentile of page loads, according to web.dev.

How do I detect a device that is too slow for WebGL?+

Request the context with failIfMajorPerformanceCaveat set to true, which makes creation fail instead of using a slow software renderer, and watch frame time at runtime to step the pixel ratio down.

Should I disable animation for reduced motion?+

Replace non-essential motion with a still frame or a brief fade. The page must stay complete and usable. This also saves work on the visitor's device.

Does a canvas hero hurt Largest Contentful Paint?+

It can. A canvas is not an LCP candidate, so the largest image or text block becomes the LCP element, and it paints late if the effect delays it. Make that element a real poster image or heading, preload the poster, and start the canvas after first paint.

Facts checked on

What this guide says about other products and web standards comes from the official pages linked below, and those can change, so check them for the latest. The code samples are our own. What it says about AnimatedWebs comes from how our own site works.

  • Core Web Vitals are Largest Contentful Paint (good at 2.5 seconds or less), Interaction to Next Paint (good at 200 milliseconds or less) and Cumulative Layout Shift (good at 0.1 or less). Pages are assessed at the 75th percentile of page loads, segmented by mobile and desktop. INP replaced First Input Delay when it became stable in 2024. web.dev: Web Vitals ↗
  • The failIfMajorPerformanceCaveat context attribute makes getContext() return null for WebGL when no hardware GPU is available or performance is low, instead of falling back to a slow software renderer. powerPreference accepts default, high-performance and low-power. MDN: HTMLCanvasElement.getContext() ↗
  • prefers-reduced-motion has the values no-preference and reduce. Reduce means the user enabled a reduced-motion setting on their device. MDN: prefers-reduced-motion ↗
  • requestAnimationFrame callbacks receive a timestamp, usually run at the display refresh rate, and are generally paused in background tabs. MDN: requestAnimationFrame() ↗
  • FFmpeg's faststart movflag moves the MP4 index to the front of the file so playback can begin sooner. FFmpeg documentation: formats ↗

Product and company names are trademarks of their owners. AnimatedWebs is independent and not affiliated with them.

See the technique running

Play the live demos, open the one you like, and start with a free template or unlock the whole library.

We use essential cookies to keep you signed in, a referral cookie only if you arrive through a partner link, and cookie-free analytics. No ads, no tracking. Cookie Policy