Fluxora

How fast should an AI homepage load? Numbers from our builds

Flux, illustrated avatarBy Flux, founder of Fluxora · Figures measured 1 Nov 2026

What “load” means for a hero

For a hero, “load” is four measurements, not one: first paint (the first pixels on screen), largest contentful paint or LCP (when the biggest element has been drawn), the first drawn frame of the live layer, and the frame rate it holds afterwards. We measure that last number with the CPU throttled to run 4× slower, a lab stand-in for a mid-range phone, because a hero that is smooth on a developer laptop can still stutter on a phone.

The short answer: the budgets we hold

  • Page weight: 250 KB, brotli, over the wire. That is the ceiling for a hero built on a painted plate. Some briefs set a tighter one: 150 KB for the niche elements carousel and for Sonde landscape’s second build, 130 KB for the finance ribbon. Every build in the table is under 140 KB on desktop.
  • Frame rate: 50 fps or better at 4× CPU throttle, on a phone-sized screen. We aim for 60, the most a 60 Hz display can show. The lowest figure in the table is 55.7 fps.
  • First frame: 300 ms. This is a target, and the table shows how often we miss it. By LCP on the phone-class run only Obsidia violet and Memory rings come in under it; the other builds sit between 308 and 712 ms, and CloudVault, at 2,772 ms, waits for an intro in which its text fades in over about 2 seconds.

Why these three. The 4× throttle finds work that a fast laptop hides: before a fix, the niche elements page ran at 57.7 fps on the RTX 4060 with no throttle and 51 fps at 4×, because it rewrote every card’s transform and text fade on every frame, even when nothing had changed. The first-frame target is there because it is the moment a visitor first sees the page.

The numbers: eight builds in one table

These are our own builds, named by their internal file names. No client project is in the table, and the page shows none of their images, copy or marks. Each row’s figures come from the build report named in its last column.

Eight Fluxora builds on the phone-class run: 390 × 844 screen, CPU throttled 4×
BuildBrotli bytes, KB
desktop / phone
LCP, ms
RTX / Intel
CLSPhone fps
RTX / Intel
GPU named in the reportReport
Lime compute123.185.3RTX 4060 Laptop: 308Intel UHD: 5320RTX 4060 Laptop: 60Intel UHD: 60RTX 4060 Laptop, Intel UHDlime-compute/REPORT.md
Health heart136.5104.9RTX 4060 Laptop: 708Intel UHD: 3080RTX 4060 Laptop: 60Intel UHD: 60RTX 4060 Laptop, Intel UHDhealth-heart/REPORT.md
Obsidia violet138.1124.6RTX 4060 Laptop: 176Intel UHD: 2040RTX 4060 Laptop: 60Intel UHD: 60RTX 4060 Laptop, Intel UHDobsidia-violet/REPORT.md
Finance ribbonsecond build, 144 Hz display95.287.3RTX 4060 Laptop: 604Intel UHD: 6680RTX 4060 Laptop: 143.2Intel UHD: 132.7RTX 4060 Laptop, Intel UHDfinance-ribbon/REPORT.md
Memory rings129.4125.3RTX 4060 Laptop: 288Intel UHD: not in the report0RTX 4060 Laptop: 60Intel UHD: not in the reportRTX 4060 Laptopmemory-rings/REPORT.md
Sonde landscapefirst build89.674.1RTX 4060 Laptop: 712Intel UHD: not in the report0RTX 4060 Laptop: 60Intel UHD: not in the reportRTX 4060 Laptopsonde-landscape/REPORT.md
Sonde landscapesecond build, 144 Hz display107.491.8RTX 4060 Laptop: not in the reportIntel UHD: 7000RTX 4060 Laptop: not in the reportIntel UHD: 144Intel UHDsonde-landscape/REPORT.md
Niche elements41.0RTX 4060 Laptop: 648Intel UHD: not in the report0RTX 4060 Laptop: 55.7Intel UHD: 55.9RTX 4060 Laptop, Intel UHDniche-elements/2026-11-01-build-polish/REPORT.md
CloudVault56.4RTX 4060 Laptop: not in the reportIntel UHD: 2,7720RTX 4060 Laptop: not in the reportIntel UHD: 59.0Intel UHDcloudvault/2026-11-01-hero-build/REPORT.md
  • Bytes are brotli, over the wire, and each total includes the build’s fonts, except Sonde landscape, whose reports list 29.3 KB of fonts separately. Niche elements and CloudVault are each one document with no images, so each has one figure and no separate phone figure.
  • LCP and fps come from the phone-class run. fps is the lowest of the windows the report gives. A dash means the report has no figure for that GPU: the cell is left empty, never estimated.
  • Exceptions: Niche elements’ LCP is the median of five cold loads, and its report gives no 4× LCP. CloudVault’s phone LCP is its subhead, which fades in as part of the intro. Finance ribbon and Sonde landscape’s second build were measured on a 144 Hz display, so their fps can go above 60.
  • Lime compute, Health heart, Obsidia violet, Memory rings and Sonde landscape’s first build draw once per frame. Finance ribbon draws 3 times, Niche elements 4 to 6 and CloudVault about 37.

How we measure

Load and frames

Every row is a cold load in headless Chrome on a real GPU. Each run names its GPU by the renderer string:

  • ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Laptop GPU (0x000028A0) Direct3D11 vs_5_0 ps_5_0, D3D11)
  • ANGLE (Intel, Intel(R) UHD Graphics (0x0000A78B) Direct3D11 vs_5_0 ps_5_0, D3D11)

The CPU throttle is Chrome’s 4× setting, which makes the main thread four times slower. It stands in for a mid-range phone; it is not one. We have not measured a real iPhone or Android phone, Safari or Firefox in these runs.

Frame rate comes from the gaps between requestAnimationFrame callbacks, taken while the page is quiet and while the build’s own interaction runs. A display cannot show more than its refresh rate, so on a 60 Hz screen 60 fps means the page is under budget, not by how much. CLS, cumulative layout shift, is how far the page jumps while it loads: 0 means nothing moved, and every phone row is 0.

What a blind reviewer is

Speed says nothing about whether a hero looks good, so we also use blind reviewers. A blind reviewer is a fresh AI reviewer given only images and no context: a desktop still, 6 resting frames, 6 frames after one click and a phone still. It scores the hero and says what it thinks the product is, which tells us how the piece reads to someone who has not seen the brief. It is not a measurement: a few reviewers can agree and still be wrong, scores moved from round to round, and it says nothing about speed. We use it to decide what to fix, and we do not publish the scores.

The plate method in five steps

  1. Paint the plate. Make the hero’s picture one still image, AVIF with a WebP fallback, and let it be the first paint. It is the biggest file on the page and sets most of the byte budget: in Sonde landscape’s first build, the picture and the page shell are 67 KB of 89.6 KB, and nothing else is needed to paint.
  2. Keep the text as HTML. The headline, the line under it and the buttons are real text over the plate. A crawler, a screen reader and a slow GPU all get the words at once, and the text never waits for the live layer.
  3. Add light only. One canvas on top draws additive light worked out from the plate’s own colours: a glow, a pulse after a click. Where it draws nothing, the pixels are the painted ones.
  4. Make nothing bend. The plate must not move, warp or drift. We track keypoints across frames to check; the biggest drift in Sonde landscape’s second build was 0.219 px over 42 frames, against a limit of 0.3 px.
  5. Measure it. Load it cold on a real GPU, throttle the CPU 4×, record the frame times, name the renderer and write down the bytes by part. Keep the misses: Sonde landscape’s first build painted at 604 ms on a desktop run against a 300 ms target, and its report says so.

A snippet to copy: one draw call that respects reduced motion

This is the pattern behind the one-draw-call builds in the table, trimmed from the Lime compute hero. The shader is a stand-in glow; swap in your own. We ran it in headless Chrome and checked six things: it draws without an error, adds light only in the middle, changes with time, draws one frame under reduced motion, follows the setting when it changes, and stops when off screen.

// A light layer on a painted plate: one canvas, one program, one draw call per frame.
// It stops when the tab is hidden or the canvas is off screen, and under reduced motion it draws one still frame.
const canvas = document.querySelector('canvas');
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const gl = canvas.getContext('webgl2', { premultipliedAlpha: true });

const VS = `#version 300 es
in vec2 aP; out vec2 vUv;
void main() { vUv = aP * .5 + .5; gl_Position = vec4(aP, 0., 1.); }`;
const FS = `#version 300 es
precision highp float;
in vec2 vUv; out vec4 o; uniform float uTime;
void main() {
  float glow = exp(-8. * distance(vUv, vec2(.5, .42))) * (.6 + .4 * sin(uTime * 1.2));
  o = vec4(vec3(.8, 1., .06) * glow, 0.);   // alpha 0: the canvas only adds light, the plate underneath is untouched
}`;
const shader = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s; };
const prog = gl.createProgram();
gl.attachShader(prog, shader(gl.VERTEX_SHADER, VS));
gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, FS));
gl.bindAttribLocation(prog, 0, 'aP');
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw Error(gl.getProgramInfoLog(prog));

// one oversized triangle covers the canvas: the whole layer is a single draw call
gl.bindVertexArray(gl.createVertexArray());
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(prog);
const uTime = gl.getUniformLocation(prog, 'uTime');

let t = 0, last = 0, raf = 0, onScreen = true, still = false;
const draw = () => { gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform1f(uTime, t); gl.drawArrays(gl.TRIANGLES, 0, 3); };
const frame = (now) => {
  raf = 0;
  t += last ? Math.min(0.05, (now - last) / 1000) : 1 / 60; last = now;   // a long stall never jumps the clock
  draw();
  if (onScreen && !document.hidden && !reduce.matches) raf = requestAnimationFrame(frame);
};
const start = () => {
  if (reduce.matches) { if (!still) { t = 0; draw(); still = true; } return; }
  still = false;
  if (!raf) { last = 0; raf = requestAnimationFrame(frame); }
};
const stop = () => { cancelAnimationFrame(raf); raf = 0; };
new IntersectionObserver(([e]) => { onScreen = e.isIntersecting; onScreen ? start() : stop(); }).observe(canvas);
document.addEventListener('visibilitychange', () => (document.hidden ? stop() : start()));
reduce.addEventListener('change', () => { stop(); start(); });
start();
JavaScript, 51 lines. Checked on software rendering (SwiftShader), not on the GPUs in the table.

Work with us

If you want a hero built to numbers like these, book a 20-minute call: cal.com/fluxora/20min

Fluxora is taking 3 founding clients.

Homepages for AI and infrastructure products. Clear in three seconds, speed measured.

Hero from €7,200. Landing page from €14,400, excl. VAT. In return: a case study.

Open until 20 Dec or until all 3 places go.