Performance is not a feature you add at the end. It is a budget you spend from the first commit. At HKQS Games, every title we ship has to clear one hard gate before it goes live: time-to-interactive under three seconds on a Moto G-class device over simulated 4G. That number is not arbitrary — it is roughly the threshold at which a casual player decides whether to wait or to close the tab, and the difference between 2.8 seconds and 4.2 seconds shows up directly in our first-session completion data.
This article walks through the techniques we actually use to hit that budget on browser games like Smash Blocks and Puzzle Hex. It is deliberately concrete: if a technique is in here, it is in our build pipeline. Where useful, I will link out to the canonical references, starting with web.dev's core performance guidance, which remains the single best starting point for anyone working on web-delivered games.
The 3-Second Rule: Why Speed Matters
The choice of three seconds as our budget is not arbitrary. It is rooted in research on user patience that goes back well over a decade. web.dev's performance guidance summarizes the modern version of the rule: every additional second of load time before the page becomes interactive produces a measurable, often compounding, drop in engagement. For e-commerce sites, that drop is well documented. For instant-play games, where the player has made no install investment and has no sunk-cost reason to wait, the drop is steeper.
Three seconds is roughly the threshold at which a casual player moves from "the game is loading" to "this is taking too long." Below three seconds, the wait feels like part of the experience — the player is already anticipating the first move. Above three seconds, the wait becomes a thing in itself, and the player starts to evaluate whether they want to be there. That evaluation is the moment we are trying to avoid.
Drop-off in first-session completion when load time rises from under 3s to over 5s, measured across our 2026 catalog. The relationship is non-linear: the curve is gentle up to about 3 seconds and steepens sharply after 4. Every second between 3 and 6 costs us roughly 15 to 18 percent of would-be players.
There is also a longer-term reason speed matters: trust. A player who waits six seconds for a game to load is not just a player who abandoned one session. They are a player who has formed a memory that this site is slow, and that memory will follow them the next time they see a link to one of our games. The cost of a slow load is paid not only in the session that left, but in the sessions that never start. Google's research on mobile page speed, covered in Think with Google's marketing research, makes the same point about the long shadow of a slow first impression.
Speed is not a feature. It is the absence of a tax on the player's first decision to play. Every millisecond above the budget is a millisecond spent telling the player they made the wrong choice. — Sari Wijaya, frontend engineering lead
- Under 1s: The load is imperceptible. The player feels the game was already running.
- 1-3s: The load is felt but accepted. The player is still anticipating the first move.
- 3-5s: The load becomes the experience. A measurable share of players begin to leave.
- 5-10s: The load is a problem. Most casual players will not wait through this for a game they have not committed to.
- 10s+: Effectively a non-starter for instant play. Even players who wait arrive frustrated.
Define the Budget Before You Write Code
The first mistake most teams make is treating load time as something to measure after the build is done. By then it is too late — the architecture has already locked in its costs. We invert the process. Before any game enters production, we agree on a performance budget that maps to the three-second target, and we break it down by phase.
DNS + connection + TTFB: 400ms · HTML + critical CSS: 300ms · JavaScript parse + execute: 1200ms · Initial asset decode (sprites, audio): 600ms · First renderable frame: 500ms. Every millisecond over any phase must be justified in review, or the build does not ship.
That breakdown is not theoretical. It is the contract between the design team and the engineering team. When a designer asks for a richer opening animation, we can point at the budget and ask which phase is going to give up its milliseconds. When an engineer proposes a new dependency, we can see immediately whether it fits inside the 1200ms JavaScript allocation. Budgets turn performance from an argument into an accounting exercise.
Cut the Initial JavaScript Payload
For most browser games, the single biggest lever is the size of the initial JavaScript bundle. Frameworks ship a lot of code that the first frame does not need. Our rule is brutal: nothing that is not on the critical path of the first playable moment is allowed in the initial bundle. Menus, settings, achievements, social features, secondary game modes — all of it loads after the player has already started playing.
The mechanism is route-level code splitting with dynamic imports. The opening screen and the core game loop are in the main chunk; everything else is fetched in the background once the game is running. Google's code splitting documentation covers the underlying patterns well; the key insight for games is that you can split along feature boundaries, not just page boundaries.
// Only the boot screen + core loop ships in the initial bundle.
// Everything else is fetched lazily once the player is in-game.
const boot = await import('./game/boot.js');
boot.mount(document.getElementById('root'));
// Kick off background fetches for non-critical features.
window.requestIdleCallback(() => {
import('./features/achievements.js');
import('./features/settings-panel.js');
import('./features/daily-challenge.js');
});
On Puzzle Hex, this pattern alone cut our initial JavaScript from 380 KB gzipped to 94 KB gzipped. The game became playable almost twice as fast, and the secondary features loaded silently during the first puzzle. Players never noticed the difference except in the form of a snappier opening.
Stream and Decode Assets in Parallel
The next biggest cost is asset decode. A puzzle game might ship dozens of sprite sheets, a handful of audio loops, and a font or two. If you wait for all of them before rendering, you have serialized the load. If you fetch and decode them in parallel with script execution, the cost mostly disappears into time the main thread was spending anyway.
The technique is straightforward: use decode() promises on images and Promise.all across the asset list, while letting the script bundle parse in parallel. Modern browsers will offload image decoding to a worker thread, which means the main thread is free to keep preparing the render. The web.dev guide on image decoding is the reference we point new engineers to.
// Fetch + decode in parallel; only await what the first frame needs.
async function loadCriticalAssets() {
const [board, pieces, font] = await Promise.all([
loadImage('/img/board.webp'),
loadImage('/img/pieces.webp'),
document.fonts.load('700 16px "Space Grotesk"'),
]);
return { board, pieces, font };
}
function loadImage(src) {
const img = new Image();
img.src = src;
return img.decode().then(() => img); // offloaded to worker
}
Preload the Next Move, Not Just the Next Asset
Once the first frame is up, the load-time story is not over — it just changes shape. The new question is: how do we keep the next interaction feeling instant? For puzzle games, this usually means prefetching the next level's data while the player solves the current one. For action games like Sky Thunder: Ace Strike, it means prefetching the next wave's asset set during a calm stretch of the current wave.
The key is to make prefetch opportunistic and interruptible. A prefetch that blocks the main thread is worse than no prefetch at all. We use requestIdleCallback with a timeout, and we always cancel pending prefetches if the player navigates unexpectedly. The pattern is small but it matters: prefetch is a guest on the main thread, never the host.
The fastest request is the one you never make. The second fastest is the one the player never has to wait for. Everything in our pipeline is some combination of those two ideas. — Sari Wijaya, frontend engineering lead
Technical Implementation: How We Achieve Sub-3s Loads
The budget breakdown above is a contract; this section is the architecture that fulfills it. The techniques we use fall into three broad categories: payload reduction, parallelism, and caching. Each is covered in isolation elsewhere in this article, but it is worth describing how they fit together, because the gains compound rather than add.
The first pillar — payload reduction — is the work of cutting what does not need to be on the critical path. That means code splitting at the route level, tree-shaking every dependency, and shipping assets in modern formats (WebP for raster images, WOFF2 for fonts, Brotli compression on text). The cumulative effect on Puzzle Hex was a reduction from 380 KB of gzipped initial JavaScript to 94 KB — a 75 percent cut that translated directly into about 700ms saved on a mid-range phone. The techniques are documented in Google's code splitting documentation and elsewhere; the discipline is in applying them everywhere, every build.
The second pillar — parallelism — is about not serializing work the browser can do at the same time. Image decode happens off the main thread. Font loading happens alongside script parse. The initial asset fetch overlaps with the connection handshake. The web.dev guide on parallel image decode is the canonical reference for the image side of this; the broader principle is that any work the browser can do without the main thread should be queued immediately, not awaited.
The third pillar — caching — is covered in detail in the CDN architecture section below. The short version: a player who has visited any of our games before has already downloaded shared assets (the font, the brand sprite sheet, common UI components) and those assets are served from the edge on subsequent visits. For first visits, the edge cache still helps because it eliminates origin round-trips.
Initial bundle: 94 KB gzipped (down from 380 KB). Asset decode parallelism: ~600ms saved on cold load. Edge cache hit rate: 87% for shared assets, 64% for game-specific assets. Origin requests per session: 4 (down from 19). Each number represents a decision, not an optimization afterthought.
The hardest part of frontend performance engineering is not knowing what to do. It is saying no to features that would push the budget over. The architecture is mostly a sequence of refusals. — Build pipeline review notes, HKQS Games
Measure on Real Devices, Not Your Laptop
It is easy to hit a three-second budget on a 2024 MacBook Pro over fiber. It is much harder on a three-year-old mid-range phone over a bus's shared 4G. If you only measure on developer hardware, you will ship a game that feels fast to you and slow to half your audience. We run every release candidate through a small device lab — a Moto G, a low-end Galaxy A-series, and an older iPhone SE — on simulated 4G with CPU throttling enabled.
Median time-to-interactive for Block Puzzle: Save Girl across real player sessions in July 2026, measured via the Core Web Vitals field collection. Lab numbers were 1.9s; the gap is the cost of real-world network jitter, and it is the number we actually optimize against.
Field data is humbling because it refuses to flatter you. A build that looks great in the lab can have a long tail of slow sessions on real devices, often driven by factors outside your control: a player on a congested train, a device with a hundred background tabs, a carrier injecting scripts. You cannot fix all of it, but you can make sure your own code is not the part that breaks the budget.
Performance Benchmarks: Before and After
It is one thing to describe the techniques; it is another to show what they actually bought us. The table below is a side-by-side comparison of four representative titles on our platform, measured on the same mid-range Android device (Moto G-class) over simulated 4G, before and after the performance work described in this article. All measurements are median time-to-interactive across 50 cold loads; the field numbers in production are slightly worse but track the same pattern.
The improvement ranges from 43 to 64 percent, and the games that improved most were not the ones that started worst — they were the ones whose original architecture had the most non-critical code on the critical path. Smash Blocks started with the heaviest bundle but had the most deferrable features; cutting them produced the largest relative gain. Puzzle: Water Sort started lighter and ended lightest, because the game itself has very little secondary feature surface.
Average increase in day-1 retention after the performance pass above was applied across the four titles. The causal chain is direct: faster load → higher first-session completion → higher probability the player returns the next day. Speed work is retention work, even though it lives in a different org chart.
It is also worth noting what did not improve. Average session length, once the game had loaded, did not change materially. Players did not play longer because the game loaded faster — they played at all because the game loaded faster. The performance work did not change the game; it changed who got to experience it. That is a meaningful distinction, because it tells us the work is never finished. There is no session-length payoff that lets us declare victory and stop measuring load. Statista's mobile connection data reminds us that median connection speeds are still rising, but the long tail of slow connections is not getting shorter as fast as the median is improving — which means the budget keeps getting harder, not easier, for the players at the bottom of the distribution.
What We Do Not Do
It is worth naming a few popular techniques we have tried and abandoned. Service worker caching, for example, helps a lot on the second visit but does nothing for the first session — and for an instant-play platform, the first session is the entire pitch. We use service workers, but we do not count them toward the three-second budget. Similarly, we have found that aggressive prerendering of likely-next-pages can backfire on low-end devices by competing for the main thread during the critical first second.
We also do not ship polyfills for browsers we do not support. Every polyfill is a tax on the modern browsers that make up the vast majority of our traffic. Targeting evergreen browsers lets us ship smaller bundles and lean on native APIs like Image.decode, requestIdleCallback, and native ES modules without apology.
CDN Architecture: Edge Caching Strategy
Everything in this article so far has been about what happens in the browser. But the three-second budget also depends on what happens before the browser receives a single byte — and that is the job of our content delivery network. The CDN's job, simply stated, is to make sure no player's first request has to travel to a single origin server on the other side of the planet.
Our edge strategy rests on three principles. First, every static asset is cached at the edge with a long max-age and an immutable cache-control header, so a returning player's browser can serve the asset from its local cache and a first-time player's request can be served from the nearest edge node. Second, shared assets — the brand font, the common UI sprite, the audio loop used across multiple games — are served from a single shared URL so that a player who has played one of our games has already downloaded most of the assets for the next one. Third, the HTML document itself is served with a short TTL and a stale-while-revalidate policy, so updates propagate within minutes without sacrificing the cache hit rate.
Of all asset requests in July 2026 were served from an edge node within 50ms of the player. The remaining 13 percent required an origin fetch, almost all of which were first-time visits to a brand-new game. The CDN is the silent partner of the three-second budget — none of the techniques in this article would matter if the network round-trip ate the time first.
Choosing edge locations is not a theoretical exercise. According to the global connection speed data tracked by Statista and the regional latency breakdowns visualized through ChartsBin, our player base is concentrated in regions where the median round-trip to a single origin would be 200 to 400ms — a significant fraction of the three-second budget before the browser has even begun to parse. Edge caching reduces that to typically under 40ms, which is the difference between the budget being achievable and the budget being a fantasy.
- Static assets: Edge-cached, immutable, long max-age. Includes all images, fonts, audio, and versioned JS bundles.
- HTML documents: Short TTL with stale-while-revalidate. Updates propagate within minutes.
- Game data (level sets, configs): Edge-cached with versioned URLs. New levels ship under new paths so they are instantly cacheable.
- Telemetry endpoints: Not cached. These are POST endpoints with low volume and write semantics.
The CDN is the part of the performance budget you cannot earn back by being clever in the browser. If the network eats 800 milliseconds before the first byte, no amount of code splitting will give you that time back. Edge caching is the precondition for everything else. — Sari Wijaya, frontend engineering lead
The Payoff
None of this is glamorous. The work is mostly subtraction — removing dependencies, deferring features, decoding in parallel, measuring on cheap devices. But the payoff is real and measurable. When a game loads in under three seconds, the player is already solving the first puzzle before they have finished deciding whether they wanted to be there. That is the entire advantage of instant play, and protecting it is the most important engineering work we do.
If you are building for the browser in 2026, the budget mindset is the single highest-leverage change you can make. Pick a target, break it into phases, and make every build defend its share. The techniques above will get you most of the way there. The discipline of the budget will get you the rest.