Back to Insights
Web Development

I Built 22 Browser Games With No Framework and No Build Step

Campus Dude is 22 games that run instantly with no login and no install. Choosing vanilla JavaScript and HTML5 Canvas over a framework was the decision that made every other constraint achievable.

Muhammad Taki Ahmed
Muhammad Taki AhmedFounder & Software Developer at Raydrim
August 6, 2026
7 min read
I Built 22 Browser Games With No Framework and No Build Step

The constraint that shaped everything

Campus Dude exists for a specific moment: a student has ten minutes between classes, on campus wifi, on a phone or a shared library desktop. They want to play something. They do not want to make an account, install anything, or wait.

That is the whole brief, and it turns out to be a demanding one. Ten minutes means the game has to be playable in under three seconds from tapping the link. Shared library desktop means no installs and no assumptions about the machine. Campus wifi means the payload has to be small. And "does not want to make an account" rules out most of the conventional scaffolding around a games site.

Every technical decision below falls out of those four sentences.

Why no framework

My day job is Next.js. My instinct was to reach for it here too. I did not, and the reason is the three-second budget.

A framework-based game page has to ship the framework runtime, hydrate, and then start the game. For a content site that cost is well worth paying — you get routing, data fetching, and a component model. For a Canvas game, almost none of that is useful. The game is a single <canvas> element and a loop. Everything the framework provides sits between the user and the thing they came for.

So each game is a plain HTML document, a stylesheet, and a JavaScript file. No bundler, no transpiler, no build step. The browser parses the HTML, runs the script, and the game starts. Deployment is copying files.

The unglamorous benefit is that this has not broken once. There is no dependency tree to audit, no build that can fail, no framework major version to migrate across. A game I wrote in week one still runs identically today because nothing underneath it moved.

Shared styling comes from CSS custom properties in a single file each game imports:

:root {
  --bg: #f4f1ec;
  --ink: #16150f;
  --accent: #ff4d2e;
  --radius: 12px;
  --font-display: 'Space Grotesk', system-ui, sans-serif;
}

That gives twenty-two games one visual identity without a component library. When I changed the accent colour, I changed one line.

One game loop, twenty-two games

The games differ enormously — Carrom Board is physics on a board, Neon Rush is an endless runner, Campus 2048 is a grid puzzle. What they share is the loop, and I wrote it once:

function createLoop(update, render) {
  let last = 0;
  let rafId = null;

  function frame(now) {
    const dt = Math.min((now - last) / 1000, 0.05); // clamp tab-switch spikes
    last = now;
    update(dt);
    render();
    rafId = requestAnimationFrame(frame);
  }

  return {
    start() { last = performance.now(); rafId = requestAnimationFrame(frame); },
    stop() { cancelAnimationFrame(rafId); },
  };
}

Two details in there earned their place the hard way. Passing delta time to update rather than assuming a fixed step means the game runs at the same speed on a 60Hz laptop and a 120Hz phone. And clamping dt to 50ms stops the physics exploding when someone switches tabs for a minute and requestAnimationFrame resumes with an enormous gap — without the clamp, a carrom striker teleports through the board.

Pairing stop() with the Page Visibility API also stops a backgrounded tab burning battery, which matters when the audience is on phones all day.

Scores without accounts

No login was a product decision, not a technical one, but it forced a technical answer: where do scores live?

They live in localStorage, keyed per game. A player gets a generated handle on first visit — Player_2185 and similar — which persists in the same store. It is not an identity, it is a label on a high score.

The honest trade-off: clear your browser data and your scores are gone, and your scores do not follow you to another device. For a study-break games site that is an acceptable loss, and it buys something valuable — there is no account system, no password reset, no personal data to protect, and no signup screen between the link and the game.

Anything that reads storage is wrapped, because storage throws rather than returning null in private-browsing contexts and when a browser blocks site data:

function readScore(gameId) {
  try {
    return Number(localStorage.getItem('cd:score:' + gameId)) || 0;
  } catch {
    return 0; // private mode, blocked storage — just play without a high score
  }
}

What this approach costs

I am not going to pretend this is free. Three things are genuinely worse without a framework.

There is real duplication. Every game page repeats its own header markup and script tags. With twenty-two games, changing the site header means changing twenty-two files. I use a small script for that, which is a build step in everything but name.

State-heavy UI is tedious. The Canvas rendering is fine — you are drawing every frame anyway. But the surrounding UI, menus and settings and score panels, is manual DOM work that a component model would have made shorter.

No type safety. On a codebase this size I feel it. Refactoring a shared helper means grepping and hoping rather than letting the compiler find the callers.

Would I choose it again? For this site, yes. The constraint was time-to-playable on a bad connection, and nothing beats a document that starts working the moment it arrives. If Campus Dude grows features that need real state — accounts, multiplayer, a persistent profile — the calculation changes, and I will happily pay the framework cost then.

Pick the constraint that actually matters first. The stack falls out of it.

#HTML5 Canvas#JavaScript#Performance#Game Dev
Share this article
Muhammad Taki Ahmed

Written by Muhammad Taki Ahmed

Founder & Software Developer at Raydrim

Muhammad Taki Ahmed is a full-stack developer based in Dhaka, Bangladesh. He builds production web applications with Next.js, React and TypeScript, and writes about what actually broke while shipping them.

Related Technical Insights