Every third-party script promises it is lightweight. The promise is worth nothing on its own, because the thing that hurts your page is rarely the number in the marketing copy. It is the fourth chunk that loads on scroll, or the observer that fires during your own animation, or the retry loop that keeps going while the tab is hidden.
So the limits are written down as ceilings and a build gate stands behind them. An artifact over budget exits the build non-zero and never ships. That is the only version of this claim that means anything.
What the budget actually is
Fifteen gzipped ceilings are declared in the widget’s build script, and the build measures every artifact against its own. These are the ones a host page can feel:
| Artifact | Ceiling (gzip) |
|---|---|
Widget loader (widget.js) | 3.5 KiB |
| Loader plus runtime core | 14.25 KiB |
| Interface, on user intent | 35.75 KiB |
| Any one complete load path | 64 KiB |
| Composer and review in one session | 75 KiB |
| Isolation Worker, lazy | 8 KiB |
The loader measured 3.3 KiB gzipped on the build that produced this page, against a ceiling of 3.5. The interface measured 35.4 against 35.75. Both are close to their ceilings on purpose: headroom that is never spent is a budget nobody is actually held to, and the margin is what the next feature has to argue for.
The 64 KiB row is the interesting one. Annotation, screen capture, element selection, review mode, surveys and replay each get that same ceiling independently, so no single capability can quietly become the expensive one while the total still looks reasonable. There is one exception, and it is named: a page running the composer and review mode in the same session reaches 74.25 KiB, because that graph is the two smaller graphs reached by one person in one visit rather than new weight.
What loads, and when
The loader is the only thing that arrives with your page. It is unversioned, inert, and it does
four things: install the public API stub, respect data-autostart="false", qualify startup health,
and select a content-hashed runtime.
Everything after that is fetched from intent. The interface loads when somebody presses the
launcher or calls open(). Element selection loads after verified reviewer intent. Screen capture
is its own lazy chunk, kept out of the interface because it never loads on iOS at all and the
interface is already near its ceiling. The isolation Worker loads lazily or not at all.
Nothing loads on idle. Nothing loads on hover. Nothing preloads in case you might want it.
What happens when your page is already busy
Budgets only describe the download. The other half is the runtime, and a script that behaves well on an empty test page can still be the thing that pushes a real one over.
The governor watches two different things. The widget’s own cost is charged against per-axis envelopes for CPU, memory, network and render. The host’s condition is read from signals the widget does not own: event loop drift and long tasks it did not cause. Then it steps down through three states.
Healthy is everything you configured, inside every envelope.
Constrained pauses session replay for the rest of the document and drops the cheap signal: informational logs, ordinary clicks, successful responses. It keeps what you would actually miss, which is uncaught errors, navigation, console warnings and network failures.
Protected takes the global hooks off your page altogether. What survives is minimal error capture, the public API, and a person’s ability to write feedback.
The triggers are specific rather than a general sense of pressure, and they are worth reading as a table because the asymmetry between them is the design.
| Move | What causes it | What undoes it |
|---|---|---|
| Healthy to Constrained | An envelope at 80 percent, an ambient long task, or two event loop probes drifting by 100 ms inside ten seconds | 5 seconds below 80 percent |
| Constrained to Protected | A hard ceiling, one attributable task of 50 ms, one probe drifting by 500 ms, or three severe ambient signals in ten seconds | 30 seconds below half of every envelope |
Why recovery is slower than escalation
Stepping down happens the moment a threshold is crossed. Stepping back up takes sustained quiet, and the right-hand column of the table above is where those two figures live.
Those two numbers are asymmetric for different reasons. Protected is the state a breach or a genuinely struggling host put the runtime in, so leaving it deserves the longer look. Constrained recovers on the same figure that caused it, and that is a correction: it used to demand 15 seconds below 50 percent, a hysteresis gap nothing documented, and with the scheduler’s own slices counted as pressure it kept replay withdrawn for roughly 20 seconds after every checkpoint. Constrained became the widget’s usual state rather than its exceptional one.
That is the failure mode worth naming. A degradation ladder built without a way back is not protecting the host page. It is turning itself off permanently on the first busy second and calling that safety.
Why this is the feature
Feedback tooling has a specific failure mode. It is installed once, by someone who cares about it, and then it lives on a page owned by someone who does not. The day it costs that person a performance regression, it comes off the page and never goes back.
Being unnoticeable is not a nice-to-have for this category. It is the whole job, and it is why the ceilings are in the build rather than in a promise. What the reader installs on the page those ceilings protect is one script tag.