Command Palette

Search for a command to run...

Hectal
Course outline
Topic 17.7Advanced ~99 min · step 7 of 7

Build: The Performance Budget

In one line

The showroom now watches itself: a budget module reads renderer.info once a second and warns when draw calls, triangles, textures or geometries pass their limits. A colour picker repaints without leaking, and the mudguards join the bike.

0/7 · 0%

Before you start

You will learn

  • Check the scene against a budget once a second
  • Repaint the frame without making anything new
  • Add the mudguards

You write code in

In your lumen-3d project: add src/systems/budget.js, and change index.html, bike.js, picking.js, panel.js and main.js

Files you touch

  • src/systems/budget.js
  • src/objects/bike.js
  • src/systems/picking.js
  • src/ui/panel.js
  • src/main.js
  • index.html

Think of it like this

A household budget. You do not wait for the bank to say you are broke. You write down what each month may cost, and a small check tells you the day you pass a line.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

budget
A written list of limits (draw calls, triangles, textures) the app checks itself against.
updatable
An object with a tick(delta) method that the loop calls every frame.
autoReset
The renderer.info setting that restarts the render counters at every render() call.
shared material
One material used by many meshes, so a change reaches all of them and nothing is created.

Step by step

01Read the numbers

The example prints a plain frame (40 draw calls), a frame through the effect chain (54), the budget and an empty list of problems.

02See the check work

It then checks the same frame against a limit of 10 draw calls, and names the one over the line.

03Paint 20 times

It repaints 20 times: textures stay 20 and programs stay 22.

04Find the mudguards

Click Mudguards in the panel to select them (the glowing outline from Phase 8 follows), and check the price line.

Try it yourself

  1. 1

    Trip the alarm

    In budget.js, change triangles: 30000 to triangles: 5000 and reload. After a second the console shows Over budget: triangles 9582 > 5000 (or close to it; the number depends on the frame). Put it back.

  2. 2

    Add a heavy part

    In main.js, after scene.add(createStand());, add scene.add(new THREE.Mesh(new THREE.IcosahedronGeometry(1, 40), new THREE.MeshStandardMaterial()));. This sphere has 33,620 triangles, so the check line reads over budget: [{"name":"triangles","value":43202,"limit":30000}], and on a normal computer the console warns after a second.

Code & diagrams

The showroom after Phase 17 New tab

Every project file of this phase joined into one (local imports cannot run in the preview). The first lines of output are the checks added for this topic.

idle

Console

Run the scene to see its console.

Expected console output

backend: WebGL 2
parts: down-tube, top-tube, seat-tube, chain-stays, seat-stays, fork, front-wheel, rear-wheel, handlebar, crankset, chain, pedals, brakes, mudguards, rear-light, headlight
plain frame: draw calls 40 | triangles 9568 | geometries 42 | textures 20
frame with the effect chain: draw calls 54 | triangles 9582
over budget: [] | the budget: {"calls":70,"triangles":30000,"textures":24,"geometries":50}
after 20 colour changes: textures 20 -> 20 | programs 22 -> 22
a tight budget warns: [{"name":"calls","value":40,"limit":10}]

Your project files

Your lumen-3d project after this topic. Click a file to see it in full.

src/systems/budget.jsjs

The whole file after this topic. If yours differs, you can copy this one and carry on.

// Phase 17: the performance budget. A budget is a list of limits the showroom checks itself against.
// Measure the cheapest phone you support, then set each limit a little above what the scene really needs.
export const BUDGET = { calls: 70, triangles: 30000, textures: 24, geometries: 50 };

// The numbers renderer.info reports, next to their limits. Returns the ones that are over.
export function overBudget(info, budget = BUDGET) {
  const now = { calls: info.render.calls, triangles: info.render.triangles, textures: info.memory.textures, geometries: info.memory.geometries };
  return Object.keys(budget)
    .filter((name) => now[name] > budget[name])
    .map((name) => ({ name, value: now[name], limit: budget[name] }));
}

// An updatable for the loop: once a second, check the budget, and speak only when the answer changes.
export function createBudget(renderer, { budget = BUDGET, every = 1 } = {}) {
  // The effect chain (Phase 15) calls render() several times per frame, and by default renderer.info
  // starts again at every call, so it would only show the last pass. Reset it ourselves, once per frame.
  renderer.info.autoReset = false;
  let waited = 0;
  let said = "";
  return {
    tick(delta) {
      waited += delta; // delta is 0 in a hidden tab, so a background tab is never measured
      if (waited >= every) {
        waited = 0;
        const text = overBudget(renderer.info, budget).map((p) => p.name + " " + p.value + " > " + p.limit).join(", ");
        if (text !== said) { // the same news is not news
          said = text;
          console[text ? "warn" : "info"](text ? "Over budget: " + text : "Back within budget.");
        }
      }
      renderer.info.reset(); // this frame's drawing starts counting from zero (the tick runs before it)
    },
  };
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Leave autoReset on

Delete the line renderer.info.autoReset = false; and the line renderer.info.reset(); from budget.js, and set calls: 10.

terminal
$ Run
── what you'll see ──
frame with the effect chain: draw calls 1 | triangles 1
over budget: []
The chain really sent 54 draw calls. With the default `autoReset`, the budget sees only its last pass, so a scene far over its limit looks fine, and while the effect chain is on, no warning appears.

Myth vs fact

Myth

A budget slows the app down.

Fact

Reading three numbers once a second is free. The cost is in not having one, and finding out from a user.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Send the budget's warnings to your analytics in production, with the device model: a real-world list of which phones exceed which limit is worth more than any lab measurement.

Remember this

  1. 1

    The problem this solves: Lena's phone. The showroom has grown through sixteen phases, and each part added draw calls, textures and effects. Nobody has looked at the total. Now the team writes the limits down and the page checks itself.

  2. 2

    What it adds. A new src/systems/budget.js with BUDGET (calls 70, triangles 30,000, textures 24, geometries 50), overBudget(info, budget) and createBudget(renderer) (an updatable for the loop that warns once a second, and again only when the answer changes). It sets renderer.info.autoReset = false and resets the counters itself each frame, because the effect chain draws several times per frame (Topic 17.1). This showroom measures 54 calls with effects on, about 9,600 triangles, 20 textures and 42 geometries. The limits sit above that: close for calls and textures (room for the next parts), wide for triangles (a heavy model should trip it).

  3. 3

    A colour picker without a leak. The panel gets four colour buttons (real buttons, labelled for screen readers). setFrameColour(bike, colour) in bike.js changes the colour of the frame's one shared paint. No material, texture or shader is made, so there is nothing to dispose: the example paints 20 times and textures and programs stay at 20 and 22. The picker's glowing copy follows the colour (picking.js).

  4. 4

    The spokes. They have been one InstancedMesh per wheel since Phase 7, so there is nothing to change: the bike's 36 spokes cost 2 draw calls. We measured the whole bike instead of guessing: 40 draw calls in a plain frame and 54 with the effect chain, well under the budget.

  5. 5

    The mudguards. createMudguards() adds two thin arcs above the tyres, named "mudguards" (price 40 in PARTS). They share one geometry and one material, and add 2 draw calls. This is the phase's bike part.

  6. 6

    What the showroom can do now: warn when it grows past its limits, repaint without leaking, and show a bike with mudguards, with every earlier feature (pause, keyboard twins, test ride, effects) unchanged. Still missing: feature detection and fallbacks for odd browsers, context-loss recovery, a text alternative for screen readers, lazy loading and deployment: Phase 18 adds them, with the bell and bottle cage.

Explain it without notes

01

Why does budget.js reset renderer.info itself?

02

Why does changing the frame colour not need dispose()?

Practice

01

Add a memory line to the budget: a limit of 12 textures for a low-end phone profile, and a function profile(name) returning the right budget.

02

Why is the draw-call limit 70 rather than the 54 we measured with effects on?

Done when you can

  • Your project has src/systems/budget.js and the loop checks it once a second

  • Repainting 20 times leaves the texture count unchanged

  • The mudguards appear as a part in the panel with their price

  • Your files match the six files of this topic