| Level | Typical size | Time to reach | Forgets when power is off? | What is stored |
|---|---|---|---|---|
| CPU registers and cache | bytes to ~100 MB | under 1 to ~10 nanoseconds | yes | the values the processor is using right this instant |
| RAM | 8 to 128 GB | ~100 nanoseconds | yes | running programs, every open tab, your JavaScript variables |
| GPU memory (VRAM) | 8 to ~200 GB | similar to RAM, but moves far more data at once | yes | textures, 3D scenes, AI models while they run |
| SSD (flash) | 0.5 to 8 TB | ~100 microseconds (1,000× slower than RAM) | no | your files, apps, and the browser's localStorage |
| Hard disk | up to ~40 TB | ~10 milliseconds (a spinning disk has to turn) | no | mostly data centers now, not laptops |
| Tape | tens of TB per cartridge | seconds to minutes (a robot loads the cartridge) | no | long-term archives, the cheapest storage per byte |
| "The cloud" | effectively unlimited | ~20 to 200 milliseconds (a trip over the internet) | no | someone else's SSDs, hard disks, and tape, in a building |
localStorage does: the browser writes it into an actual file on disk.
~/Library/Application Support/Google/Chrome/Default/Local Storage/This folder is hidden, and the files inside are a database mixed with every other site's data, so it is hard to find and read. You don't need to open it: DevTools shows you the same saved text much more clearly.
localStorage.
It holds text, and it survives reloads and even quitting the browser. Lets call this state.
// save: whenever something changes
// "myScene: is a label for the data you save
// state: is the data itself
localStorage.setItem("myScene", JSON.stringify(state));
// load: when the page starts
const saved = localStorage.getItem("myScene");
if (saved) {
const state = JSON.parse(saved);
// use state to rebuild the scene
}
The difficult part is to decide what goes into
state, and how your scene rebuilds itself from it.
session=a8f3k2 and is saved on your disk. The
difference from local storage is that your browser sends it back to the website with every request.
1. You log in. The server answers: Set-Cookie: session=a8f3k2 2. Your browser saves the cookie on disk. 3. Every later visit: your browser sends: Cookie: session=a8f3k2 4. The server looks up a8f3k2 in its own records: "I've seen you before " !!(What does this mean? I've seen you before??)
continuity?
On its own, the web has no memory. Each request is answered as if the server has never seen you before.
| Film set | Web |
|---|---|
| separate shots, filmed out of order | different requests to same site (page 1 -> page 2 -> page 3) |
| edit:the audience sees one smooth scene | cookie: your visit feels like one continuous visit |
file:/// don't get cookies.)
doubleclick.net) serves banner ads on
millions of sites.youtube.com, so
Google sees the visit even if you never went to YouTube.ads.example, that is on thousands of sites. (Real companies that do this include Google, Meta and Criteo.)
ads.example banner. ads.example gives your
browser a cookie: id=ABC123.id=ABC123 back, along
with which page you are on.ABC123 read "Best hiking trails near Portland" ABC123 viewed trail running shoes, size 9 ABC123 read 3 articles about new parents
<script src="https://ads.example/ad.js"></script> <div class="ad-slot" data-size="300x250"></div>Google's version (AdSense) looks like this:
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1234"></script> <ins class="adsbygoogle" data-ad-client="ca-pub-1234" data-ad-slot="5678"></ins>2. An iframe: a small window in the page showing another website.
<iframe src="https://ads.example/banner?size=300x250" width="300" height="250"></iframe>3. A tracking pixel: an invisible image. Its only job is to make your browser contact the server.
<img src="https://ads.example/pixel.gif?page=trail-shoes" width="1" height="1">Notice that all three addresses point to the ad company, not to the site you are on. That is the same kind of tag you use to load three.js from a CDN: a file from someone else's server. What differs is what that server does with the request.
iframe or googlesyndication.
localStorage, and "fingerprinting" (recognizing your exact
browser and device settings) work without them.counter.html, and open it in a browser.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Counter</title>
</head>
<body>
<!-- onclick: when this button is clicked, run the function addOne() -->
<button onclick="addOne()">Click me</button>
<!-- the number is shown inside this span; it starts at 0 -->
<p>Clicks: <span id="count">0</span></p>
<script>
// The state of this program: one number.
// "let" makes a variable that can change later.
// Variables live in RAM, so this one is erased on every reload.
let count = 0;
// A function: a named box of code. The button's onclick runs it.
function addOne() {
// change the state: add one
count = count + 1;
// show the new state: find the span by its id,
// and put the number inside it
document.getElementById("count").textContent = count;
}
// Reload the page and the code runs again from the top,
// so count starts over at 0.
</script>
</body>
</html>
Try it: click a few times, then reload. The count goes back to 0. The variable
count was in RAM, and the reload ran the code from the top.
counter_saved.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Counter that remembers</title>
</head>
<body>
<!-- same button and span as Step 1 -->
<button onclick="addOne()">Click me</button>
<p>Clicks: <span id="count">0</span></p>
<script>
// LOAD, once, when the page starts.
// localStorage.getItem("clicks") looks up the saved value
// under the name "clicks". It gives back text, like "7",
// or null if nothing has been saved yet.
// Number() turns the text into a number. Number(null) is 0,
// so the very first visit starts at 0.
let count = Number(localStorage.getItem("clicks"));
// show the loaded number right away
document.getElementById("count").textContent = count;
// runs every time the button is clicked
function addOne() {
// change the state: add one
count = count + 1;
// SAVE, after every change.
// setItem stores the value under the name "clicks".
// The browser writes it to a file on disk, so it
// survives reloads and quitting the browser.
localStorage.setItem("clicks", count);
// show the new state
document.getElementById("count").textContent = count;
}
</script>
</body>
</html>
Try it: click, reload, quit the browser and open it again. The count is still there.
In DevTools → Application → Local Storage you can see the key
clicks and its value.
localStorage only holds text. One number is easy to turn back and forth.
A list of things needs JSON.
JSON.stringify
turns the array into text to save, and JSON.parse turns it back. Save it as
guestbook_tiny.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Tiny guestbook</title>
</head>
<body>
<!-- a text box to type in, and a button that runs addMessage() -->
<input id="msg" placeholder="Leave a message">
<button onclick="addMessage()">Add</button>
<!-- an empty list; JavaScript fills it with the messages -->
<ul id="list"></ul>
<script>
// The state of this program: an array (a list) of messages.
// Each message is an object: { text: "hello", time: "1:01:10 PM" }
// LOAD, once, when the page starts.
// getItem gives back the saved text, or null the first time.
// JSON.parse turns that text back into an array.
// JSON.parse(null) is null, and "|| []" means
// "if that gave nothing, use an empty array instead".
let messages = JSON.parse(localStorage.getItem("guestbook")) || [];
// draw the saved messages on the page
show();
// runs every time the Add button is clicked
function addMessage() {
// read what was typed in the text box
const text = document.getElementById("msg").value;
// if the box is empty, stop here and do nothing
if (text === "") return;
// change the state: add a new message object to the end of the array,
// with the text and the current time
messages.push({ text: text, time: new Date().toLocaleTimeString() });
// SAVE, after every change.
// localStorage only holds text, so JSON.stringify
// turns the whole array into one string of JSON text.
localStorage.setItem("guestbook", JSON.stringify(messages));
// empty the text box, ready for the next message
document.getElementById("msg").value = "";
// redraw the list so the new message shows up
show();
}
// Draws every message in the array as a list item.
function show() {
const list = document.getElementById("list");
// erase what is on the page, then draw the whole array again
list.innerHTML = "";
// go through the array one message at a time
for (const m of messages) {
// make a new <li> element
const li = document.createElement("li");
// put the text and time inside it. textContent (not innerHTML)
// shows a message like <b>hi</b> as plain text, so nobody
// can sneak HTML or code into the page.
li.textContent = m.text + " (" + m.time + ")";
// add the <li> to the list on the page
list.appendChild(li);
}
}
</script>
</body>
</html>
The pattern is the same as the counter: load once when the page starts, save after every
change, then redraw.
| Library | What it has |
|---|---|
| three.js | scene.toJSON() and ObjectLoader save and
reload a whole scene. GLTFExporter exports glTF, a standard 3D file Blender
can open. |
| Babylon.js | SceneSerializer saves a whole scene as JSON. |
| Leaflet | toGeoJSON() saves markers and shapes as GeoJSON (JSON for
maps). |
| p5.js | storeItem() and getItem(), a friendlier
localStorage. |
| PixiJS, Phaser | No scene saver. You write your own JSON. |
| [Lab Option 1] Pixel Art Lab | [Lab Option 2] Save Your Scene's State | |
|---|---|---|
| For | those who want to read code (you learn by reading code) and code by hand | those who want to think big picture |
| What you do | Build a small drawing tool in four steps, then change it yourself: a different grid size, your own colors. You can add additional features like your own saved drawing if you like | Look at a three.js or PixiJS scene, decide which of its states are worth remembering, and have the AI tool write the save and load code. Then do the same with your own scene. |
| You practice | events, arrays, localStorage, JSON, read code line by line |
deciding what state is, and judging whether the AI's code does what you asked |
| Start at | [Lab Option 1] READ: Pixel Art Lab, Step 1 (next slide) | [Lab Option 2] State |
pixel_1.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pixel art 1</title>
<style>
/* 16 columns of 20px squares, with 1px gray lines between them */
#grid {
display: inline-grid;
/* CSS Grid: a standard CSS layout for rows and columns.
repeat(16, 20px) means: 16 columns, each 20px wide.
The cells fill the grid left to right, and start a new row after every 16. */
grid-template-columns: repeat(16, 20px);
gap: 1px;
background: #ccc; /* shows through the gaps as grid lines */
border: 1px solid #ccc;
}
.cell {
width: 20px;
height: 20px;
background: white;
}
</style>
</head>
<body>
<!-- an empty box; JavaScript fills it with 256 cells -->
<div id="grid"></div>
<script>
// find the empty grid box
const grid = document.getElementById("grid");
// repeat 256 times (16 x 16): i counts 0, 1, 2 ... 255
for (let i = 0; i < 256; i++) {
const cell = document.createElement("div"); // make a new <div>
cell.className = "cell"; // give the div class="cell", so the .cell style applies
grid.appendChild(cell); // put it inside the grid
}
</script>
</body>
</html>
Try it: open DevTools → Elements and look inside #grid. The loop wrote
256 <div> elements. Change 16 in the CSS and
256 in the loop to get a different size.
onclick on the whole grid. When it runs, the browser hands
paint an event object describing the click, and
event.target is the exact cell that was clicked. Save it as pixel_2.html.
onclick="paint(event)" means: when this is clicked, run
paint, and hand it event.event is an object, a bundle of information the browser fills in
about what just happened:
event.type: what kind of event, "click"event.target: the HTML element that was clickedevent.target.id: the HTML id of the element that was clicked.
The grid has id="grid" and the cells have no id, so this tells a cell
apart from the thin lines between cells (which are the grid itself)event.clientX, event.clientY: where, in pixels from the
top-left of the windowevent.key: which key was pressed (for key events)onclick runs, and event.target still says which cell was
clicked. One handler covers all 256 cells.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pixel art 2</title>
<style>
/* 16 columns of 20px squares, with 1px gray lines between them */
#grid {
display: inline-grid;
/* CSS Grid: a standard CSS layout for rows and columns.
repeat(16, 20px) means: 16 columns, each 20px wide.
The cells fill the grid left to right, and start a new row after every 16. */
grid-template-columns: repeat(16, 20px);
gap: 1px;
background: #ccc; /* shows through the gaps as grid lines */
border: 1px solid #ccc;
}
.cell {
width: 20px;
height: 20px;
background: white;
}
</style>
</head>
<body>
<!-- onclick on the whole grid: a click on any cell runs paint(event) -->
<div id="grid" onclick="paint(event)"></div>
<script>
// find the empty grid box
const grid = document.getElementById("grid");
// repeat 256 times (16 x 16): i counts 0, 1, 2 ... 255
for (let i = 0; i < 256; i++) {
const cell = document.createElement("div"); // make a new <div>
cell.className = "cell"; // give the div class="cell", so the .cell style applies
grid.appendChild(cell); // put it inside the grid
}
// Runs when anything inside the grid is clicked.
// event describes the click. event.target is the element
// that was actually clicked: one of the cells.
function paint(event) {
// event.target.id is the id of the clicked element.
// The cells have no id, but the grid has id="grid".
// A click on the thin lines between cells hits the grid itself,
// so if the id is "grid", stop here and paint nothing.
if (event.target.id === "grid") return;
// color the clicked cell
event.target.style.background = "black";
}
</script>
</body>
</html>
Try it: draw something, then reload. It's gone. The colors were only on the page, and
nothing remembered them.
colors, with one color per cell.
paint changes the array, and draw colors the cells from it. Save it as pixel_3.html.
let colors = ["white", "black", "red"]; // slot 0 slot 1 slot 2 colors[1] // read slot 1: "black" colors[2] = "blue"; // change slot 2: now ["white", "black", "blue"] colors.length // how many slots: 3
new Array(256).fill("white") makes 256 slots, all "white",
numbered 0 to 255. Slot i holds the color of cell number i.
Because the whole drawing is one value with one name, JSON.stringify can turn
it into text and save it.
colors[i], paint needs to know which cell was
clicked. The loop gives every cell its number, 0 to 255, with
cell.dataset.index = i. dataset stores your own information on an
element as a data- attribute: in DevTools → Elements each cell shows
data-index="17". paint reads it back from
event.target. It comes back as text, so Number() turns it into a
number.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pixel art 3</title>
<style>
/* 16 columns of 20px squares, with 1px gray lines between them */
#grid {
display: inline-grid;
/* CSS Grid: a standard CSS layout for rows and columns.
repeat(16, 20px) means: 16 columns, each 20px wide.
The cells fill the grid left to right, and start a new row after every 16. */
grid-template-columns: repeat(16, 20px);
gap: 1px;
background: #ccc; /* shows through the gaps as grid lines */
border: 1px solid #ccc;
}
.cell {
width: 20px;
height: 20px;
background: white;
}
</style>
</head>
<body>
<!-- onclick on the whole grid: a click on any cell runs paint(event) -->
<div id="grid" onclick="paint(event)"></div>
<script>
// find the empty grid box
const grid = document.getElementById("grid");
// make 256 cells, and give each one its number, 0 to 255
for (let i = 0; i < 256; i++) {
const cell = document.createElement("div");
cell.className = "cell";
// saved on the element as a data- attribute. In HTML, cell 17 looks like:
// <div class="cell" data-index="17"></div>
cell.dataset.index = i;
grid.appendChild(cell);
}
// The state of this program: the drawing, as an array of 256 colors.
// Cell number i is drawn with colors[i]. Every cell starts white.
let colors = new Array(256).fill("white");
// draw the whole picture from the array
draw();
function paint(event) {
if (event.target.id === "grid") return;
// which cell? read back the number stored on it.
// dataset gives text ("17"), so Number() turns it into a number
const i = Number(event.target.dataset.index);
// change the state: the new color for that cell
colors[i] = "black";
// redraw the picture from the state
draw();
}
// Colors every cell from the array.
// The cells only show the drawing. The array stores the drawing.
function draw() {
for (let i = 0; i < 256; i++) {
grid.children[i].style.background = colors[i];
}
}
</script>
</body>
</html>
This looks the same as before, but the important change is inside: the picture is now
data, a list of 256 words. This is waht we are going to save next.
pixel_4.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pixel art 4</title>
<style>
/* 16 columns of 20px squares, with 1px gray lines between them */
#grid {
display: inline-grid;
/* CSS Grid: a standard CSS layout for rows and columns.
repeat(16, 20px) means: 16 columns, each 20px wide.
The cells fill the grid left to right, and start a new row after every 16. */
grid-template-columns: repeat(16, 20px);
gap: 1px;
background: #ccc; /* shows through the gaps as grid lines */
border: 1px solid #ccc;
}
.cell {
width: 20px;
height: 20px;
background: white;
}
</style>
</head>
<body>
<!-- onclick on the whole grid: a click on any cell runs paint(event) -->
<div id="grid" onclick="paint(event)"></div>
<script>
// find the empty grid box
const grid = document.getElementById("grid");
// make 256 cells, and give each one its number, 0 to 255
for (let i = 0; i < 256; i++) {
const cell = document.createElement("div");
cell.className = "cell";
// saved on the element as a data- attribute. In HTML, cell 17 looks like:
// <div class="cell" data-index="17"></div>
cell.dataset.index = i;
grid.appendChild(cell);
}
// LOAD, once, when the page starts.
// getItem gives back the saved text, or null the first time.
// JSON.parse turns the text back into the array of 256 colors.
// If nothing was saved yet, start with 256 white cells.
let colors = JSON.parse(localStorage.getItem("pixels")) || new Array(256).fill("white");
// draw the saved picture
draw();
function paint(event) {
if (event.target.id === "grid") return;
// change the state
const i = Number(event.target.dataset.index);
colors[i] = "black";
// SAVE, after every change: the whole array as JSON text
localStorage.setItem("pixels", JSON.stringify(colors));
draw();
}
// Colors every cell from the array.
function draw() {
for (let i = 0; i < 256; i++) {
grid.children[i].style.background = colors[i];
}
}
</script>
</body>
</html>
Try it: draw, reload, quit the browser and open it again. The drawing comes back. In
DevTools → Application → Local Storage, the key pixels holds your
picture: an array of 256 color names.
pixel_mine.html and change it.
16 and every
256. 8×8 is 64 cells.repeat(16, 20px) sets the number of columns and their width. Try
repeat(8, 40px), and make .cell 40px wide and 40px tall so the
drawing stays the same size on screen.i < 256: the one that makes the cells and
the one in draw(). Change both.new Array(256).pixels, and its first 64 colors fill your new grid. Fix it either way:
"pixels8", in both getItem and
setItem, orpixels → Delete.colors:
let currentColor = "black";
function pick(color) {
currentColor = color;
}
pick with a different color:
<button onclick="pick('red')">red</button>
<button onclick="pick('blue')">blue</button>
paint, replace "black" with currentColor.'tomato', or codes like
'#ff3366'.pick('white').<input type="color" oninput="pick(this.value)">
gives a color picker. this.value is the color that was chosen. In the HTML,
put it above the grid:
<!-- a color picker. Every time a new color is chosen, oninput runs pick(),
and this.value is the chosen color, as a code like "#ff3366" -->
<p>Color: <input type="color" oninput="pick(this.value)"></p>
<!-- onclick on the whole grid: a click on any cell runs paint(event) -->
<div id="grid" onclick="paint(event)"></div>
In the script, add:
// The color we are painting with. It lives in RAM and is not saved,
// so every reload starts with black (the color picker also starts black).
let currentColor = "black";
// Runs when a color is chosen in the color picker.
function pick(color) {
currentColor = color;
}
| Sample | Library | Page |
|---|---|---|
| Clickable, draggable cubes | three.js | html |
| Platformer game | PixiJS | html |
| Sample | Library | Page |
|---|---|---|
| Clickable, draggable cubes | three.js | html |
| State | Changes when | Save it? |
|---|---|---|
| which cubes still exist | you double-click one | yes |
| each cube's position | random at load, then you drag it | yes |
| each cube's color | random at load | yes, or every reload looks different |
| each cube's rotation | constantly, it spins | probably not, would anyone notice? |
| camera position | you orbit or zoom | maybe, do you want people to return to their own view? |
| lights, cube size, the pop sound | never, they're written in the code | no, not state |
| Sample | Library | Page |
|---|---|---|
| Platformer game | PixiJS | html |
| State | Changes when | Save it? |
|---|---|---|
| player position (x, y) | every frame | maybe, resume where you left off? |
| player velocity, "on the ground" | every frame | no, resuming mid-jump is strange |
| which keys are held down | every key press | no, only matters this instant |
| score | every second, reset on fall | maybe, but only together with position, to resume a run |
| high score | doesn't exist yet! | yes, the classic thing games save |
| platforms | never, written in the code | no, not state (unless players could build them) |
{
"cubes": [
{ "id": 0, "x": 1.2, "y": -0.4, "z": 3.0, "color": "#ff3366", "deleted": false },
{ "id": 1, "x": -2.5, "y": 0.8, "z": 1.1, "color": "#33ccff", "deleted": true }
],
"camera": { "x": 0, "y": 0, "z": 3 }
}
{ "player": { "x": 350, "y": 232 }, "score": 12, "highScore": 40 }
Javascript has utility functions like
JSON.stringify(state) that turns an object into text, and
JSON.parse(text) turns the text back into an object.
if there is a save: build the cubes from the saved positions and colors otherwise: make 20 random cubesComing back to a known state can happen in several ways:
| Strategy | Example | Good | Watch out |
|---|---|---|---|
| Every change | the cubes demo: after each drag, delete, or orbit | nothing gets lost | fine for small state, too much for things that change every frame |
| Every so often | autosave every 10 seconds | works for constantly changing state, like a moving player | you can lose the last few seconds |
| At key moments | checkpoints, finishing a level, a new high score | saves only states that make sense to return to | everything between moments is lost |
| When leaving | when the tab closes or reloads | one save, right at the end | a crash, or a phone closing the tab, may skip it. Use it as a backup only |
| When asked | a Save button, save slots | the person chooses what to keep, and can have several versions | forget to press it and the work is gone |
localStorage. Compare its list with our
table. Saving is only half of it: also ask it to load the saved state when the
page starts and rebuild the scene from it. Without that, the data sits on disk and the
scene still starts over.