Keep the effects. Lose the stutter.

At 60 Hz a frame lasts 16.7 ms, and every effect on your page spends part of it. framebudget measures what a device can really do and turns on only the effects that fit. Slow phones stay smooth. Fast phones keep everything.

npm i framebudget
This page, frame by frame
Frame rate
60 fps
Late
0 of 0

Each bar is one frame: the page's own work, then every effect framebudget allowed. The outline is the same frame with every effect on.

Run this page as

app.ts
import { budget } from "framebudget";

if (budget.allows("parallax")) startParallax();

budget.on("change", (snapshot) => {
  setEffects(snapshot.effects);
});

Every effect spends the same 16.7 ms

Parallax, blur, canvas, transitions and sound each take a slice of every frame. A laptop has time to spare. A phone from 2019 runs out, so frames arrive late and the page stutters. Most sites ship one set of effects to everyone. framebudget picks per device.

Score 100, this device

Every effect on

0.0 ms

framebudget's choice

0.0 ms

This device runs every effect inside the frame, so framebudget keeps them all. Pick a slower device in the hero or the simulator to see the difference.

What happens, and when

framebudget acts at six moments in the life of a page. Every panel below is reading this page right now.

  1. Before the first paint

    Decide before anything moves

    An inline boot script in <head> runs a 1.6 ms benchmark and writes the decision on <html> before your CSS loads. An animation never starts only to freeze, and CSS can gate effects without any JavaScript.

    On this page

    <html data-framebudget="...">
  2. At the load event

    Measure again, warm

    Once the CPU has ramped up, a longer benchmark runs in five short tasks, never one long one. Four kernels give a score where 100 is the reference phone, using the median of each and the geometric mean of all four, so no single kernel dominates. The score is cached for the next page.

    This device, warm

    KernelRateRatio
    Float math......
    Typed arrays......
    Allocation......
    Canvas paths......

    Score ...Waiting for the load event

  3. For every effect

    A threshold per effect, with hysteresis

    Each effect has its own threshold instead of one global tier. An effect that is on stays on until the score falls 10% under its threshold, and an effect that is off needs 10% over it. A device that sits near a line does not flicker between visits.

    Thresholds against the score of this device

    1. Hover states10
    2. Counting numbers12
    3. Interface sounds20
    4. Staggered reveals24
    5. Frame chart, 1x32
    6. Deadline glow38
    7. Text reveals46
    8. Morphing controls49
    9. Page transitions62
    10. Spring presses66
    11. Magnetic buttons85
    12. Parallax90
    13. Cursor spotlight112
    14. Backdrop blur135
    15. Frame chart, full res180
  4. While the page runs

    A governor watches real frames

    It judges windows of 30 frames. Three windows in a row under 45 fps step one effect down, the most expensive first, then it waits two seconds. A phone that heats up loses its blur before it loses its frames.

    Governor log

      Turn on the load test and heat in the simulator to make it act.

    1. On the next visit

      It remembers what stuttered

      An effect the governor stepped down stays off on this device next time. After five clean visits it gets another try, and if it stutters again the wait doubles. All of it lives in one localStorage entry that never leaves the device.

      Learned on this device

      Nothing yet: no effect has stuttered on this device.

    2. Only if the site opts in

      Telemetry is off by default

      There is no default endpoint. A site can share anonymous, sampled reports to help calibrate the numbers, with one beacon when the page hides. Nothing is sent under Global Privacy Control or Save-Data, while a tier is forced, or while a device is simulated. This site opts in for its own visitors: see what it keeps.

      A report contains

      • Calibration version and rounded scores
      • Kernel rates and clock resolution
      • Cores, memory and CPU pressure
      • Tier, allowed and stepped-down effects
      • Median fps per source

      It never contains

      • Identifiers or cookies
      • The URL or the user agent
      • Timestamps

    Try a slower device

    Pick a device and this page changes for real: framebudget gets a different score, and every effect on the page turns on or off with it. Run the load test to burn that device's frame time on this thread, then heat it up and watch the governor step effects down.

    Device

    100

    100 is the reference phone. A device twice as fast scores 200.

    You are on your real device. Pick another one above.

    Score
    100
    Tier
    Full
    Effects on, of 15
    0
    Frame rate, fps
    60

    One frame on my device

    0.0 ms

    Effect decisions

    EffectThresholdCostDecision
    Hover states101Checking
    Counting numbers121Checking
    Interface sounds201Checking
    Staggered reveals242Checking
    Frame chart, 1x323Checking
    Deadline glow382Checking
    Text reveals462Checking
    Morphing controls492Checking
    Page transitions623Checking
    Spring presses663Checking
    Magnetic buttons852Checking
    Parallax905Checking
    Cursor spotlight1124Checking
    Backdrop blur1356Checking
    Frame chart, full res1808Checking

    Governor and changes

      Four ways in

      Ask in JavaScript, ask in React, decide before paint with the boot script, or let CSS read the decision straight from <html>.

      effects.ts

      import { budget } from "framebudget";
      
      // Your own effect: a threshold, a relative cost, and flags.
      budget.register("confetti", { threshold: 60, cost: 4, motion: true });
      
      if (budget.allows("confetti")) launchConfetti();
      
      budget.on("change", (snapshot, reason) => {
        // reason: "warm", "governor", "pressure", "motion", ...
        setEffects(snapshot.effects);
      });

      Hero.tsx

      import { useBudget, useTier } from "framebudget/react";
      
      export function Hero() {
        // false during server rendering and hydration, then the real answer
        const parallax = useBudget("parallax");
        const tier = useTier();
      
        return <Layers parallax={parallax} data-tier={tier} />;
      }

      vite.config.ts

      import { createBootScript } from "framebudget/boot";
      
      // The same calibration you pass to configure(), so both agree.
      const boot = createBootScript({ calibration: { effects } });
      
      // First script in <head>, before your CSS.
      const html = page.replace("<head>", `<head><script>${boot}</script>`);

      site.css

      /* Written on <html> before the first paint. */
      html[data-framebudget-effects~="blur"] .header {
        backdrop-filter: blur(16px);
      }
      
      html[data-framebudget="Lite"] .hero-video {
        display: none;
      }

      The effects on this page

      Fifteen effects, each registered with a threshold and a cost. A tier is every effect at or below its floor: Lite 20, Medium 49, High 90, Full 180. The column of this device's tier is lit.

      EffectWhat it doesThresholdCostLite 20Medium 49High 90Full 180
      Hover states hoverLinks, rows and controls answer the pointer.101
      Counting numbers countersScores, frame times and counts roll to their new value on a spring. Off under reduced motion.121
      Interface sounds soundSynthesized taps, toggles and outcomes, from cuelume. No audio files. Off under Save-Data.201
      Staggered reveals entrancesSections arrive in a short stagger as they scroll into view. Off under reduced motion.242
      Frame chart, 1x canvasLowResThe hero's live frame chart, at one pixel per CSS pixel, stepping. Off under reduced motion.323
      Deadline glow shimmerThe 16.7 ms line breathes and the install command catches a sheen. Off under reduced motion.382
      Text reveals textRevealHeadlines arrive by blur, by word, by line or by wipe. Off under reduced motion.462
      Morphing controls morphSelectors slide, buttons confirm in place, readouts swap their text. Off under reduced motion.492
      Page transitions pageTransitionA cross-document view transition between this page and the API reference. Off under reduced motion.623
      Spring presses springsButtons give under the press and spring back. Off under reduced motion.663
      Magnetic buttons magneticThe main buttons lean toward the pointer. Off under reduced motion.852
      Parallax parallaxThe hero's headline and grid drift at different speeds as you scroll. Off under reduced motion.905
      Cursor spotlight spotlightA soft light follows the pointer across the panels. Off under reduced motion.1124
      Backdrop blur blurFrosted glass behind the header and the device dock.1356
      Frame chart, full res canvasHiResThe frame chart at the screen's pixel ratio, scrolling smoothly. Off under reduced motion and Save-Data.1808

      Questions

      Does measuring the device slow the page down?

      The boot script is 3.7 KB gzipped and its benchmark runs for 1.6 ms. The warm benchmark runs after the load event in five separate tasks, so it never becomes a long task. The governor samples frames for the first seconds and while the visitor interacts, not forever.

      Why not just read the core count or the memory?

      They are hints, not speed. Two phones with eight cores can differ by five times. framebudget uses them only as caps: one gigabyte of memory or two cores limit the score to 60. The score itself comes from running code on the device.

      What about reduced motion and Save-Data?

      Effects flagged motion turn off under prefers-reduced-motion: reduce, and effects flagged data turn off under Save-Data or a 2g connection. Both are watched live. These preferences do not lower the reported tier, because the device did not get slower.

      Does it send anything anywhere?

      Not unless the site developer configures an endpoint. Local learning stays in one localStorage entry. Optional telemetry is anonymous, sampled, sent as one beacon, and never sent under Global Privacy Control or Save-Data. This site does share its own visitors' measurements, to calibrate the numbers: what it sends and how to say no.

      What happens during server rendering?

      Without a window, allows() answers false and the tier is Lite. In React, useBudget answers false during server rendering and hydration, so effects start on the client only.

      Are the reference numbers final?

      No. The reference rates and thresholds come from one desktop measurement scaled down to stand for a mid-range phone. They will change once they are calibrated on real devices, and every value can be overridden with configure().