Simon Corry

Science & Creativity

Product Designer
Front-End Developer
Team Leader

I'm Simon, a multi-award winning product designer based in New York. During my career I've had the privilege of creating experiences for clients including the New York Times and Google. I've also helped guide teams at Facebook and startups like Highfive and WeTransfer.

  • Visual Design
  • Design Systems
  • IA/UI/UX
  • Strategy
  • Prototyping
  • HTML/CSS
How We Beat WebAssembly at Its Own Game

Why our MMORPG never needed WebAssembly

Simon Corry

A few weeks ago Sol Wilds (the game I’m developing) suffered a major performance hitch. Frames in Safari nosedived from 110-120 to 10-30 per second. I started to lose my mind because no matter what I tried I couldn’t find the root cause. Just to make things interesting Chrome and Firefox were buttery smooth, same build, same hardware with each sitting around 110-120 FPS. The other interesting tidbit, this only seemed to be occurring on the production site, never locally. So Safari on local 110-120 but Safari on prod 10-30 which seemed to rule out the browser itself, or so I thought.

Heavy blue wave bands pressing down toward a calm copper band, small fragments suspended in the gap between them.↵ Generated with Nano Banana Pro↵
Heavy blue wave bands pressing down toward a calm copper band, small fragments suspended in the gap between them.

So Claude and I decided to set out on an epic adventure together, one which would have me cursing in all caps and throwing my laptop across the room on more than one occasion. We would try every conceivable flavor under the sun starting with the obvious stuff like turning off all the extensions, running in private windows and spinning up clean automation profiles. We then graduated to sticking a relay in front of the local server (to fake production latency) and bisecting CSS properties one at a time on the live site. Eventually we shipped runtime kill switches so we could turn whole rendering subsystems off in production. Nothing moved, except my rage.

Then we decided to go full balls to the wall in a fit of desperation. We rolled production back to a five-week-old snapshot of the game, basically the last time I remembered everything being perfect. Of course the frame collapse survived it. I remember staring at that result feeling something between awe and nausea, because there was simply no way what I was seeing was possible.

I'll tell you how it ended in a minute. First I need to mention that a friend (and fellow game dev) recently suggested to me that I consider rewriting parts of the game in WebAssembly to improve performance. On the surface this a very fair and rationale suggestion. However the absurd Tolkien esque journey I was about to embark on would end up teaching me a very valuable set of lessons. I passed those lessons onto my friend and when he’d seen the results with his own eyes he began questioning his own WASM journey.

Branching copper lines from the lower left interleaving with a soft blue density field from the upper right.↵ Generated with Nano Banana Pro
Branching copper lines from the lower left interleaving with a soft blue density field from the upper right.

Let me back up

If you've been following along with my series of posts, you'll know I've spent this year building an MMORPG (a massively multiplayer online roleplaying game) with an AI agent as my better half. It's called Sol Wilds (if you want to help me drum up some early adopters). If you read my earlier posts you’ll see me use a couple of other names for it and that’s because I like to test out story in situ and see what sticks. Sol Wilds seems to have struck a chord so I’m locked in on this now. The game is still pre-alpha and it will remain that way for quite a while, closed alpha is the next milestone (aiming for end of year). I’m sure you’ve seen many folks posting about building entire games in an afternoon and the truth is that’s totally possible but if you want to build something that lasts the old rules still apply no matter how flashy the tools get. So I’m dedicated to hand rolling the things I think still matter as a form of human expression, the GUI, the artwork, the story and the sound design. And yes that is a metric ton of shit to do for one solo dev on nights and weekends. Here’s the thing though, I fucking love it, this is easily my life’s work.

Here’s the constraint I drew up at the beginning of the project. I wanted the build to exist in a form I could read so even if I didn’t write the code by hand I could always look over any given file and understand the functions as they were written. As a result my very first principle boiled down to “write the client in vanilla HTML, CSS and JavaScript”. No framework, no build step, no bundler. The browser is the engine. The server owns the entire simulation (combat, enemy brains, spawning, the weather, etc), and the client's whole job is to draw what the server says and send those inputs back. Two players standing in the same spot must see the same world, so the client never gets to decide anything that matters. It just renders. In the industry that’s called server authoritative and for an MMORPG it’s table stakes.

The division of labor between Claude and I has evolved into something quite remarkable to be honest. On my side of the fence I bring twenty-five years of design engineering, taste and all the spicy takes you can shake a stick at. Claude brings unbounded patience, never gets bored and he keeps going whether I’m awake or not. I feel a problem, he measures the problem, and then we argue the point ad nauseam. I don’t automatically overrule him, I read and listen to what I imagine he sounds like in my head. He’s also taken to quizzing me to make sure I’m not talking out of my ass. This weird symbiotic relationship matters and it’s a testament to everything that follows.

A clean wedge of bare paper cutting through a dense blue field, dissolving into scattered triangular fragments.↵ Generated with Nano Banana Pro
A clean wedge of bare paper cutting through a dense blue field, dissolving into scattered triangular fragments.

The optimization war

This particular fire wasn’t the only example of performance gremlins. We’ve actually been fighting this war all year, mostly against Safari. Not because Safari is inherently bad but rather because its priorities lie in lowering consumption. As a result it becomes a very useful teacher for performance. It amplifies costs that Chrome absorbs and gave us a real and measurable price for any sloppy work within WebKit. So now our whole method fits in one gloriously simplistic sentence: profile, form a suspicion, build the experiment, measure, keep whatever survives.

The clearest example is crowds. Dense scenes caused drops (lots of them) and we didn't know which part of an NPC was actually expensive. The body? The shadow? The name tag floating overhead? So my boy ran a subtraction experiment, the same crowded scene, one ingredient hidden at a time. Hiding nameplates alone recovered 27 frames per second (wild). And that had some pretty big downstream effects which culminated in us deciding to act on a real piece of architecture. Distant crowd actors now get painted onto one shared canvas with a smaller footprint, and the up-close actors you actually interact with stay ordinary elements with all their detail (this is in addition to culling). Nothing about the math got faster. There was just too much stuff, and now there's less so… “number go up”.

Same thing with the mini-map, which a real profile from my machine caught.The map was redrawing itself top to bottom on every frame while the camera moved. Terrain, fog, dots, the lot, continuously. We ran an experiment that capped how often it's allowed to redraw and as a result it vanished from the profile. Again, no computation got faster just less stuff to compute.

Spawn waves taught us the same lesson from a slightly different angle. When a pack of enemies appeared, the game used to dump the whole crowd into the page between two frames, and the browser choked every single time. Now creation drips instead of dumping, a few actors per frame, and the wave streams in smoothly. I'd love to tell you that this required some deep computer science wizardry or that my skills are just that elite. But it didn’t and they are not. It just required the powers of observation and perseverance.

My favorite lesson is almost too embarrassing to admit because I should have known better but I will share it because it’s useful context. Our staging site felt like a jittery mess the moment my character started moving, while on production it felt smooth. I screamed and shouted in Cursor at Claude because I assumed it had bypassed our risk gate and somehow polluted the environment variables. Of course the actual truth contained zero drama. It turned out that skin tones were the culprit. Any character with a non-default skin tone carried its recolored sprite sheet as one giant inline style value, 563KB of image data sitting on the element all because I’d forgotten to finish the performance pass on the feature (this was before I created Foundry). Safari repriced that style on every movement update, at roughly a thousand times the cost of a normal write. The character I spun up when we launched the staging environment happened to use a non-default tone while my production character (which I’ve been using for months) uses the default. This is why the game felt broken across two seemingly identical environments. The fix was to hand Safari a short address pointing at the image instead of the image itself. Movement smoothed out overnight and I relearned a valuable lesson about dogfooding your own shit.

I should also mention that this great performance war had plenty of red herrings. At one point we built a whole message-batching mechanism on the theory that too many little updates were landing between frames. Measured properly, it batched nothing, because the server only sends one update per actor per beat and there was nothing to batch. We reverted it the same day. Measure twice, cut once.

Across all of it, one finding kept repeating. Whenever we caught a slow frame and put it under a microscope, our JavaScript was barely visible. The script cost was tiny, a rounding error of the frame. The time went to waiting on things like the layout, the compositor (the part of the browser that assembles all the layers into the picture you see) and the browser's own housekeeping. The lost time was waiting, not computing and that turned out to be our smoking gun.

A large turbulent blue mass stopping abruptly at a thin vertical copper line, sparse dots beyond it.↵ Generated with Nano Banana Pro
A large turbulent blue mass stopping abruptly at a thin vertical copper line, sparse dots beyond it.

Back to the fire

So at this point we’d finally figured out how to measure the game performance reliably and built a pretty impressive set of reusable instruments. But none of it helped, which scared me to the edge of a genuine quit moment. We’d ruled out extensions, private state, the network, the CSS, entire subsystems, and finally all of our own history by deploying that five-week-old snapshot. Every time we measured the code was exonerated.

Fast forward to a gross muggy morning in NYC. I take a shower and as is usually the case a thought popped into my head. What about Safari per-site settings? A couple of weeks before this epic drama unfolded I’d set up a new machine and installed AdBlock Pro. I recalled that I’d forgotten to turn off the add-on for my production and staging environments. But then I quickly dismissed the idea because how could my old machine be experiencing the same issue? Sure it has AdBlock Pro installed as well but I’d explicitly turned off the feature for production and staging environments so it couldn’t be that, could it?

Well as it turns out my old nemesis, technology, had outsmarted me again. iCloud in its infinite wisdom had actually synced that per-site switch after installing it on the new machine. So my old Mac went from blocker off to blocker on without me noticing. This meant that days before when I reached for my old machine thinking it was neutral territory, it wasn’t. Which explains why the collapse followed me across machines, why turning extensions off didn't clear it (the per-site switch lives separately from the extension toggle), and why all the reverts in the world changed nothing.

One toggle. Thirty seconds. Fixed on both machines, instantly. FML.

I won't pretend to know exactly why blocker rule evaluation stalls a busy page inside Safari, I leave that to the nerds. We never isolated the mechanism and tbh at this point I’d reached my sane limit. The toggle was the proof, and no hard feelings toward AdBlock Pro, which was doing its job. But the lesson went straight into our runbook. When a problem is one browser plus one site plus every machine you own, check the browser's own per-site settings before you bisect your code. The check costs thirty seconds. Our bisect cost me days of high blood pressure.

Flowing blue wave bands with a single copper burst erupting through one band.↵ Generated with Nano Banana Pro
Flowing blue wave bands with a single copper burst erupting through one band.

The ghost on a thirty-second timer

The false alarm left us with unusually good instruments though, and once the smoke cleared, those instruments showed a real ghost. Every so often the game would hitch. One long frame, small enough that most players would shrug it off, regular enough to bother me. It kept landing on a thirty-second rhythm. There is no thirty-second timer in the game. A blank page didn't hitch. Chrome and Firefox didn't hitch. Nothing in our code lined up with it.

This is where the hunt turned from being a stressed induced rage fit to genuinely fun. WebKit, the engine inside Safari, is open source. So I sent Claude and his child minions to go read it. Deep in there, Safari checks its own memory footprint on a hardcoded thirty-second clock, and when a page crosses certain thresholds it starts purging caches to claw memory back. Our hitches were phase-locked to Apple's clock, and we proved the threshold behavior directly. Deliberately inflate a test page's memory and the hitch becomes a metronome, stay under the line and it's occasional. Finally, we'd caught the little bugger!

So at this point our war had boiled down to memory. Most of the graphics memory billed to our page belongs to the browser's own tile cache and most of it remains outside our reach. The memory that grew over a long session turned out to be transient garbage from our own drawing loop. The fix was nice and simple, basically allocate less. Reuse one scratch object, stop manufacturing strings in plain JavaScript. A big slice of the churn was gone by the next profile and my own recordings in Safari's Web Inspector confirmed it. Nothing of ours was actually being retained, the pile was garbage in transit, and the answer was to reduce the amount of garbage in transit, not send more trucks.

Chasing this ghost took us from our codebase into Apple's source tree, through my own live recordings, all the way to attaching Apple's profiler to a WebKit build after stock Safari refused to be watched. I get that this is a strange definition of fun but wait til you see me at parties.

A calm copper wave field scattered with many small separate blue dot clusters.↵ Generated with Nano Banana Pro
A calm copper wave field scattered with many small separate blue dot clusters.

Where the record stands

The game holds a high, steady frame rate on crowded screens in the browser, Safari included usually between 70-110 depending on hardware and load. The lessons we learnt along the way probably sound obvious in hindsight but I prefer to think of them as earned through evidence.

  • Draw fewer things
  • Redraw less often
  • Allocate less
  • Stop stapling image data to elements
  • Respect the strictest engine's rules and the others improve for free.

On the server side we ran a proper load test with synthetic players: 250 concurrent connections spread across the world, one server keeping pace with its 100 millisecond simulation beat. Cramming everyone into a single spot runs hotter, and that's my next fight (in an MMO it always is). But hey, for now the server is genuinely happy and you know what they say about that. Happy server, calm observer… I’ll see myself out.

A tight cluster of sharp blue triangular shards sitting apart from a soft copper flow, bare paper between them.↵ Generated with Nano Banana Pro
A tight cluster of sharp blue triangular shards sitting apart from a soft copper flow, bare paper between them.

So, WebAssembly?

Oh yeah, time to address that spicy take. WebAssembly, if it's new to you, is compiled code (from languages like Rust or C) that runs in the browser at near-native speed inside its own sealed memory. It's genuinely great at what it says on the tin… heavy, uniform number crunching. Hand it a big batch of work, let it grind, take a small answer back. For the right problem it's the right tool, and I'd use it without hesitation.

Here's what a year of profiling says about our problem though. The standard advice wants me to move every workload to WebAssembly BUT those workloads live on our server. Collision for hundreds of entities, enemy pathfinding, combat math, etc. All server bound with the architecture doing the work. The client, the only place browser WebAssembly could help us, spends its slow moments waiting on the browser itself, and WebAssembly doesn't make the browser wait less. It can't even touch the page directly. It talks to the page through JavaScript, so any work that ends in something you can see crosses right back into the world it supposedly escaped. Our network format has been binary for ages. Our garbage problem got solved by allocating less, in place, in the language we already had. There was never a math-shaped bottleneck to hand over. We even took our newly minted instruments for a spin here, and the profiler pointed somewhere else every single time.

We're also not the only ones who found this, which of course made me feel better. A startup called Zaplib bet itself on speeding up web apps by incrementally porting JavaScript to Rust and WebAssembly, and wound down after discovering with real users that the speedups mostly weren't there. Their post-mortem is worth reading if you care about these things but paraphrased it surmises that there are usually simpler ways to find performance improvements. That matches our journey almost to the letter. Browser vendors spent two decades making JavaScript indecently fast, and it turns out you're allowed to just... use that.

So no, we never beat WebAssembly in a benchmark, the title is just a bit of rage-bait fun for nerds. What actually happened is we spent weeks asking one question over and over (where did this frame actually go?) and the answer was never about the language itself. If our profiler ever names a genuine number-crunching whale, I’m probably gonna reach for WebAssembly. But I’m pretty far through this project and it hasn't happened yet. My hot take is that on a well-architected 2D game client, it probably won't.

The end of a long road which basically landed on the following conclusions:

  • The five-alarm fire was an ad blocker setting
  • The ghost was Apple's own thirty-second clock
  • The jitter was bloated image data
  • None of it was the language

And it was all surmounted by trusting measurements over ego. Claude is better at the second part than I am. I'm still learning.

But in all honesty my friends, the journey humbled me. For all the talk of AI taking over the world one thing remains true. Persistence matters. If you abandon a project the minute something gets hard, well, you’re not going to survive very long in this game. AI isn’t a cheat code, you don’t get to one shot success, it’s just a tool. And just like any tool the craftsman on the other end of it maters more than the tool itself.