Ten open-source games, 48 MB: what self-hosting third-party games actually costs

I vendored ten MIT/public-domain HTML5 games into this static site — 48.4 MB in total. The work was not copying files; it was licences, size, and the fact that all of them loaded things from CDNs.

Every game on this site used to be mine: one directory, one meta.ts, one Vue island, all logic testable. This time I took a different route — ten open-source games written by other people, vendored as-is: not split up, not rewritten, not merged into the bundle.

The result is 48.4 MB across ten directories under public/games-vendor/. This post is about the cost, not the result.

Size: two games eat two thirds

Game Size Licence
HexGL 16.36 MB MIT
Server Survival 15.33 MB MIT
Pool 4.62 MB MIT
Idle RPG 3.52 MB MIT
Flexbox Froggy 2.46 MB MIT
Bomber Girl 2.19 MB MIT
Tower Builder 1.83 MB MIT
Astray 1.55 MB Unlicense
Star Battle 0.50 MB MIT
Match Three 0.06 MB MIT

The first two are 31.7 MB, 65% of the total. HexGL is mostly textures (two sets, textures/ and textures.full/, both referenced) and audio; Server Survival is mostly 11.5 MB of sound effects.

The first thing I cut was Pool’s 8.6 MB background track (Kevin MacLeod’s Bossa Antigua, CC BY). It was 65% of that game’s weight, and the share of static-site visitors who want background music is not worth 8.6 MB. The load call now points at a silent stub: play() does nothing, nothing throws, the game runs.

Size distribution across the ten games

Two WebGL/3D projects account for 65%; the other eight together weigh less.

Licences: three traps, none of them “no LICENSE file”

Trap one: the repo LICENSE and the file header disagree. Match Three ships an MIT LICENSE, while match3-example.js opens with a GPL-3.0-or-later header. When both are arguably valid, take the stricter one: I labelled it GPL-3.0-or-later, included the full GPL text as COPYING, and said so on the page. It runs in an iframe and never enters the site bundle, so it does not infect the site code — but labelling it wrong would still be infringement.

Trap two: MIT code, non-MIT art. Bomber Girl’s code is MIT; its sprites are CC BY-NC-SA 3.0 (non-commercial, attribution, share-alike). This site has no ads and no payments, so NC holds; attribution survives through the game’s own credits screen, and the page adds a line of its own. If this site ever monetises, that game has to go — which is why the constraint lives in a comment in the data file.

Trap three: music. Pool’s track was CC BY, so attribution would have sufficed, but I removed it anyway (size). Once removed, THIRD-PARTY.md has to say what was removed and why, or the next person assumes it never existed.

Together those become one rule: a LICENSE file in the repo does not mean everything in that directory is redistributable.

Engineering: three bugs I actually hit

One: .gitignore ate the entry script. Tower Builder’s runtime lives in dist/main.js, and the site’s .gitignore contains dist/. Everything worked locally and the deployed game was dead, because the file never entered git. Fixed by renaming the directory to app/, plus a self-test assertion that every vendored game’s entry page still contains a <script>.

Two: the analytics-stripping regex deleted the game’s own script. To guarantee “no third-party requests” I removed Google Tag Manager with a regex. It was greedy enough that [\s\S]*? crossed the whole document and took the game’s <script src> with it. The page rendered, the console was clean, and the game sat on its loading screen forever. Only an assertion catches “looks fine, completely dead” — the self-test now checks three things per game: entry page has a script, no third-party requests, no root-absolute paths.

Three: tsconfig was scanning public/. The vendored games bring minified jQuery and engine sources; astro check emitted hundreds of thousands of characters of warnings from them. "exclude": ["public"] cleans it up — static assets were never meant to be type-checked.

Zero third-party requests: localising every CDN

All of these games depended on external resources. Each dependency was replaced with a local file:

  • Flexbox Froggy: Google Fonts (three families, 16 woff2 files), jQuery, Animate.css — plus an AdSense slot, which was removed;
  • Idle RPG: the Material Icons font, and an off-site favicon pointing at the author’s GitHub Pages;
  • Server Survival: the Tailwind Play CDN build script and three.js r128 — hard dependencies; without them the page is a white screen;
  • HexGL: Google Analytics, and a favicon on the author’s domain.

About twenty files, each recorded with source and licence in THIRD-PARTY.md. What it buys: open any game and the Network panel shows no cross-origin request at all.

The boundary: iframe, not bundle

The site’s own games use “static shell + createApp” and share the build pipeline. Vendored games cannot:

  • they bring their own globals, their own window.onload, their own canvas lifecycle; inside a Vue island they fight each other;
  • the licences require keeping the code intact with its notices;
  • and by size it is the right call — people who never open a game download zero bytes of it.

So the play page is an iframe shell: header, footer, theme and language switching all stay, and the game loads as-is from /games-vendor/<slug>/. The price is that keyboard events, fullscreen and audio autoplay inside the frame each need handling, especially on mobile.

One judgement

The real cost of self-hosting third-party games is not copying files. It is the licence boundary, the size trade-off, and re-plumbing someone else’s runtime to work offline. The first two are one-off judgements; the third is dirty work repeated per game. What makes it worthwhile is that afterwards they share one list, one search box, one pagination rule and one attribution pattern with the games I wrote myself.

← Back to all posts

Comments

…