Native JavaScript & Web APIs
A component's <script> is ordinary JavaScript running in the page. It is not
a sandbox and not a compile target. So everything the browser can do is available
to it directly, including WebGPU, Web Workers, Web Audio, WebRTC and WebAssembly.
PulsePoint supplies what sits around those APIs: state, server calls, and updates
to exactly the DOM nodes that changed.
What native means here
-
The script runs in the page's own global scope.
window,document,navigatorand every other global are the same objects any script on the page sees. There is no wrapper layer between your code and the browser. - New browser APIs work the day browsers ship them. PulsePoint does not need to add support for an API before you can call it, because it never stands between you and the API.
- What you write is what runs. No build step transforms the script, so browser DevTools debug it directly, breakpoints included.
-
Markup stays plain HTML. A
<canvas>,<video>or<audio>element is written as itself and reached throughpp-ref.
Who does what
PulsePoint handles the data flow and the DOM; the browser API does the heavy work. The two meet in a ref and an effect:
| Job | Use | Why |
|---|---|---|
| Data from the server | pp.rpc, pp.socket, RPC streaming | Fetch, stream or push the values the browser API will work on. |
| Values the markup shows | pp.state | Counts, labels, status. A change re-renders only the nodes that differ. |
| Handles to browser objects | pp.ref | GPU devices, contexts, workers, audio graphs, streams. Changing a ref never re-renders. |
| Acquire, feed and release | pp.effect | Create the object on mount, push new state into it when it changes, and dispose of it in the cleanup. |
| The heavy work | The browser API | Shaders, threads, audio processing, codecs and hardware run at native speed. PulsePoint is not in that path. |
Live: WebGPU fed by pp.rpc
This chart is drawn by a WebGPU fragment shader. The first series is rendered by the
server into the page; each click asks the server for a new one through
pp.rpc. The result lands in state, an effect writes it to a GPU storage
buffer, and the GPU redraws every bar. The only markup that re-renders is the two
labels. In a browser without WebGPU, the same effect draws with Canvas 2D instead.
Renderer: {backendLabel}
{series.length} values from the server
{error}
The pattern behind it, without the WebGPU setup code:
<div pp-component="gpu_chart">
<canvas pp-ref="{canvas}"></canvas>
<button onclick="load()">Refresh</button>
<script>
const canvas = pp.ref(null);
const gpu = pp.ref(null); // device, buffers: never state
const [series, setSeries] = pp.state([]); // what the server sent
const [ready, setReady] = pp.state(false);
// 1. Acquire the GPU once, release it on unmount.
pp.effect(() => {
let cancelled = false;
initGpu(canvas.current).then((g) => {
if (cancelled) return g?.device.destroy();
gpu.current = g;
setReady(true);
});
return () => {
cancelled = true;
gpu.current?.device.destroy();
};
}, []);
// 2. Every new series: upload to a GPU buffer and draw.
pp.effect(() => {
if (ready) draw(gpu.current, series);
}, [series, ready]);
// 3. The server decides what to draw.
async function load() {
const { values } = await pp.rpc("gpu_series", { points: 64 });
setSeries(values);
}
// initGpu() and draw() are plain WebGPU: navigator.gpu, a pipeline,
// device.queue.writeBuffer(...), a render pass. Nothing PulsePoint-specific.
</script>
</div>
Rules
- Keep browser objects in pp.ref, not pp.state. State is for values the markup shows; storing a device or a context there re-renders for no visible change.
- Acquire in an effect with an empty dependency array and release in its cleanup. Cleanups must be synchronous, so start async setup inside the effect and guard it with a cancelled flag, as in the demo.
- Push data into the API from a second effect whose dependencies are the state it reads. That is the reactive bridge: server data lands in state, and the effect forwards it to the GPU, worker or audio graph.
- Run per-frame loops on requestAnimationFrame and keep per-frame values in refs. Calling a state setter every frame re-renders the component 60 times a second; set state only when something on screen should change.
- Feature-detect before you use a new API (if (!navigator.gpu) ...) and provide a fallback. WebGPU and most device APIs also require a secure context: HTTPS, or localhost in development.
- Static import/export and top-level await are not allowed in a component script, because it runs as a function body. Use import() inside an effect or an async function, or load the library with its own <script type="module"> and read the global it sets.
Animation loops
A continuous animation belongs to the browser's frame loop, not to PulsePoint's render cycle. State only starts and stops it:
const frame = pp.ref(0);
const [running, setRunning] = pp.state(true);
pp.effect(() => {
if (!running) return;
let id = requestAnimationFrame(function tick(t) {
frame.current += 1; // a ref: no re-render per frame
renderFrame(gpu.current, t); // the GPU does the per-frame work
id = requestAnimationFrame(tick);
});
return () => cancelAnimationFrame(id);
}, [running]);
Workers: server to thread to DOM
The same bridge works in the other direction. Here the server supplies a file, a worker processes it off the main thread, and its answer becomes state:
const worker = pp.ref(null);
const [result, setResult] = pp.state(null);
pp.effect(() => {
const w = new Worker("/js/parse-worker.js", { type: "module" });
w.onmessage = (event) => setResult(event.data); // worker -> state -> DOM
worker.current = w;
return () => w.terminate();
}, []);
async function analyze() {
const file = await pp.rpc("exportCsv"); // server -> worker
worker.current.postMessage(file);
}
Third-party libraries
Any library that runs in a browser runs in a component. Load it with import()
inside an effect, hand it the element from a ref, and destroy it in the cleanup:
pp.effect(() => {
let cancelled = false;
let chart = null;
// Static `import` is not allowed in a component script; `import()` is.
import("https://esm.sh/some-chart-library").then(({ Chart }) => {
if (cancelled) return;
chart = new Chart(host.current, { data: points });
});
return () => {
cancelled = true;
chart?.destroy();
};
}, []);
Other APIs, same pattern
| API | Typical use | Release in the effect cleanup |
|---|---|---|
| WebGPU | GPU rendering and compute: charts, simulations, ML inference | device.destroy() |
| Canvas 2D / WebGL | Drawing, charts, image processing | None for 2D; WEBGL_lose_context for WebGL |
| Web Workers / OffscreenCanvas | Parsing, number crunching or rendering off the main thread | worker.terminate() |
| WebAssembly | Native-speed modules compiled from Rust, C or Go | Release with the module's own API |
| Web Audio | Synthesis, effects, visualizers | audioContext.close() |
| Media capture / WebRTC | Camera, microphone, screen share, calls | track.stop(), peerConnection.close() |
| IndexedDB / Cache Storage | Offline data and assets | db.close() |
| Intersection / Resize / Mutation observers | Lazy loading, measuring, reacting to layout | observer.disconnect() |
| Web Serial / WebUSB / WebHID / Web Bluetooth | Talking to hardware from the page | port.close(), device.close() |
| Clipboard, Notifications, File System Access, Geolocation | One-off actions | Call them inside the event handler that needs the user gesture |
The hooks involved are documented in full in Ref, Effect and RPC, Errors & Uploads; for pushed data, see WebSockets.