On October 11, 2026, a solo developer going by JPTER78 posted a link to pokesurvivor.com and went to bed. By the weekend it was in every group chat I have — not because it was marketed, but because it does the one thing a browser game has to do to spread: it opens in ten seconds, and then it eats your afternoon.

PokéSurvivor (usually searched as pokesurvivor) is a Vampire Survivors-shaped action roguelite with 966 Pokémon, four-player co-op, and zero install, ads or in-app purchases. You pick a partner, attacks fire on their own, and you spend the run doing the only thing that matters: positioning.

I'm not here to review it. I'm here because it's the cleanest public case study I've seen of shipping a real-time action game on the open web without a game engine — and the constraints it exposes are the ones you'll hit the moment your enemy count crosses a few hundred.

TL;DR

  • PokéSurvivor is a free browser survivor-like by JPTER78, launched Oct 11, 2026: 966 Pokémon, nine generations, 1–4 player co-op.
  • It's written in plain HTML, CSS and JavaScript — no Unity, no Godot, no Phaser. The source is public on GitHub.
  • The developer reports ~420 enemies per frame in about 3ms in Chrome. That's ~5x headroom against a 16.6ms budget, and it tells you almost everything about the architecture.
  • Three unglamorous techniques make it work: spatial partitioning, object pooling, and an allocation-free render path.
  • The hard parts weren't the horde. They were content volume and netcode — and one observable behaviour gives away how the netcode works.

What PokéSurvivor actually is

Strip the skin off and the loop is well understood:

  1. A personality test picks your partner — functionally a starter screen with a quiz stapled on.
  2. The map is endless. Enemies spawn in waves from off-screen, and some aren't just damage: reports describe support roles that heal, shield or summon.
  3. Attacks are automatic. You steer. That's the entire input surface, which is why it works one-thumbed on a phone.
  4. Every level-up is a three-card choice. Moves evolve at max level with the right upgrade card — Ember into Fire Blast, Bubble into Surf.
  5. Type matchups and weather are real systems. Rain buffs Water and nerfs Fire, and you learn it in your hands rather than in a wiki.
  6. Rifts pull you into one of 18 type-themed arenas for a legendary fight. Win, and you earn a Bond: two moves fused into one slot that fire together, stackable up to four.
  7. Co-op is four players, room codes, shared XP, and you revive a downed teammate by standing next to them.

Runs land at 10–30 minutes — the length that makes "one more" feel cheap and then costs you two hours. And the monetisation is why people sat up: gacha tickets drop from play and cannot be bought, coins only buy permanent upgrades, and donations cover server costs with no in-game advantage. In a genre where the free tier is usually a funnel, that's a deliberate choice.

The constraint nobody talks about: no engine

Plain HTML, CSS and JavaScript, no engine. Which means no scene graph, no sprite batcher, no physics broadphase, no asset pipeline, no profiler you didn't write yourself. Every one of those is now a problem you own.

It also means a few hundred kilobytes instead of a 40MB WebGL build — which is why it starts in ten seconds on a school Chromebook. That trade is the entire thesis of the project, and for this genre I think it's the right one.

Where the 3ms comes from, and why I believe it

The developer says the game handles roughly 420 enemies per frame in ~3ms in Chrome. I can't verify his profiler, but I can sanity-check the order of magnitude, because that number is either completely plausible or completely impossible depending on one decision.

Do the arithmetic: 3ms ÷ 420 ≈ 7 microseconds per enemy per frame. In 7μs you get a few hundred simple operations — enough for a position integrate, a distance check against a handful of neighbours, a damage tick. It is not enough for 420 entities to each test against the other 419: that's ~176,000 distance checks, which alone blows the budget and produces visible hitching around wave 20.

So the number is only believable if collision and targeting are not O(n²). Which means there's a spatial index in there. There has to be.

That's the whole insight, and it isn't exotic — it's a grid.

// The idea, not his code: bucket enemies by cell, query only neighbouring cells. const CELL = 64; const buckets = new Map(); function rebuild(enemies) { buckets.clear(); for (const e of enemies) { const k = `${(e.x / CELL) | 0},${(e.y / CELL) | 0}`; let cell = buckets.get(k); if (!cell) buckets.set(k, (cell = [])); cell.push(e); } } function near(x, y, radius, out) { out.length = 0; const r = Math.ceil(radius / CELL); const cx = (x / CELL) | 0, cy = (y / CELL) | 0; for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const cell = buckets.get(`${cx + dx},${cy + dy}`); if (cell) for (const e of cell) out.push(e); } } return out; } 

Swap the O(n²) pair test for a 3×3 bucket query and 420 enemies stops being a problem and becomes a Tuesday.

Pooling, and why your frame time looks like a sawtooth

The second thing 3ms implies is that almost nothing allocates during a frame.

A survivor-like spawns and kills hundreds of entities per second. If each one is new Enemy() or a closure per projectile, you're handing the GC a few thousand objects a second. V8 copes fine until a major GC lands mid-wave and you eat a 20ms hitch exactly when the screen is fullest. Players don't call that "GC pressure." They call it "it feels laggy when it gets busy," and then they stop playing.

The fix is a free list: allocate once, reuse forever. Two details worth stealing — cap the pool (dropping a spawn beats a stutter; nobody notices one fewer enemy in a wave of 400), and swap-and-pop instead of splice when releasing, because splice is O(n) and you're calling it 400 times a frame.

Then the render path, where engines usually save you. Canvas 2D at 400 sprites is fine if you avoid the expensive per-sprite work: one image per entity type (every texture swap is a state change — pack into atlases), save()/restore() around every draw (not free; skip when you're not rotating or clipping), shadowBlur and per-sprite filters (a glow on 400 enemies isn't a glow, it's a CPU melt), and any layout read inside the loop — a single getBoundingClientRect() forces a synchronous reflow and is the most common cause of "why is my canvas game at 30fps."

And the classic: delta time, not frame count, or the game runs at double speed on a 144Hz monitor.

The real cost was never the horde

Here's the part the "420 enemies" stat distracts from: content volume was the hard problem.

966 creatures means 966 stat blocks, type combinations, move pools, evolution relationships and sprite variants — plus shinies. That's not a rendering problem, it's a data pipeline problem, and it's the kind of work that doesn't demo well and doesn't get discussed.

The systems on top are just as wide. The game ships 103 moves, 97 of which have evolution recipes, 18 types with five weather states, and 18 arena variants. Each one is small; together they're a content budget no solo developer hand-authors — which is why the roster is assembled from community sprite sources rather than drawn from scratch.

What I find genuinely clever is the balance decision hiding in the type chart. Rather than the mainline ×2 / ×0.5 multipliers, PokéSurvivor uses softer modifiers: ×1.6 super-effective, ×0.6 resisted, ×0.3 immune, with dual types multiplying up to ×2.4. That's not sloppiness — it's a real-time tuning choice. In a 30-minute run you cannot afford a ×2 swing to decide the fight, because the player has no turn to spend recovering. Softening the curve keeps type matchups meaningful without letting a bad draw end the run. (The full chart lives in this independent reference, which is where I pulled those numbers.)

The lesson is unglamorous: build the pipeline before the content. A spreadsheet and a build script that emits typed data will save you more time than any rendering optimisation.

PokéSurvivor's co-op, and the tell that gives away its netcode

Four-player co-op is what separates this from the dozen browser survivor-likes that appear every month, and its reported behaviour is genuinely informative.

One detail: if the host switches browser tabs, the run pauses for everyone.

That sentence tells you most of the architecture. Browsers throttle requestAnimationFrame to roughly zero in background tabs. If the host's simulation is authoritative and stops ticking, every client stops receiving state — and the game freezes for all four players. That's not a bug, it's a signature: host-authoritative simulation, clients sending input and receiving snapshots.

Which is the right call here. Deterministic lockstep would be cheaper on bandwidth but brutally fragile — one float divergence and four players desync permanently, on a codebase you wrote alone. Host authority means one machine owns the truth, clients stay thin, and the worst case is a rubber-band instead of a corrupted run.

The rest fits that model: shared XP (one authoritative pool), proximity revive (a server-side check against the downed player's position), in-run coins shared but progression saved per player.

The thing to internalise: the network isn't the hard part — reconciliation is. Decide up front who owns what, and make the answer boring.

The AI-assisted question, honestly

PokéSurvivor was built with AI assistance and has drawn criticism for it, some fair, some reflex. The interesting question isn't whether AI helped. It's what it couldn't help with.

An assistant will happily generate a level-up card system or a 966-entry data table. It will not tell you your collision is O(n²) at wave 20, that your GC sawtooth is eating the last 4ms, or that the run feels wrong because knockback is 40ms late. Those are measured, not generated — the 3ms figure is profiler output, and profiling is still a human reading a flame chart and deciding what to delete.

Where the criticism lands, I think, is on asset provenance and derivative-work ethics: a fan game on a licensed IP with community-sourced sprites is legally and ethically complicated regardless of how the code was written. That argument doesn't get settled by a frame-time number. Judge the engineering on its merits; keep the IP question separate.

What I'd steal

  • Spatial grid before anything else. Forty lines, an order of magnitude. Day one, not the day it breaks.
  • Pools with a hard cap. Never let entity count grow the heap.
  • Fixed timestep, interpolated render. Non-negotiable for feel across devices.
  • Atlases early. Retrofitting one after shipping 900 individual images is a week you'll resent.
  • Decide network authority on day one. Host-authoritative is the boring correct answer for small co-op.
  • Ship it as a URL. Ten seconds to first frame beats a download prompt every time.

FAQ

Is PokéSurvivor free?​ Yes — browser-based, no download, no ads, no in-app purchases. Tickets drop from enemies and bosses and can't be bought.

What is PokéSurvivor built with?​ Plain HTML, CSS and JavaScript, no game engine. Source is public at github.com/JPTER78/pokesurvivor.

How many Pokémon does PokéSurvivor have?​ 966 across nine generations, Kanto to Paldea, including shinies.

Does PokéSurvivor have co-op?​ Yes, up to four players via room codes. Co-op needs an account; solo works as a guest.

Is PokéSurvivor an official Pokémon game?​ No. It's an unofficial, non-commercial fan project, unaffiliated with Nintendo, Creatures Inc., GAME FREAK or The Pokémon Company.

Notes and disclosure

Facts here come from the official site (pokesurvivor.com), the public repository, and contemporaneous launch reporting. The ~420 enemies / ~3ms figure is the developer's own claim, not my measurement — I've reasoned about its plausibility rather than repeating it as fact. Code samples illustrate the techniques, not the shipped implementation.

Disclosure:​ I maintain pokesurvivor.club, an independent fan guide for this game (Pokédex, move recipes, type chart). It's unaffiliated with the developers, and it's where the roster and matchup numbers above come from.

Written October 2026. The game is under active development, so balance and features will drift.