Chapter 1: Tooling for web games in 2026
tl;dr: Vite and an image optimizer are all you need.
Although tooling in the JS ecosystem moves fast, a small, well-chosen toolchain can hold up well over time. In this chapter you'll get a solid, reusable starting point with minimal tooling from which you can build just about any browser game. Our examples use Bun to install dependencies and run scripts, so if you do not have it installed, start there. Or if you already have a preferred runtime, alternatives like Node and Deno are fine substitutes.
The bundler
Vite is a bundler. It takes the code from your many source files and assembles them into a single "bundle", hence the name. In 2026, bundlers also do a whole lot more. Vite will also...
- Transpile TypeScript source code into JavaScript that the browser can run
- Run a local dev server with live reloading and hot module replacement
- Generate source maps for debugging
- Optimize assets for production builds
- Allow you to import assets and optionally inline them directly into your bundled code. Images, sound effects, wasm files, and fonts are much easier to work with when using Vite.
Scaffold the project
Scaffold out a minimal app by running the following command:
bun create vitePick the "vanilla" option and follow the rest of the prompts.
Your project should look roughly like:
Go ahead and delete everything in the src/ folder. Then re-add a placeholder main.ts for us to return to later. Put whatever you want in there for now.
console.log("it works!");Optional configuration tweaks
Update tsconfig.json
Base tsconfig
Vite scaffolds a sensible default tsconfig.json to tell the TypeScript compiler how to handle your code. These defaults are fine as-is, but I recommend one small update...
Add a path alias
Add a paths alias to your project's source root so that instead of import example from '../../../../something' you'll be able to simply write import example from '~/something'. This becomes more useful as your project grows, as it allows you to reorganize your files without messing up the import paths.
{"compilerOptions": {"target": "ES2024","module": "ESNext","lib": ["ESNext", "DOM", "DOM.Iterable"],"types": ["vite/client"],"skipLibCheck": true,"moduleResolution": "bundler","allowImportingTsExtensions": true,"moduleDetection": "force","noEmit": true,"strict": true,"noUncheckedIndexedAccess": true,"noPropertyAccessFromIndexSignature": true,"noImplicitOverride": true,"erasableSyntaxOnly": true,"noFallthroughCasesInSwitch": true,"noUncheckedSideEffectImports": true},"include": ["src"]}
Update vite.config.ts
Next, install vite-plugin-image-optimizer [GitHub]. This is the only additional dependency needed. The plugin will automatically compress any image assets added to your project source at build-time.
bun add vite-plugin-image-optimizerNow create or edit vite.config.ts in your project root:
Default config
Vite generates a minimal config to get you started.
Image optimizer
Add the image optimizer plugin you just installed. Import ViteImageOptimizer and pass it into the plugins array.
Source maps
Enable source maps [MDN] with sourcemap: true.
Why? (A philosophical detour on source maps)
One of the defining characteristics of the web is that you can read the full source code of any webpage you load by simply clicking "View Source" or "Inspect Element" in your browser.
However, modern websites and games use source code minification to reduce bundle sizes, which makes it difficult to learn anything interesting from the source directly.
See for yourself. The following minified code...
...was the compressed output of this source code:
// draw a health bar above the character
function drawHealthBar(ctx, x, y, health) {
const maxHealth = 100;
const barWidth = 48;
const barHeight = 6;
const healthPercent = health / maxHealth;
const width = barWidth * healthPercent;
// dark background
ctx.fillStyle = "#1e1e2e";
ctx.fillRect(x, y - 12, barWidth, barHeight);
// green when healthy, red when critical
const isCritical = healthPercent < 0.3;
ctx.fillStyle = isCritical ? "red" : "green";
ctx.fillRect(x, y - 12, width, barHeight);
}
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
let currentHealth = 50;
let x = 0;
let y = 16;
drawHealthBar(ctx, x, y, currentHealth);With source maps, you get the best of both worlds: players can load your game quickly thanks to the minified bundle, and curious game developers can read and learn from your source in their dev tools.
Minification and obfuscation won't stop people from reverse engineering your games, but they will stop curious minds from learning from you. So embrace the open ethos of the web and enable source maps for your games!
To make it personal for a moment: my own interest in building web games started when I clicked View Source on A Dark Room way back in 2014 and realized that building a game like this was maybe within reach. That game had readable, UNMINIFIED, familiar-looking source code--and very little of it! You could be responsible for sparking a similar moment for the visitor that clicks View Source on your game someday.
TSConfig paths
Enable tsconfigPaths so your path alias from the tsconfig.json works with Vite.
import { defineConfig } from "vite";export default defineConfig({});
Update package.json
Replace Vite's default dev command with bunx --bun vite so the dev server runs with Bun instead of Node. Why?
Default scripts
Vite's default dev script uses NodeJS instead of Bun as the runtime.
Use Bun for dev
Change dev to bunx --bun vite to make Vite run on Bun during development instead.
{"name": "my-game","private": true,"version": "0.0.0","type": "module","scripts": {"dev": "vite","build": "vite build","preview": "vite preview"},"devDependencies": {"typescript": "...","vite": "..."}}
Start the dev server
First, start your local dev server so you can see changes live as you make them:
bun devThen open the URL displayed in your terminal, which will probably be like http://localhost:5173. You'll just see a blank page for now. If you open the browser console, you should see the log you added in main.ts.
Add some starter code
Update main.ts
Return to main.ts
Start from your placeholder:
Create and mount a canvas
Create a new canvas element and attach it to document.body.
Measure the canvas
Read the width and height of the canvas so we can fill it. getBoundingClientRect returns an object with many position-related properties.
Paint the background
Fill the canvas with a blue rectangle. We'll cover this more in Chapter 2.
console.log("it works!");
You should see a blue rectangle in the top-left corner. That's the canvas at its default size. The last step is to make it fill the window.
Update index.html
Base
Start from Vite's generated HTML.
Add a canvas style block
Add an inline <style>. This stretches the canvas to fill available space and ensures there won't be scrollbars.
<!doctype html><html lang="en"><head><meta charset="utf-8" /><link rel="icon" /><meta name="viewport" /><title>Your Game</title></head><body><script type="module" src="/src/main.ts"></script></body></html>
If you see a full-page blue rectangle, you have a working starting point for just about any web game. Before moving on, initialize your git repository if you haven't already. If you're new to version control, this beginner's guide to git is a good place to start.
Up next
We'll cover painting to the canvas using CanvasRenderingContext2D in chapter 2 →.