Before: 14 requests, 480KB After: 3 requests, 28KB Reducing page weight by 94%
Our game pages went from 480KB to 28KB total weight

When Sam and I started Gerk Games, I made one non-negotiable rule: no game page can take more than two seconds to become interactive on a throttled 3G connection. Not "two seconds on my office WiFi," not "two seconds in Lighthouse on a desktop simulation." Two seconds on an actual slow connection, measured from the moment a user taps the link to the moment they can play.

This article is the technical breakdown of how we got there. No frameworks, no CDN tricks, no expensive infrastructure. Just disciplined decisions about what actually needs to be in the browser.

The Baseline Problem

Before building anything, I benchmarked 15 popular browser game sites — the kind that show up on page one of Google for "free online games." The numbers were terrible:

These sites are loading more JavaScript to display a game grid than the actual game code they're embedding. The irony is that most of the games themselves — usually iframed from third-party portals — are smaller than the wrapper page that contains them.

I set our target: every game page under 50KB total (HTML + CSS + JS + inline assets), every game under 15KB. This forces every byte to justify its existence.

Decision 1: No Frameworks

The single biggest decision was also the simplest: no React, no Vue, no Svelte, no jQuery, no Bootstrap, no Tailwind. Not even a lightweight alternative. Just vanilla HTML, CSS, and JavaScript.

I know this sounds extreme. The conventional wisdom is that frameworks improve developer experience, and for complex applications, that's true. But a game grid page isn't a complex application. It's a list of cards with links. You don't need a virtual DOM to render 30 game thumbnails.

The cost of a framework isn't just the bundle size. It's the runtime overhead — the hydration step, the event delegation system, the state management layer. On a slow device, React's initial render before hydration can take hundreds of milliseconds, during which the page is visible but completely non-interactive. That's the worst possible user experience: the page looks ready but isn't.

Our total JavaScript — shared across the entire site — is 1.4KB. That includes the mobile menu toggle, the search filter, and a console.log Easter egg. Everything else is static HTML.

Decision 2: CSS as a Single File, No Build Step

Our CSS is one file: style.css, 10.7KB uncompressed, about 3.2KB after gzip. It covers the entire site — homepage, game pages, blog, static pages, mobile responsive, dark theme, print stylesheet, and reduced-motion preferences.

I wrote it by hand over two afternoons. No preprocessor, no PostCSS, no autoprefixer. Modern browsers don't need vendor prefixes for the properties we use. The CSS is organized by component (header, hero, game grid, blog cards, footer) with mobile breakpoints at the bottom. It's structured so that the critical rendering path CSS — the styles needed for the first paint — all live in the first 2KB of the file.

The build "pipeline" is a 40-line Python script that reads all HTML files, inlines the CSS into a style tag for production, and outputs minified files. No Webpack config that's 200 lines long. No node_modules directory with 40,000 files. No dependency that will break when someone runs npm install three years from now.

Decision 3: Games as Self-Contained HTML Files

Each game is a single HTML file with embedded CSS and JavaScript. No separate .js files, no external stylesheets, no image assets. When you load Snake Arena, the browser makes exactly one HTTP request for the game itself — everything else (the parent page, the shared CSS) was already cached from the homepage.

This architecture has several advantages:

The games use the HTML5 Canvas API exclusively. We don't touch the DOM during gameplay — no innerHTML manipulation, no style recalculation triggers, no layout thrashing. The Canvas is a single draw surface, and all game rendering happens there. This keeps the rendering pipeline predictable and avoids the performance cliffs that come from mixing DOM and Canvas operations.

Decision 4: System Fonts Only

No Google Fonts. No Typekit. No custom web font downloads. Our font stack is:

'Segoe UI', system-ui, -apple-system, sans-serif

This means zero font downloads, zero render-blocking CSS requests, and zero flash-of-unstyled-text. The text renders immediately with whatever the user's operating system considers its best sans-serif font. On Windows, that's Segoe UI. On macOS, it's SF Pro. On Android, it's Roboto. Each user sees a font that was designed specifically for their platform.

The performance gain is substantial. A typical Google Fonts setup with two weights adds about 40-60KB of font data and requires an additional CSS request that blocks text rendering until it completes. On a slow connection, that's 1-2 seconds of invisible text. Eliminating it was the single largest latency improvement we made.

Decision 5: Inline SVGs for All Graphics

We don't serve a single PNG, JPEG, or WebP image. Every visual element — the logo, the favicon, the Open Graph image, the blog illustrations — is an inline SVG. This has multiple benefits:

For the game thumbnails on the homepage grid, we use emoji characters rendered at large sizes. A single emoji character weighs essentially zero bytes but provides an instantly recognizable visual anchor for each game card. It's not fancy, but it's fast, and it gives each game a distinct visual identity without any asset loading.

The Results

After applying all of these decisions, here's where we landed:

We didn't use a CDN beyond Cloudflare's default caching. We didn't implement service workers, code splitting, lazy loading, or any other advanced optimization technique. We just shipped less code.

The lesson isn't "frameworks are bad." It's "understand what your page actually needs, and don't ship anything else." For a game portal, what the page needs is: show a grid of games, let people click on them, make the games load fast. That's about 20KB of code. Everything beyond that is overhead you're paying for with your users' time.