🌾 FarmHeart
Game DevJavaScriptTutorial

How to Build a Save Game System with localStorage

2026-09-14 · 9 min read

Why Save Systems Are Harder Than They Look

Saving a game sounds simple: serialize the game state to JSON, store it, load it later. In practice, save systems are one of the trickiest parts of game development because they touch every system in your game and break in subtle ways.

Browser games have a unique challenge: your save data lives in localStorage, which the user can clear at any time, which has a 5-10MB size limit, and which doesn't sync between devices. Understanding these constraints helps you design a system that's robust despite them.

Choosing What to Save

Don't save everything — save the minimum needed to reconstruct the game state. This keeps your save data small and makes it easier to add new features without breaking old saves.

Rule: Save the inputs to your systems, not the outputs. If crop growth is deterministic given a plant time and moisture history, save the plant time and moisture events — not the current growth percentage.

What to save vs. derive:

Save (persistent state)Derive (computed at load)
Player position, inventory, moneyUI state, animation frames
Crop plant time, type, tile indexGrowth progress, visual state
Achievement unlock timestampsAchievement descriptions, icons
Settings (volume, controls)Menu state, button positions
World seed (if procedural)Terrain, object placement

Structuring Your Save Data

Always include a version number in your save data. This is the single most important decision you'll make about your save system — it enables every future migration.

A well-structured save object looks like this:

Auto-Save Architecture

Players expect auto-save. Implement it with a timer that fires every 30-60 seconds and on specific events (tab close, chapter completion, significant progress).

Use the beforeunload event to save when the player closes the tab. This is your last chance to persist progress — don't skip it. Note that beforeunload handlers must be synchronous, so localStorage.setItem() (synchronous) works but fetch() (async) is unreliable.

Performance tip: Auto-saving every 30 seconds is fine for most games. Don't save every frame — JSON.stringify() on a large state object can cause noticeable frame drops. If your save data is over 100KB, consider saving on a Web Worker.

Data Serialization

JSON.stringify() handles most JavaScript types, but watch out for these gotchas:

Consider using a custom serializer/deserializer pair that handles your specific types. This is cleaner than patching up data after JSON.parse().

Save Versioning and Migration

Your save format will change as you add features. Version migrations let old saves load in new versions of the game without losing progress.

The pattern: write a migration function for each version bump. When loading, check the save's version against the current version and run each migration in sequence.

Example migration chain: v1 → v2 (add achievements), v2 → v3 (rename "gold" to "money"), v3 → v4 (add weather system). A v1 save loads by running all three migrations in order.

Never delete a migration function. A player who hasn't played in six months might have a v1 save that needs to traverse the entire chain.

Corruption Detection and Recovery

localStorage data can get corrupted — browser crashes, storage quota exceeded, malformed writes. Build defenses:

Multiple Save Slots

Even simple games benefit from multiple save slots. Players want to experiment, start over, or share with family members. Implement slots as separate localStorage keys: farmheart_save_1, farmheart_save_2, etc.

Show metadata for each slot on the load screen: character name, playtime, last save date, chapter progress. Store this metadata separately from the full save data so you can render the slot list without parsing every save.

localStorage Limits and Alternatives

localStorage typically gives you 5-10MB per origin. For most games, this is plenty. But if your game generates lots of data (world maps, crafting recipes, chat logs), you might hit the limit.

Storage APICapacitySync?Use For
localStorage5-10 MBYesSave data, settings
IndexedDBHundreds of MBNo (async)Large worlds, assets
sessionStorage5-10 MBYesTemporary state (undo)

For most browser games, localStorage is the right choice. If you're running into size limits, compress your save data before storing it — a simple LZ-based compression can shrink JSON saves by 60-80%.

Cloud Saves (Without a Backend)

Players want to continue on different devices. Without a full backend, you have a few options:

If you want proper cloud saves, you'll need a backend — but a serverless function that stores saves in a key-value store (Cloudflare KV, Redis) is cheap and simple to build.

Testing Your Save System

Save systems are notoriously hard to test because bugs only appear after real play time. Automated tests should cover:

Add debug tools: a button to export the current save as JSON, a button to import a save, and a button to corrupt the save intentionally (for testing recovery). These tools are also invaluable for user support — when a player reports a bug, ask them to export their save.

Play FarmHeart

A cozy 3D farming game that runs right in your browser. No download, no signup.

Play Now