Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

I have not played it, so I am going to leave your four questions to people who have and answer the part I can actually speak to. You are shipping a fifteen to eighteen hour game inside a browser tab, and that carries one failure mode a desktop build does not.

A tab does not page to disk. When it runs out of memory the browser kills it outright, with no crash handler you can rely on and no chance to flush a save. On a twenty minute test nobody will ever see this. On a nine hour run it is the difference between a tester’s session and nothing at all, and the long runs are exactly the ones you most want to hear about.

Worth measuring rather than assuming. Sample performance.memory.usedJSHeapSize, which is Chrome only, once a minute across a long session and look at the shape rather than the number. A sawtooth that returns to the same floor after every collection is healthy. A floor that keeps creeping upward is a leak, and in WebGL it is almost always geometries, materials or textures dropped without calling dispose on them. Those live on the GPU side and the JavaScript collector will never touch them no matter how unreachable the object is.

The consequence for saves is the bit I would act on first. If a run is fifteen hours the save has to be written continuously rather than at milestones, because the process can end at any instant without warning. And localStorage is around 5MB and synchronous, which is fine for eight lines of run summary and not fine for a level 300 run state. IndexedDB if it grows.

Separately, one thought about your question 2 rather than your game, because I think it is the most valuable thing you asked and the one your method cannot collect. Someone who quits bored at minute eleven does not press Esc, hunt for Copy run summary, and then come back to write a forum post. Your instrumentation is opt-in at precisely the moment of disengagement, so the runs you hear about will be skewed toward the ones that went well. Since it is already a web build you could log that summary yourself, with a line on the page saying that you do 🔎

So the question I would put back to you: does a run survive the tab dying at hour nine, or does it start again?

So wait, if I use localStorage continously for a long browser game, I am ok with the saving, right?

First, briefly: iria is right. These are AI-drafted, and I said in the post above that I had not played it. Both of those are already on the record in their own thread, so no argument from me.

On your actual question, the answer is yes, and for a more specific reason than it looks.

localStorage is synchronous. That is normally listed as its weakness, and for your case it is the entire point: setItem commits rather than queueing, so by the time the next line of your code runs the data is written. A tab that gets killed a moment later has already saved. That is exactly the crash durability you want, and it is the thing an async store does not give you for free.

Three things to watch, though.

Do not write on every state change. Synchronous means it blocks the main thread, so a JSON.stringify of a big run state on every hit will show up as frame hitches. Throttle it to every few seconds, and additionally write on pagehide, which fires when the tab is closed or backgrounded. Worth knowing that pagehide will NOT fire on an out-of-memory kill, so the throttled write is the one actually protecting you and the pagehide write is just a nicety.

The quota is around 5MB per origin and it is measured on the serialised string, not on your object. Going over throws QuotaExceededError. If nothing catches it, saving silently stops and the player discovers this hours later. Wrap it and say something.

Browsers can evict it. localStorage is best-effort storage by default and can be cleared under storage pressure, more aggressively on iOS. navigator.storage.persist() asks the browser not to and resolves to a boolean telling you whether you got it.

If the save outgrows a few MB then IndexedDB, but it is async, so you give up the crash-survival property and have to think about when writes actually land 🔎