Open the HTML5 game on most browser game portals in 2025, and your browser will download 4 to 12 megabytes of code before you can play. That includes the game engine (Phaser, PixiJS, Three.js), the ad SDK (often 1-2MB on its own), analytics libraries, font files, audio files in formats your browser will never use, and increasingly bloated UI framework overhead.

For a game whose actual logic is maybe 500 lines of code, this is absurd. The 2048 source code in its original 2014 release was 2,176 lines and ran fast. The version on a typical aggregator portal in 2025 weighs more than 50× that. None of the extra weight made the gameplay better.

On GerGame, we made a different decision: each game is its own self-contained HTML file with embedded CSS and JavaScript, no framework, no external runtime, no analytics inside the game frame. The result is that most games on the site weigh 5 to 30 kilobytes. The whole catalog of 23 games, fully cached, is under 1MB.

Here's how we got there, and why we think the constraint actually produced better games.

What we gave up

Before listing techniques, the honest accounting: we gave up real things to hit these sizes. We don't have:

No physics engine. Matter.js is 91KB minified. We didn't use it. Games that need physics (Breakout, Pong, Tower Stack) have hand-written 50-line physics functions that do exactly what each game needs and nothing else. The downside: if we wanted to add ricocheting projectiles or rope simulations, we'd be writing it from scratch.

No state management library. Redux, Zustand, anything like that is overkill for a single-screen game. We use plain JavaScript objects and closures. The downside: if a game's complexity grew significantly (say, into the 5000+ line range), our pattern would start to feel cramped.

No build pipeline. No webpack, no Vite, no TypeScript compilation. The files we ship are exactly what we write. The downside: we can't use modern ES features that need transpiling for older browsers (we use enough ES5 syntax to support 99%+ of browsers).

For us, these tradeoffs are correct because the games we're building are intentionally small. If you're building a sprawling RPG in the browser, you should absolutely use a framework. Use the right tool for the job size.

Technique 1: Render only when something changes

The default mental model for game development is the game loop: at 60fps, every 16.67ms, you re-render the entire scene. This is right for games where things are constantly moving — Pong, Breakout, Snake. It's wrong for games where most frames are identical — Minesweeper, Tic-Tac-Toe, Solitaire.

For static-state games, we don't run a game loop at all. We re-render only when state changes (a click, a key press, a timer tick). This means the CPU is idle 99% of the time the game is "running."

// In Minesweeper, after every click or flag:
function reveal(i) {
  /* mutate state */
  render();  // single re-render, no loop
}

Compare this to a Phaser-based equivalent, which would be running a 60fps loop the entire time someone is staring at a Minesweeper board not moving. The loop doesn't make Minesweeper faster — it makes it slower and burns battery.

Technique 2: requestAnimationFrame for actual animation

For games that do need continuous animation (Snake, Pong, Breakout, Maze Runner), we use requestAnimationFrame, not setInterval. RAF synchronizes with the browser's repaint cycle, so we're never doing work that won't be visible. On a 60Hz display, that's 16.67ms per frame; on a 120Hz display, it adapts automatically.

The pattern is the same in every game:

function tick() {
  updatePhysics();
  draw();
  requestAnimationFrame(tick);
}
tick();

This is 4 lines instead of the 200+ lines a typical "engine" would generate to do the same thing. The "engine" version might have features we don't need — multi-scene routing, sprite atlases, ECS architecture — but for our scope, we'd be paying for capabilities we'll never invoke.

Technique 3: Canvas for graphics, but only where it's needed

Many of our games don't use Canvas at all. Minesweeper, Solitaire, Hangman, Crossword, Memory Flip, 2048 — these are pure DOM. Cells are CSS-styled divs, manipulated by event handlers. The browser's rendering engine, which is heavily optimized, does the heavy lifting.

For games that need pixel-level control (Pong, Breakout, Maze Runner), we use a single Canvas element and a 2D context. We don't use WebGL — it's 10× more code for 1% better performance on these scales.

The decision rule: if your game can be expressed as "rectangles that update colors and positions on event," use DOM. If it needs sub-pixel motion or particle effects, use Canvas. WebGL is for 3D, large particle systems, or shader effects — none of which we currently need.

Technique 4: One file per game

Every game is a single index.html with embedded CSS and JavaScript. There are no separate .css, .js, or .json files to load. Why?

HTTP requests have overhead. Even with HTTP/2 multiplexing, a typical game requesting separate HTML + CSS + JS + audio files pays 100-300ms of round-trip time on a 4G connection. A single 30KB HTML file with everything inline arrives in one round trip — usually under 80ms.

The tradeoff: we can't cache CSS or JS across games. Each game has its own CSS embedded, which means roughly 3KB of duplicated style. We share one external stylesheet (game-shell.css) for the topbar and basic typography, which gets cached after the first game load. The rest is inlined for arrival speed.

Technique 5: localStorage instead of a backend

Every game saves high scores, preferences, and (where applicable) mid-game state to localStorage. There's no server, no account, no API call. The browser stores it locally; it never leaves the device.

Benefits: instant reads and writes. No latency. No server costs. Works offline. Respects privacy (we literally can't see your scores from our side). The data structure is simple key-value:

localStorage.setItem('gergame.snake.best', 47);
const best = parseInt(localStorage.getItem('gergame.snake.best') || '0');

Drawbacks: scores don't sync across devices. If you clear browser data, scores disappear. For our scope, that's fine — no one is going to demand cross-device leaderboard sync for our 60-second whack-a-mole. For other genres, this would be a real limitation.

Technique 6: Inline SVG for everything visual

Cover art, UI icons, decorative elements — all SVG, all inline in the HTML where used. No external image requests. SVG also compresses well with gzip (often 70-80% reduction) and scales perfectly at any resolution.

Each game's cover.svg averages about 1.2KB. A comparable PNG screenshot would be 30-80KB depending on dimensions and compression. The SVGs we hand-craft also look cleaner because they're not photographs of gameplay — they're stylized representations.

The numbers, audited honestly

Here's the actual size breakdown of our smallest and largest games:

Smallest: Reaction Test — 3.1KB HTML (gzipped: 1.4KB). Single function, single screen, no images. The entire game including instructions fits in less code than a single React component import.

Largest: Solitaire — 14.8KB HTML (gzipped: 4.2KB). 52-card deck logic, drag-to-move UI, undo system, foundation auto-complete, save/restore state. Still smaller than the loading screen of most mobile games.

Across the catalog: average 7.8KB per game HTML, 2.4KB gzipped. The 23 games combined are 180KB raw, 56KB after gzip.

The deeper point about constraints

When you have unlimited resources, you tend to add unnecessary features and complexity. When you have a 30KB budget, every line has to justify itself. The result, in our experience, is that games are forced to be more focused. There's no room for the daily-quest system, the 50-tier progression, the energy meter, the loot box. The game has to be the game.

This constraint also makes the games more maintainable. We can read the entire source for any of our games in 10 minutes. We can make changes confidently because there's nothing hidden in node_modules to break. Six months from now, when we want to fix a bug, we can.

The web's reputation as a "bloated platform" is mostly a function of how it's used, not what it is. Browsers in 2026 are remarkably capable. They can render complex graphics, play audio, save state, handle multiple input modes, and do it all reliably across hundreds of device configurations. The fact that we treat the web like a poor man's app store is a cultural choice, not a technical limitation.

If you want to see what the web can do with a strict budget, click any game on our catalog and time the load. It's almost always under a second. The browser is fast. We just have to stop sending it junk.