A .nbt structure stores palette indexes, not block names — which is why it is so small
Three keys hold an entire build: size, palette and blocks. Each block is an integer pointing into the palette, so a 32³ volume of stone costs one string and 32,768 numbers.
A .nbt structure file — the thing a structure block saves and a datapack ships — is a much simpler format than it looks from the outside. Everything a build needs sits in a handful of top-level keys, and one design decision explains why the files are as small as they are.
The three keys that carry the build
size [x, y, z] — a list of exactly three integers
palette [ { Name, Properties } … ] — every DISTINCT block state, once
blocks [ { pos, state, nbt } … ] — one entry per placed block
palette holds each distinct block state exactly once. An entry is a Name — the block id, like minecraft:oak_stairs — plus an optional Properties compound holding its blockstate, such as facing: north and half: bottom.
blocks never repeats a name. Each entry carries a pos of three integers and a state, and state is an index into the palette, not a block id. Oak stairs facing north are string data once, and every one of them after that is a small integer.
That is the whole trick. A 32 × 32 × 32 volume of plain stone is one palette entry and 32,768 index numbers — not 32,768 copies of the word minecraft:stone. The palette grows with how many different things you built with, and the block list grows with how big you built. Those are very different numbers, and only one of them is usually large.
Two consequences worth designing around
Blockstates multiply your palette, not your block list. Oak stairs are not one palette entry — they are one per orientation you actually used. Facing four ways, top and bottom half, straight and both inner and outer corners: the same block can occupy a dozen palette slots. So a detail-heavy build has a big palette and a plain one does not, regardless of size.
Only placed blocks get an entry. The block list is a list, not a dense 3D array, and an entry that is not there is simply not there — the viewer on this page fills its grid with -1 and treats a missing coordinate as nothing placed. A hollow build is genuinely cheaper to store than a solid one of the same dimensions.
nbt is per-block and optional
Each entry in blocks may carry its own nbt compound, and most do not. This is where a chest's contents, a sign's text, a spawner's mob and a banner's patterns live — attached to the individual block rather than to the palette.
The distinction matters: palette data is shared and block data is not. Two chests are one palette entry, because they are the same block state — but each keeps its own inventory in its own nbt. Change the block state and you touch the palette; change what is inside it and you touch one entry in the block list.
Reading a layer at a time
The viewer here is deliberately 2D. To draw layer y it walks the block list, keeps entries whose pos[1] equals y, and looks each one's state up in the palette. That is the entire algorithm, and it is worth knowing because it is also how you would inspect one of these files yourself: there is no spatial index to build and no decompression beyond the gzip wrapper. The format is a flat list you filter.
Which also means a structure file tells you nothing about where it belongs in a world. size is a bounding box and pos values are relative to its corner. The placement is the structure block's business, not the file's.