We cache every stone as a sprite, then throw it away
A pebble in Pebble Merge is not a circle. It is a tumbled, polished, faceted, color graded shape with speckles and a banding and a rim light, and drawing that shape fresh every frame would eat the frame. So we draw it once, keep the picture, and paste it. Then a resize erases all of it.
A stone is expensive to draw
Look at what one stone costs. The body color is interpolated by polish. The rough ones get mineral speckles and river worn banding. The high tiers get a translucency bloom and a set of flat facets lit from one fixed direction. None of that is a single fill call. It is a dozen gradient stops and a handful of masked ellipses, and it runs in a loop of noise so two pebbles of the same tier are never quite identical. Done every frame, for every pebble on the board, during a cascade, the cost is real. The calm promise of the game depends on a steady frame, and the steady frame depends on not rebuilding a gem forty times a second.
The fixed direction matters here. Because the light never moves, the stone never has to be redrawn to catch a new angle. That single choice, light locked to the world instead of the camera, is what makes caching possible at all, and it is the kind of decision that pays off in three different files before you realize it was the same decision each time, because a locked light is a cached stone is a steady frame is a calm game, and they are all one sentence wearing four outfits. I did not plan it as a caching decision. I planned it as a calm decision. The performance win was a side effect of refusing to let the world hurry, which is the only honest way most of my good performance wins have ever arrived, never as the thing I set out to buy.
So we draw it once
The renderer keeps a dictionary keyed by tier and variant. When it needs a pebble it asks for the sprite; if that key is missing it builds the canvas once and stores it, and every later request for the same key pastes the stored picture. Build once, blit forever. There are eleven tiers and four silhouette variants per tier, so the whole dictionary is forty four small canvases at most, and most boards never show the top two tiers at all, so in practice the cache stays tiny. Tiny enough that I stopped worrying about its memory the day I counted it.
Variant is the quiet part. Two pebbles of the same tier should not look cloned, so each gets one of four silhouettes chosen by its id, and the id is stable, so a pebble keeps its shape for its whole life on the board, which matters more than it sounds because a shape that flickers between drops reads as a bug even when it is working exactly as designed and the player is not wrong to distrust it. Four variants is a guess, not a measurement. I tried two and the repeat was visible in a full board; I tried six and could not tell the difference from four. Four. The kind you make by eye and then forget to write down, which is the part I keep failing at, the writing down, not the guessing. kind of number you pick by eye and never revisit, except now it is written down, which is the part I keep failing to do.
The resize that throws it all away
On resize the renderer empties the dictionary. All of it. Not just the entries that changed, the entire thing, and the static backdrop cache beside it, in the same breath. The reason is honest: the sprite was built at a specific pixel scale, and after a resize the scale is different, so the old pictures would be the wrong size, and pasting a wrong size picture is worse than rebuilding. So we throw the work out and let the next frame pay to rebuild only what the board actually shows. A board mid game shows maybe eight tiers. The rebuild is therefore a handful of canvases, not forty four.
The backdrop is the bigger loss on paper. It is a full screen underwater scene, light shafts and drifting caustics, cached to an offscreen canvas because it never changes within a round, a scene that took real care to make look like a place rather than a gradient and that I am still quietly proud of. Wiping it means redrawing the whole scene once. Once, and only when the player has already been interrupted by the resize, so they will not notice the cost, because the view was already moving when the cost arrived, and a cost that arrives during an interruption is a cost nobody accounts to you. The scene is static by design, so it is the same every round, and building it again on resize is a fraction of a second the player will not notice because the resize itself already interrupted the view.
What I was afraid of
I was afraid of the wipe. Emptying a cache on every resize event sounded like a stutter waiting to happen, because resize fires a lot, especially on a phone where the address bar showing and hiding counts as a resize, which means it fires while you are just reading the page. My instinct was to be clever: diff the keys, keep the ones that still fit, rebuild only the changed scale. I almost wrote that. Then I timed the naive version, the one that throws everything and rebuilds what is visible, and it was instant, because the visible set is small and the builds are cheap individually and the browser does this sort of thing all day.
The clever version would have been more code, one more place to be wrong, and a diffing bug I would have found only on a device I do not own. The dumb version is twelve characters and cannot lie. I kept the dumb version. I am not proud of how often the lesson is "measure the dumb version before inventing the smart one," but it is true often enough that I have stopped trusting my fear of the obvious approach. The fear is usually about a cost that does not exist at this scale.
There is a habit buried in this file that I am trying to make a rule. Cache the thing that is expensive and static. Wipe the cache when its assumptions change, completely, if the rebuild is cheap. Do not protect a cache from a resize it was never going to survive. A cache you are afraid to invalidate is a cache that will eventually serve a wrong picture, and a wrong picture is worse than a brief rebuild in every case I have actually measured. We cache every stone. We throw them all away on resize. Both sentences are the same decision seen from either end.