Immediate mode: why the canvas forgets everything you draw
The single fact that explains almost everything strange about the 2D canvas is that it has no memory of what you drew — every command paints pixels into a bitmap and is instantly forgotten, leaving no shapes, no objects, nothing to move or click. This lesson makes that concrete, contrasts it with the retained-mode world of the DOM and SVG, and derives the consequences that the rest of the module lives inside.
Immediate mode: why the canvas forgets everything you draw
Almost every surprising thing about the 2D canvas — why you can't move a circle after drawing it, why there's no onclick for a shape, why animation means clearing the whole surface and redrawing it sixty times a second — comes from one root fact that the API never states out loud. Learn it first and the rest of the canvas stops being a bag of quirks and becomes a single coherent design.
Here it is: the canvas has no memory of what you draw. Every drawing call paints pixels into a bitmap immediately and then forgets everything — there is no circle, no rectangle, no object left behind, only colored pixels. This is called immediate mode, and it is the opposite of how the DOM and SVG work. Everything in this lesson is that sentence, unpacked.
Two ways a graphics system can work
There are two fundamental designs for a graphics API, and it's worth naming both because the canvas only makes sense as a deliberate choice between them.
Retained mode is what the DOM and SVG do. When you create a <div> or an SVG <circle>, you hand the browser a description of an object, and the browser keeps that description. It maintains a tree of these objects (the DOM), and it takes responsibility for turning that tree into pixels — re-rendering when something changes, figuring out what you clicked, redrawing after a scroll. You mutate the model (circle.setAttribute('cx', 50)) and the browser re-derives the picture. The system retains your objects.
Immediate mode is what the canvas does. When you call ctx.fillRect(10, 10, 100, 50), the canvas does not create a rectangle object. It rasterizes a filled rectangle into its pixel buffer right now, at that instant, and retains nothing about it. A moment later those pixels are indistinguishable from any other pixels — the canvas could not tell you "there is a rectangle at (10,10)" because as far as it's concerned there isn't one; there are just pixels that happen to be that color. You issue a command, it paints, it forgets. The system draws immediately and moves on.
Neither is "better" — they're suited to different jobs, a tradeoff we'll settle at the end. But you cannot use the canvas correctly without knowing which world you're in, because immediate mode pushes a set of responsibilities onto you that retained mode handles silently.
Watch the forgetting happen
The clearest way to feel immediate mode is to issue commands one at a time and watch pixels accumulate in a buffer that has no idea what produced them. Each command below paints and returns; the canvas keeps only the growing image, never the list of what drew it.
ctx.fillStyle = terracotta; ctx.fillRect(30, 30, 110, 60);ctx.strokeStyle = ink; ctx.lineWidth = 4; ctx.moveTo(30, 110); ctx.lineTo(280, 110); ctx.stroke();ctx.fillStyle = sage; ctx.arc(230, 70, 40, 0, 2*Math.PI); ctx.fill();ctx.fillStyle = ink; ctx.font = '20px sans-serif'; ctx.fillText('pixels', 40, 150);ctx.fillStyle = gold; ctx.fillRect(160, 130, 90, 40);Each command paints onto the canvas immediately, then is forgotten — there is no rectangle object or circle object sitting around afterward, only pixels. Once all five commands have run, the canvas holds a single flat bitmap; the shapes are not retained as shapes. That is why the draw effect above replays every command from 0 up to the current step on every change: to move or erase one shape, you must clear the whole canvas and redraw everything you still want, in order.
Step through it and notice: after the third command, the surface doesn't "contain a rectangle, a line, and a circle." It contains a bitmap. If you wanted to erase just the rectangle, there is no "rectangle" to erase — you would have to clear the region and redraw everything else that was supposed to be there. That single limitation is the source of the three big consequences below.
Consequence 1: to change anything, you clear and redraw
Because there are no retained objects, there is no such thing as "moving" or "editing" a shape. If a ball is at x=100 and you want it at x=110, you cannot nudge it — there is no ball. What you actually do is erase the whole canvas and draw the entire scene again with the ball one step further along:
function frame() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // wipe everything
x += 1; // update your own state
ctx.beginPath();
ctx.arc(x, 100, 20, 0, Math.PI * 2); // redraw from scratch
ctx.fill();
requestAnimationFrame(frame); // again next frame
}This is why canvas animation is inseparable from the frame loop: every frame is a full repaint, not an edit. In retained mode the browser would diff and repaint only what changed; in immediate mode you are the render loop, and "the scene" exists only as your own JavaScript variables plus whatever you choose to redraw. The pixels are downstream of your state, never a store of it.
Consequence 2: there is no hit-testing — you own that too
Click a <button> and the browser knows which element you hit, because it retained the element and its geometry. Click on a canvas and the browser knows only that you clicked the canvas — a single element — at some (x, y). It cannot tell you that you clicked "the red circle," because there is no red circle in any model; there are pixels.
So if you want clickable shapes on a canvas, you maintain your own list of logical objects ({type:'circle', x, y, r}) in JavaScript, and on a click you do the geometry yourself — loop your objects, test whether the point falls inside each. The canvas draws them; you remember them. This is the recurring shape of canvas code: a data model you own, plus a draw function that projects it onto the immediate-mode surface each frame. The canvas is a projector, not a database.
Consequence 3: the context is a sticky state machine
There's a subtler consequence of "issue a command, it paints now." A command like ctx.fill() needs to know what color to fill with — but fill() takes no color argument. Instead the color lives in the context as state you set beforehand: ctx.fillStyle = 'red'. And that state is sticky — it stays set until you change it, affecting every subsequent draw.
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 50, 50); // red
ctx.fillRect(60, 0, 50, 50); // STILL red — fillStyle persisted
ctx.fillStyle = 'blue';
ctx.fillRect(120, 0, 50, 50); // blueThis is the same shape as the WebGL state machine from the graphics pipeline module: a small pile of current settings (fillStyle, strokeStyle, lineWidth, the current transform, the clip region) that every immediate command reads from. It follows directly from immediate mode — since each command paints instantly with no arguments for style, the style has to already be sitting in the context. Managing that sticky state (and the save()/restore() stack that tames it) is its own lesson, but the reason it works this way is the one you now hold.
When immediate mode is the right choice
Given all these responsibilities land on you, why ever choose the canvas over the DOM? Because retained mode has a cost that immediate mode doesn't: the browser must store, lay out, and manage every retained object. A DOM tree with 10,000 nodes is slow — each node carries style, layout, event, and memory overhead. But drawing 10,000 shapes on a canvas is just 10,000 paint commands into one bitmap, with no per-shape object kept around at all.
So the tradeoff is clean: retained mode (DOM/SVG) when you have a manageable number of elements that benefit from built-in layout, events, and accessibility; immediate mode (canvas) when you have a huge number of visual elements, or need per-pixel control, and are willing to own the state and hit-testing yourself. Games, data visualizations with tens of thousands of points, image editors, particle systems, and drawing apps all live in the second world. That's the canvas's home, and immediate mode is why it's fast there.
Where this goes next
If the canvas is "just a bitmap you paint into," the next question is literally what is that bitmap — how big is it in real device pixels, how do its coordinates map to the CSS size on screen, and why does canvas art so often come out blurry until you account for the device pixel ratio. The pixel backing store takes apart the surface itself: the raw pixel buffer every command in this lesson was painting into.
Go deeper
- MDN — Canvas API basic usage — The getContext('2d') entry point and the first immediate-mode drawing calls this lesson is built on.
- MDN — Canvas API overview — The full surface of immediate-mode commands, and where the retained-mode DOM/SVG alternatives sit relative to it.
- web.dev — Improving canvas performance — Why the immediate-mode 'clear and redraw every frame' model is fast, and how to keep it that way — the performance thread this module ends on.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- State the single defining fact of immediate mode, and contrast it with what retained mode (DOM/SVG) keeps on your behalf.
- After ctx.fillRect(...) runs, in what sense does the canvas not 'contain a rectangle'?
- Why does animating a shape on a canvas require clearing and redrawing the whole scene, and how does that tie the canvas to the requestAnimationFrame loop?
- You want the shapes on your canvas to be clickable. What must you build yourself, and why can't the browser do it for you the way it does for a <button>?
- Why does ctx.fill() take no color argument, and what does that reveal about how the context stores drawing state?
- Give the tradeoff that decides between using the DOM/SVG and using the canvas for a given UI. Name two applications that clearly belong on the canvas and say why.
- What does a library like Fabric.js or Konva add on top of the raw canvas, and which mode is it reintroducing?