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:
- Average page weight: 4.2MB
- Average HTTP requests per page: 87
- Average Time to Interactive on 3G: 12.4 seconds
- Average JavaScript bundle size: 780KB (uncompressed)
- Worst offender: 14.3MB for a single game page, 214 requests
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:
- Cache efficiency: The parent page CSS and JS are shared across all pages. After visiting the homepage, loading any game page only requires fetching the game-specific HTML.
- No render-blocking resources: Since each game is self-contained, there are no external stylesheets or scripts to block rendering.
- Offline capability: Once a game is loaded, it stays in the browser cache. If someone plays Snake Arena, switches to Tower Stacker, then switches back, the second load is instant.
- Portability: Each game file can be opened directly in a browser and works perfectly. No web server required.
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:
- SVGs are resolution-independent. They look sharp on a 1x display and a 3x retina display.
- Inline SVGs add zero HTTP requests.
- SVGs compress extremely well with gzip since they're just text.
- The favicon and OG image are the same file, just rendered at different sizes.
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:
- Homepage: 8.9KB HTML + 10.7KB CSS + 1.4KB JS = 21KB total. Time to Interactive on 3G: 1.8 seconds.
- Game page: ~4KB HTML + cached CSS/JS + ~12KB game file. Time to Interactive on 3G: 1.3 seconds (assuming CSS/JS cached from homepage).
- Total HTTP requests for cold load: 3 (HTML, CSS, JS). For game pages after homepage visit: 2 (HTML page, game HTML).
- Lighthouse Performance score: 99 on desktop, 96 on mobile.
- LCP (Largest Contentful Paint): 1.1 seconds on 3G.
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.