I have a document on my laptop called "things I want to understand eventually." It has been accumulating for three years. WebGL. Service Workers. The actual mechanics of TCP handshakes. How Canvas animation compares to CSS transforms under the hood. How a chess engine decides which moves to consider.
The items on this list tend to stay there unless I build something with them. Reading about how WebSockets work gives me a conceptual model that feels solid until someone asks me a specific question and I realize it's mostly vibes. Building a small real-time game where two players can see each other's cursors move — that gives me a different kind of knowledge. The kind that stays.
The word game
I wanted to understand WebSockets. Not just 'a persistent two-way connection' — I wanted to know: what happens to the connection when a tab goes to the background? What happens on a mobile phone when the screen locks? What's a reasonable heartbeat interval? How do you handle the case where one player disconnects mid-game?
I built a simple word-guessing game. Two players. One picks a word, the other guesses letters one at a time, classic hangman mechanics. No UI library. Just a Node.js WebSocket server and a plain HTML/JS frontend. The entire thing was maybe 300 lines of server code and 200 lines of client code.
// Server pings every 25s, client must pong within 5s
// If not, we close the connection and notify the other player
const PING_INTERVAL = 25_000;
const PONG_TIMEOUT = 5_000;
function startHeartbeat(ws: WebSocket, playerId: string) {
let pongReceived = true;
const interval = setInterval(() => {
if (!pongReceived) {
ws.close(1001, "Heartbeat timeout");
notifyDisconnect(playerId);
clearInterval(interval);
return;
}
pongReceived = false;
ws.ping();
}, PING_INTERVAL);
ws.on("pong", () => { pongReceived = true; });
ws.on("close", () => clearInterval(interval));
}The heartbeat code above looks obvious in retrospect. At the time I wrote it I had to think hard about every line. Why 25 seconds? Because most mobile networks terminate idle connections around 30 seconds. Why 5 seconds for the pong timeout? Because that's long enough to survive a brief signal drop without declaring the player gone. I know these numbers because I broke them and fixed them, not because I read them in a blog post.
The canvas physics thing
I wanted to understand Canvas animation — specifically, whether requestAnimationFrame was as smooth as people said, and how it compared to CSS transitions for something with actual physics. So I built a simulation of balls bouncing around a box with elastic collisions. Nothing novel. This has been done ten thousand times. That's fine. I wasn't trying to ship a product. I was trying to understand something.
The thing I learned: Canvas is great for things where you're drawing many small elements that change every frame. It is not great for static UI elements that you animate occasionally. CSS wins there, decisively, because the browser can do that on the compositor thread without touching the main thread. Putting a sliding panel animation in Canvas is like using a chainsaw to slice bread. The tool is real. The use is wrong.
What the pattern taught me
I don't finish most of these projects. The word game has no leaderboard, no authentication, no database. The physics simulation has no level design. That's not the point. The point is to push against something real — a constraint, a failure case, a browser quirk — and have to solve it with actual code. The solutions become vocabulary I use for years.
The projects that taught me the most were the ones I built because I wanted to know something, not because I wanted to have built something. Curiosity is a better teacher than ambition.
The playground section of this site is an extension of that habit. Some of those experiments are old. Some are embarrassing. All of them taught me something I still use.