Anything remotely intensive in Node.js that needs visuals: tracing, telemetry, profiling, you basically have no choice but to depend on the browser.
And the browser, while great, idles at around ~2GB of memory minimum.
Not to mention constant alt-tabbing.
But why?
Node.js is just as much C++ as the next browser. There’s nothing stopping it from rendering things.
I remember trying to profile a real-time system once, it was awful. Constant context switching to the browser, memory exploding, losing flow. It felt wrong.
So I built a software renderer, you know the thing responsible for everything visual you see in the digital world, from UIs to games.
It’s backed by raylib, C++, and shaders and it’s fast.
For example, this animated noise demo:
800 × 600 = 480,000 pixels
Per pixel: 2 trig calls
That’s ~960,000 trig calls per frame <- very expensive
Unoptimized, it still clocks around 90 FPS.
Threaded, it jumps to about 200 FPS.
I’m also working on my own real-time profiler, shard.js, using the same renderer
All of this is possible because of one simple idea:
the framebuffer.
Once you understand that, everything clicks.
If you take a screen:
_______________
| |
| |
| | <- screen (it’s 2D)
| |
|_____________|
and divide it into rows and columns:
|------------|
|------------|
|------------|
|------------|
^
|
imagine columns here
then unwrap it into rows only:
row 1 row 2 .... row n
-------- -------- --------
-------- -------- --------
You get an array.
But not a typical array.
It’s an array of bytes, contiguous memory; a raw chunk of memory called a buffer.
In Node.js, it looks like this:
const buf = Buffer.alloc(1024); // <- raw memory
Now take that buffer and fill it with numbers between 0 and 255, using this simple formula:
(y * width + x) * 4; // 4 channels: r, g, b, a
This maps screen coordinates (x, y) - with (0, 0) being the top-left of the screen, directly to this linear memory buffer.
What you get are pixels:
[ R, G, B, A, R, G, B, A, R, G, B, A, ... ] // Linear array
˄ ˄ ˄
Pixel 0 Pixel 1 Pixel 2
(x=0,y=0) (x=1,y=0) (x=2,y=0)
Pixels give you an image.
Do that 60 times per second (FPS), or more, and you get an animated image.
That buffer is the framebuffer.
If you understand that, you now understand graphics programming 101.
So what does this have to do with Node.js and my renderer?
Thinking About Node.js
Here’s a rough mental model for Node.js:
average Node.js usage -> via JavaScript
-------------------------------------
| |
v8 / bytecode
whole world of C++
So the question becomes:
What if I put a buffer between the world of C++ and JavaScript?
Because that’s literally all you need to paint the screen.
This isn’t new, it’s a classic systems technique: shared memory.
And Node.js gives us exactly that:
const buf = new SharedArrayBuffer(size);
Threads use this all the time for zero-copy access. It’s blazingly fast, but also easy to mess up. Shared memory bugs can get nasty fast.
So I built a controlled version of that idea.
This is roughly how the core of tessera.js works.
How Tessera Works
import { PixelBuffer, loadRenderer } from "tessera.js"; // raw memory
const { Renderer, FULLSCREEN, RESIZABLE } = loadRenderer();
const renderer = new Renderer();
renderer.initialize(1920, 910, "Renderer");
const canvas = new PixelBuffer(renderer, 1200, 800);
Under the hood:
- C++ creates two buffers (double buffering): a read buffer and a write buffer
- The read buffer is what’s currently on screen
- C++ creates a GPU texture and links it to that buffer
- JavaScript requests the current read buffer (raw memory, insanely fast)
From there, JavaScript just writes pixels:
const data = canvas.data;
const tracker = new DirtyRegionTracker(canvas);
// random star-like noise (direct memory access)
for (let i = 0; i < 2000; i++) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
const brightness = 100 + Math.random() * 155;
const idx = (y * width + x) * 4;
data[idx] = brightness;
data[idx + 1] = brightness;
data[idx + 2] = brightness;
data[idx + 3] = 255;
tracker.mark(x, y);
}
Then you notify C++ only about what changed:
tracker.flush(); // send dirty regions only
And issue one cheap command to update the GPU texture:
canvas.upload(); // very cheap
The screen updates.
While JavaScript is mutating memory, C++ keeps rendering the last valid texture.
Both worlds run in tandem.
What makes this powerful is raw memory.
Raw memory is absurdly fast in Node.js. And because canvas.data is a SharedArrayBuffer, we can write to it from multiple threads.
canvas.data // SharedArrayBuffer
That’s how the animated noise example, which is insanely expensive, jumps from ~80 FPS to ~200 FPS when threaded.
You can check out the tessera.js repo for more examples and docs.
Below is a full, minimal demo you can run locally.
Demo
npm i tessera.js
Your js file:
import { loadRenderer, PixelBuffer, DirtyRegionTracker } from "tessera.js";
const { Renderer, RESIZABLE } = loadRenderer();
const renderer = new Renderer();
if (!renderer.initialize(1920, 910, "Renderer")) {
console.error("Failed to initialize renderer");
process.exit(1);
}
renderer.setWindowState(RESIZABLE);
renderer.targetFPS = 60;
const canvas = new PixelBuffer(renderer, 650, 400);
canvas.clear(1, 1, 1, 255);
const data = canvas.data;
const width = canvas.width;
const height = canvas.height;
const tracker = new DirtyRegionTracker(canvas);
// random star noise
for (let i = 0; i < 2000; i++) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
const brightness = 100 + Math.random() * 155;
const idx = (y * width + x) * 4;
data[idx] = brightness;
data[idx + 1] = brightness;
data[idx + 2] = brightness;
data[idx + 3] = 255;
tracker.mark(x, y);
}
tracker.flush();
canvas.upload();
renderer.onRender(() => {
renderer.clear({ r: 0, g: 0, b: 0, a: 50 });
canvas.draw(0, 0);
renderer.drawText(
`FPS: ${renderer.FPS} | Buffer: ${canvas.width}x${canvas.height} | Memory: ${(canvas.width * canvas.height * 4 / 1024).toFixed(1)}KB`,
{ x: 20, y: 750 },
16,
{ r: 1, g: 1, b: 1, a: 1 }
);
});
function Loop() {
renderer.input.GetInput();
if (renderer.step()) {
setImmediate(Loop);
} else {
renderer.shutdown();
}
}
Loop();
process.on("SIGINT", () => {
console.log("\nshutting down gracefully...");
renderer.shutdown();
process.exit(0);
});
Here's an optimized version that maintains your casual, direct tone:
More from me:
Visualizing Evolutionary Algorithms in Node.js
Thanks for reading!
Find me here:



Top comments (0)