Get CV
Back to business cases

Data

A locale slab instead of a 2 MB JSON blob

Three locales in one JSON column inflated every listing. A packed slab in Memcached cut the worker heap in half.

The catalog had fifty thousand SKUs and three locales. Each row stored title and description as a JSON object with en, ru, and de. A listing of forty items pulled 2 MB of JSON out of MySQL, then json_decode() built a tree in the worker, then Vue SSR received the same tree again in the page payload.

The request asked for Russian. The worker still allocated English and German. That is waste you can see in RSS, and waste you can see in TTFB.

The problem was treating a grid as a document store. Visitors waited on decode and on HTML that carried two unused dictionaries. I needed one locale on the wire and a packed value the worker could slice without json_decode().

Header with offsets, then three locale slices. PHP reads one slice with substr().
Header with offsets, then three locale slices. PHP reads one slice with substr().

The slab

A slab is one Memcached value per listing page. The first bytes are a header: magic, item count, and an offset table. After that come three packed regions, one per locale. Each item is id, slug, price, and a length-prefixed title. No JSON. No Eloquent. A reader that wants ru jumps to the ru offset and walks forty records.

Building the slab is not free, so it does not happen on the request. A write in MySQL adds the SKU id to a Redis set named catalog:dirty. A Horizon job drains that set, rebuilds the pages that contain those ids, and SET the new slabs. Readers never wait on the job. They keep the previous slab until the new one arrives.

MySQL is the source of truth. Redis marks dirty ids. Horizon writes Memcached.
MySQL is the source of truth. Redis marks dirty ids. Horizon writes Memcached.

What Vue SSR stopped shipping

The SSR payload used to include the full localized object for every tile. After the slab, the server renders with one locale and serializes that locale only. The client hydrates the same strings. Switching language is a navigation, not a second dictionary in memory.

  • Listing payload dropped from about 180 KB HTML to about 40 KB for the same forty tiles.
  • json_decode() left the hot path.
  • MySQL lost the JSON extract on the listing query. The covering index on id, slug, price is enough for the job.

The trick is not exotic. It is the same idea as a binary protocol, applied to a catalog that looked like a document store and behaved like a list. JSON is the admin format. The public path reads bytes.

What I took from this

A listing is a range of scalars. JSON is a convenient write format. Putting both on the public path is how 2 MB of unused locales land in the worker and in the document.

Rebuild off the request. Readers keep the last slab. Dirty ids in Redis are cheaper than json_decode() on every hit.

Language switch as navigation matches how people actually change locale. Shipping three dictionaries for a client-side swap was a cost nobody paid for.

Back to business cases