500字节能干什么?
一条微信文字消息,大约500字节。一张手机拍的最小尺寸缩略图,也得几KB起步。但最近,开发者 Iwo Kadziela 做了一件反直觉的事:他用不到500字节的数据,画出了一张可辨认的世界地图。
注意,是可辨认——你能看清七大洲的轮廓,分得出非洲和南美。不是那种少到只剩两个圆圈的”抽象艺术”。
笔者看到这个项目的第一反应是:这怎么可能?一张地图需要描述大陆边缘的每一个弯曲、每一个半岛、每一个海湾。即便只保留最粗略的轮廓,SVG 路径坐标数也轻松破千。500字节?连塞坐标都不够。
然后笔者读完了原作者的博客文章——发现整个故事的关键转折,恰恰在于”SVG试过了,失败了”。
反派登场:坐标数 vs 字节限额
这里有一个根本性的张力:地图的视觉精度,和你能用来描述它的字节数,本质上是对手。
一张真实的世界地图,核心是海岸线——那些弯曲、凹入、凸出,而不是色块填充。简化地图数据,通常的思路是用 Douglas-Peucker 算法:遍历路径上的点,找到距离基线最远的那个点,如果距离超过了容差阈值就保留,否则丢弃中间所有点。反复递归,直到整条路径被精简到最少的关键点集。
这个算法在”去掉95%的点但肉眼看不出来”的场景下效果很好。问题是,当你把容差调到足够大——大到只剩几十个坐标点时——地图就不再像地图了。非洲变成了三角形,南美洲成了一个椭圆,东南亚群岛直接消失了。
原作者正是从 SVG 路径出发的。他用 AI 编码助手 Codex 尝试了多种 SVG 方案,结果一致:一旦路径简化到500字节以内,形状已经面目全非。
SVG 路径有一个冷酷的代价规则:每一个坐标都要为它的精确性付费。 一个典型的 SVG 路径命令,比如 M123.4 567.8 L234.5 678.9,光是坐标数字就要消耗十几到二十几个字符。一个大陆的轮廓线,少则几十个点、多则几百个。算上 SVG 标签开销(<path d="..." />),500字节只能容纳大约20-30个坐标点——这对世界地图来说远远不够。
工程判断:Douglas-Peucker 在”适度简化”区间表现优秀,但500字节的极端约束把简化比推到了算法能力的边界之外。路径简化在这里碰到的已经不是精度问题:表达方式本身出了问题。
转机:放弃 SVG,拥抱 ASCII
Codex 最终回到了一个看起来更”原始”的方案:ASCII 字符画。
做法直接得令人意外:用一个字符矩阵表示地球表面,* 代表陆地,空格代表海洋。长这样(缩略示意):
*****
*******
*********
***********
*********
*******
*****
这个方案的优势在哪?不是更精确——ASCII 网格的分辨率远低于 SVG 坐标。优势在于:每个人眼觉得”重复”的东西,压缩算法也觉得重复。
原作者做了三个关键优化:
第一刀,砍掉海洋。 最初的 ASCII 地图用 . 表示水、* 表示陆地。原作者意识到,识别地图时人们只看大陆块在哪里,水是什么符号根本无关。删掉所有海洋符号后,整张图变成了稀疏的陆地斑块——但它节省了大量无用字符。
第二刀,裁剪画布。 原始地图左右两侧有大片空白海洋。把画布裁到刚好包住所有大陆,进一步缩小了字符矩阵。
第三刀,选择填充而非轮廓。 这是一个关键决策。原文作者尝试过只保留大陆轮廓线(细线条勾勒形状),理论上信息密度更高。但实际效果却更差——为什么?因为轮廓线在字符矩阵里表现为稀疏分布的 *,之间隔大量空格。而填充的大陆块产生了长串连续的 *。压缩算法不擅长处理”一个字符、一个空格、再一个字符”的稀疏模式,但它极擅长处理”连续100个 *”这样的重复序列。
这三刀下去,ASCII 地图的原始大小是8523字节——作为纯文本,不算小。但好戏在后头。
核心引擎:deflate 为什么能让8523字节变成445字节
压缩前:8523字节。压缩后:445字节。压缩比接近95%。
这不是魔法,是 deflate 算法对 ASCII 地图这种数据天然适配的结果。
deflate 是 ZIP 和 gzip 使用的压缩算法,它由两阶段组成:
第一阶段:LZ77(字典压缩)。 算法从左到右扫描数据,寻找之前出现过的重复序列。找到后,用”往回看 N 个字节,复制 M 个字节”的指令代替原始数据。一行的 **** 和下一行的 **** 是重复的,一块大陆内部的连续 * 更是重复的。ASCII 世界地图里,大陆块的每一行都和前一行高度相似——LZ77 对这种”相邻行之间的冗余”处理效率极高。
第二阶段:Huffman 编码(熵编码)。 LZ77 产生的输出里,某些符号(比如”重复长度=1”、“往回距离很小”)出现的频率远高于其他符号。Huffman 编码给高频符号分配短码(2-3比特)、低频符号分配长码,进一步压缩。
这里有一个值得展开的细节:填充大陆 vs 轮廓线的压缩差异。填充大陆产生的是大块连续 *,LZ77 用一个指令就能描述”往后复制整行”。轮廓线则是 * 和空格的交替,LZ77 找不到足够长的重复序列,只能退回到逐字符编码,压缩率大幅下降。
原作者在文章中写得直白:“We tested removing the filled interiors and keeping only continent outlines, but that actually compressed worse.”——填充反而比轮廓压缩得更好,这在直觉上是反过来的,但理解压缩算法的内部机制后,它合情合理。
工程判断:选择压缩友好的数据表示,比直接削减数据本身更有效。 填充大陆的数据量更大(8523字节 vs 轮廓线的可能只有3000字节),但压缩后的结果更小。这意味着在极端字节约束下,优化目标不应该是”让原始数据变小”,而是”让压缩后的数据变小”——这两个目标有时是矛盾的。
浏览器怎么解压
最终的实现极其精简。HTML 文件本体不到1KB,核心逻辑只有一行 fetch 调用:
fetch('data:;base64,...')
.then(r => r.body.pipeThrough(new DecompressionStream('deflate-raw')))
.then(s => new Response(s).text())
.then(t => b.innerHTML = '<pre>' + t)
工作流程:Base64 解码 → 浏览器内置的 DecompressionStream API(deflate-raw 模式)解压 → 把解压后的 ASCII 文本塞进一个 <pre> 标签 → 用 font-size: 0.65vw 让字符足够小,使地图在任意屏幕上完整可见。
值得留意的是,这里用的是浏览器原生 API。DecompressionStream 在 Chrome 80+、Firefox 113+、Safari 16.4+ 上都可以使用,不需要额外引入任何 JS 库,零依赖——这本身也为总字节限额争取了空间。
从500字节里学到的
笔者从这个实验里提炼出三条思考,它们和日常工作中遇到的优化问题其实相通:
一是表达方式决定压缩上限。 如果你用 SVG 路径坐标表示地图,无论怎么简化,每个坐标都在单独消耗字节。而 ASCII 字符矩阵天然具备行间冗余——压缩算法的设计正是为了捕获这种冗余。选择哪种表达方式,比用什么压缩参数更根本。
二是”原始数据越小越好”是直觉陷阱。 填充大陆的原始大小比轮廓线大,但压缩后更小。这个反直觉的结果说明:你需要理解压缩算法的工作方式,才能设计出真正适合压缩的数据结构。只看原始大小做决策,可能走向错误的方向。
三是约束本身可以成为创造力引擎。 500字节不是一个实用需求——现实中没有人需要把地图塞进一条微信消息的长度。但这个荒谬的约束迫使作者放弃了”把 SVG 画得更精简”的常规思路,转而追问”压缩算法到底喜欢吃什么”。限制越紧,越能暴露惯性思维的盲区。
压缩过的世界地图,仍然是世界地图。
但不是因为数据被削减了——数据的组织方式恰好和压缩算法的胃口对上了。500字节不是画出来的,是”骗”出来的:骗过压缩算法的代价函数,让它以为自己在高效存储重复数据,而实际上存储的是一张能认出来的世界。
参考链接:
- https://www.experimentlog.com/blog/building-a-world-map-with-only-500-bytes
- https://news.ycombinator.com/item?id=48747762
- https://github.com/piwodlaiwo/smallest-world-map
本文技术分析基于原作者 Iwo Kadziela 的博客文章、GitHub 仓库源码以及 HN 社区讨论。