| title | WebviewJS Event Loop: Non-Blocking GUI Pump Explained |
|---|---|
| sidebarTitle | Event Loop |
| description | Learn how WebviewJS integrates with Node's event loop using non-blocking event pumping, and how to control pump frequency, readiness, and shutdown. |
Every native GUI toolkit needs to drive its own event loop — a tight loop that continuously drains the OS message queue to process redraws, input events, and window signals. Node.js also has its own event loop for async I/O and timers. Traditionally these two loops are incompatible: you can only block on one at a time.
WebviewJS solves this by using tao's non-blocking pump_events() primitive. Instead of handing full control to the native event loop, it drains the OS message queue in a single call that returns immediately, then hands control back to Node. By scheduling that call inside a regular setInterval, both loops run cooperatively on the same thread — the GUI stays responsive and all of Node's async I/O continues working normally.
app.run() is a small JavaScript wrapper that sets up a setInterval targeting app.pumpEvents() at a configurable interval (default 16 ms, roughly 60 FPS):
// This is the conceptual equivalent of what app.run() does
const timer = setInterval(() => {
if (!app.pumpEvents()) {
clearInterval(timer);
}
}, 16);pumpEvents() drains all pending OS events without waiting, then returns:
true— the application is still alive; keep pumping.false— the application should exit; the interval is cleared.
Because the pump runs inside a Node timer, you keep full access to fetch, fs.promises, child_process, WebSockets, and every other Node API while the window is open.
You have three ways to start the pump, depending on how much control you need:
```js app.whenReady() — recommended import { Application } from '@webviewjs/webview';const app = new Application();
// Starts the pump automatically and resolves after the first native "resumed" event await app.whenReady();
const win = app.createBrowserWindow({ title: 'Ready', width: 1024, height: 768 }); win.createWebview({ url: 'https://example.com' });
```js app.run() — non-blocking, manual
import { Application } from '@webviewjs/webview';
const app = new Application();
// Start the pump first, then create windows
app.run({ interval: 16, ref: true });
const win = app.createBrowserWindow({ title: 'Manual run', width: 1024, height: 768 });
win.createWebview({ url: 'https://example.com' });
import { Application } from '@webviewjs/webview';
const app = new Application();
const win = app.createBrowserWindow({ title: 'Sync', width: 1024, height: 768 });
win.createWebview({ url: 'https://example.com' });
// Hands control to Tao's native loop; no other JS runs until the app exits
app.runSync();app.whenReady() resolves after tao emits its first native Resumed event, which signals that the application has been granted its window by the OS. Use it as the canonical entry point for creating windows and webviews:
app.whenReady().then(() => {
const win = app.createBrowserWindow({ title: 'Hello', width: 800, height: 600 });
win.createWebview({ url: 'https://nodejs.org' });
});Or with async/await:
await app.whenReady();
const win = app.createBrowserWindow({ title: 'Hello', width: 800, height: 600 });
win.createWebview({ url: 'https://nodejs.org' });Pass an options object to configure the pump that whenReady() starts internally:
await app.whenReady({
interval: 16, // milliseconds between pumps (default: 16)
ref: true, // keep the process alive while the timer is running (default: true)
});To wait for readiness without starting the pump automatically — for example, when you want to call app.run() yourself with different options — set autoRun: false:
const ready = app.whenReady({ autoRun: false });
// Start your own pump with custom settings
app.run({ interval: 33, ref: false });
// Await the readiness signal separately
await ready;The interval and ref options let you trade responsiveness for CPU usage:
| Option | Type | Default | Description |
|---|---|---|---|
interval |
number |
16 |
Milliseconds between pumpEvents() calls. 16 ms ≈ 60 FPS. |
ref |
boolean |
true |
When false, the timer is unref()'d — the process can exit naturally even while the pump is running. |
// 30 FPS — friendlier to battery and CPU for simple or mostly-idle UIs
app.run({ interval: 33 });// Unref'd timer — the process exits when all other async work finishes,
// regardless of whether the GUI is still open
app.run({ interval: 16, ref: false });// Combine with whenReady for a 30 FPS unref'd pump
await app.whenReady({ interval: 33, ref: false });Two methods stop the event pump, with different levels of finality:
// Stop the interval but leave all windows open and native resources intact.
// You can restart pumping manually with app.run() afterwards.
app.stop();
// Stop the pump AND hide all windows AND mark the application as exited.
// Retained BrowserWindow / Webview wrappers subsequently report isDisposed() === true.
app.exit();stop() without exit() is useful when you want to take over the loop yourself temporarily:
app.stop();
// Drive a tight custom loop — for example, a CPU-intensive rendering burst
while (framesToProcess-- > 0) {
app.pumpEvents();
}
// Hand control back to the interval-based pump
app.run();Listen for application-close-requested to run cleanup code before the process exits:
app.on('application-close-requested', () => {
console.log('All windows closed — cleaning up.');
// flush logs, save state, etc.
app.exit();
});Without calling app.exit() in this handler, the pump timer keeps the process alive even after the last window is closed.