Performance & Debugging
PulsePoint re-renders a component when its state or props change, then patches only the DOM that differs. In practice, speed comes down to which values you keep in state and which prop identities you keep stable. The profiler shows where the time goes, and the console messages tell you what went wrong.
The render contract
pp.statemeans "render required". Timers, request generations, cursors and text that only feeds an RPC go inpp.ref, because changing a ref never re-renders. Debouncing a setter reduces how often a render happens, not what each render costs.- Keep fast-changing state in the smallest component that needs it. A search input that owns its query re-renders itself, not the page.
- Prop identity decides child re-renders. Children compare props shallowly by identity. Wrap arrays and objects in
pp.memo, handlers inpp.callback, and context provider values too. Primitives cost nothing. - Key every
pp-forrow with a stable id and keep one root element per row. A row whose markup did not change is reused without being parsed again. Each unchanged row costs about one string comparison. - A mounted child is reconciled by its attributes. The parent does not re-parse a child's body; pass changing data as props.
pp.transition()gives you a pending flag but does not split rendering into chunks;pp.deferredValuelets an expensive child lag one commit behind.- Never "optimize" with
querySelector,addEventListenerorinnerHTML. It bypasses reconciliation, and the next render undoes it.
Profiling
Timing is off by default and costs nothing while off. Turn it on for an interaction:
// In the console, for work after page load:
pp.enablePerf();
// …interact with the page…
console.table(
Object.entries(pp.getPerfStats()).map(([id, s]) => ({
id,
renders: s.renderCount,
totalMs: s.phases.total?.totalMs.toFixed(1),
domDiffMs: s.phases.domDiff.totalMs.toFixed(1),
})),
);
pp.resetPerfStats(); // clear, then measure the next interaction
pp.disablePerf();
Timing the first mount needs a flag that survives a reload. Storage errors (private mode, sandboxed frames) are ignored:
// Mount happens before you can type into the console. To profile it:
localStorage.setItem("pp-perf", "1");
location.reload();
// after load:
pp.getPerfStats();
// when done:
localStorage.removeItem("pp-perf");
pp.getPerfStats() returns one entry per component id:
{
"order_table": {
renderCount: 12,
phases: {
script: { count: 12, totalMs: 3.1, maxMs: 0.6 },
compile: { count: 1, totalMs: 2.4, maxMs: 2.4 },
domDiff: { count: 12, totalMs: 9.8, maxMs: 2.1 },
effects: { count: 3, totalMs: 0.4, maxMs: 0.2 },
// also: ctor, template, bindEvents, bindRefs, bootstrapNested,
// portals, restoreFocus, layoutEffects, total
}
}
}
| Phase | What it measures |
|---|---|
script |
Running the component's script (your hooks and top-level code). |
compile / template |
Compiling the template once, then producing markup each render. |
domDiff |
Patching the live DOM. High here with a low 'template' means the output really changed a lot. |
bindEvents / bindRefs |
Attaching on* handlers and pp-ref targets. |
bootstrapNested |
Mounting and updating child components. |
layoutEffects / effects |
Your pp.layoutEffect / pp.effect callbacks. |
restoreFocus / portals |
Putting focus and selection back; moving portaled nodes. |
Console messages
The runtime prefixes its own messages with [PP-WARN] or [PP-ERROR], so you can
filter the console on PP-. Most warnings are logged once per element or component.
| Message | Meaning and fix |
|---|---|
[PP-WARN] <input#x> changed from uncontrolled to controlled |
State bound to value/checked started undefined. Initialize it. |
[PP-WARN] Hook order changed for component … |
A hook call sits inside an if, a loop or an early return. Hooks must run in the same order every render. |
[PP-WARN] … dependencies … must be an array or omitted |
A dependency list was passed as a non-array (often a single value). |
[PP-WARN] … returned a Promise |
An async function was passed to pp.effect. Start the async work inside a synchronous effect. |
[PP-WARN] Duplicate key values detected: … |
Two pp-for rows share a key. Keyed reuse is switched off for that render; use a unique id. |
[PP-WARN] Invalid template child of type … |
An object or function was interpolated as text. Render a field of it, or a string. |
[PP-WARN] pp-for collection is not an array or iterable |
The loop expression evaluated to something that cannot be iterated (often undefined before data loads). Default it to []. |
[PP-WARN] Dynamic pp-component values are not supported |
A boundary id contained {…}. Ids are static; pass changing values as props. |
[PP-WARN] Could not resolve pp-ref=… |
The ref name is not declared in the owning component's script. |
[PP-WARN] Synchronous rerender limit exceeded … |
A render or layout effect keeps setting state (more than 25 synchronous re-renders). Break the loop with deps or a guard. |
[PP-WARN] Loop row reuse/patching was abandoned … |
The runtime found an inconsistency, gave up its row cache for that component and re-rendered from full markup. Output stays correct but slower. Report it with a reproduction. |
[PP-ERROR] Template Expression Failed |
A {…} expression threw (usually reading a property of undefined). Guard it with ?. |
[PP-ERROR] Handler failed |
An on* handler threw. Error boundaries do not catch handlers; add try/catch. |
[PP-ERROR] Render Cycle Failed |
A render threw and no pp.errorBoundary() above it caught the error. |
[PP-ERROR] Compilation Failed / AstParser Failed |
The template or script is not valid. Check for JSX syntax and unquoted brace attributes. |
Silent failures
| Symptom | Cause |
|---|---|
The whole component is blank, no console error |
Invalid HTML in the template, almost always an unquoted brace attribute (class={x}). Quote it. |
The page stays invisible |
pp.mount() never ran (wrong script path, a module error) or a <body style="opacity: 0"> cloak was paired with bootstrap(). See Plain HTML. |
A binding shows nothing |
true, false, null, undefined and "" render as nothing by design. Use a ternary for display text. |
A stray 0 appears |
{items.length && 'x'} renders 0. Use {items.length ? 'x' : ''}. |
A prop is undefined in the child |
The attribute was not on the rendered child boundary, or its name is class/for (reserved words are dropped). |
ReferenceError from a handler in slot content |
Slot content runs in the scope of the template that wrote it. Move the function into that template's script. |