Почему Chrome весит 7 000 Марио или как сжать «Змейку» в 1 000 раз
Раньше Super Mario Bros весила 40 КБ. Сейчас одно обновление Chrome — это ~7 000 таких Марио. Как мы дошли до жизни такой, и почему все идет по кругу?
Один скриншот тяжелее целой игры
Начнем с интересного факта. Super Mario Bros. на NES — одна из самых продаваемых игр в истории — занимала 40 КБ. Это 32 КБ программного кода и 8 КБ графики. В этих 40 КБ уместились 32 уровня, десятки врагов, физика, музыка и тот самый звук монетки (который вы, возможно, сейчас услышали в голове).
Скриншот этой игры в PNG весит больше, чем вся игра целиком.
Теперь к расчетам. Установочный файл Google Chrome весит около 140 МБ. После установки, с учетом кеша и служебных файлов, объем разрастается до 280 МБ. А 286 720 КБ ÷ 40 КБ, эквивалентны, примерно, 7 000 копям Super Mario Bros.

То есть браузер весит как 7 000 полноценных видеоигр 1985 года.
Но Chrome — это еще цветочки. ARK: Survival Evolved со всеми DLC занимает 435 ГБ, а это уже около 11 млн копий Марио. Причем разработчики ARK не удосужились сделать общий репозиторий ассетов: каждое DLC тащит за собой дубликаты моделей. У вас на диске буквально семь одинаковых моделей птицы Додо.
Хаб Call of Duty (со всеми загруженными кампаниями последних частей и Warzone) переваливает за 250–300 ГБ. Microsoft Flight Simulator — 250+ ГБ. И это не какой-то уникальный графический шедевр в каждом пикселе, а в значительной степени дублирование ассетов и расчет на дешевые терабайты.

Как же мы пришли из точки «целая игра в 40 КБ» в точку «одно обновление стрелялки весит как маленькая библиотека»? И главное — как мы возвращаемся обратно?
Как раньше умещали слона в спичечный коробок
Тайловая магия NES
Разработчики Super Mario Bros. не хранили уровни как картинки. Вместо этого весь визуал состоял из тайлов — крошечных блоков 8×8 пикселей. Облако, куст, холм — это один и тот же тайл, просто перекрашенный через палитру. Куст Марио — это дефолтное облако, опущенное на землю и покрашенное в зеленый.

Весь спрайтшит игры — это несколько сотен таких восьмипиксельных квадратиков, которые комбинируются и переиспользуются бесконечно. Picture Processing Unit консоли сам собирал из них картинку, разработчикам нужно было только сказать: «поставь тайл #14 в координату (X, Y) с палитрой N».
А уровни? Они не хранились целиком. Вместо матрицы тайлов использовались команды: «отсюда начинается земля», «здесь поставь трубу высотой 3», «включи паттерн неба». По сути, это процедурная инструкция, а не растровая карта.
Палитра как прием
NES поддерживал всего 25 цветов на экране одновременно, и разработчики научились извлекать из этого максимум — меняя палитру, они, в действительности, меняли визуальный стиль целой локации. Они могли использовать одни и те же спрайты для подземелья и замка, но в результате локации выглядели совершенно по разному.

Музыка без аудиофайлов
Звуковой чип NES (APU) имел пять каналов: два прямоугольных, треугольный, шумовой и DPCM. Никаких MP3, WAV или OGG. Музыка в игре — это ноты и команды для синтезатора. Весь саундтрек Super Mario Bros. — каждая мелодия и каждый звуковой эффект — занимает пару килобайт инструкций.
Для сравнения: один несжатый сэмпл удара барабана в современной игре весит больше, чем весь звуковой код Марио.
Трюки 90-х и 00-х для оптимизации
С ростом мощности железа росли и аппетиты разработчиков, но некоторые умудрялись вмещать графику и звук в смешные по нынешним меркам объемы.
.kkrieger — шутер в 96 КБ
В 2004 году немецкая демогруппа .theprodukkt выпустила .kkrieger — полноценный 3D-шутер от первого лица, который занимал 96 КБ. Это меньше, чем аватарка в Telegram.
Внутри нет ни одного готового ассета. Все текстуры, модели, звуки и музыка генерировались процедурно и прямо при запуске. Вместо хранения текстуры как растра использовалась история ее создания — последовательность операций: «возьми шум Перлина, наложи размытие, добавь дисторсию». Пара десятков байт инструкций превращались в мегабайты визуала.
Они не хранили готовые текстуры и спрайты — вместо этого движок генерировал их при запуске по математическим формулам. Поэтому .kkrieger весит 96 КБ, в нем были только инструкции по созданию контента.
А вот если бы .kkrieger хранил ассеты традиционным способом, он бы весил 200–300 МБ — как современная небольшая инди-игра. А так — 96 КБ. Скриншот этого шутера весит больше, чем сам исполняемый файл.

Кармак и его черная магия
Джон Кармак в id Software тоже не разбрасывался ресурсами. Doom (1993) использовал BSP-деревья для разделения уровней — движок быстро отбрасывал невидимые полигоны, чтобы не нагружать процессор лишней отрисовкой. Quake (1996) ввел PVS — заранее рассчитанный набор того, что может видеть игрок из каждой точки карты. Если стены закрывают комнату, то она вообще не рисуется.
Эти техники экономили и память, и трафик, и вычислительные ресурсы.
Трюки с LOD и импостерами
Level of Detail — древний, но рабочий подход: далекие объекты рендерятся упрощенными моделями. Дерево вдали — это не 50 000 полигонов с текстурами листьев, а прямоугольник с фотографией дерева (импостер).
| Дистанция < 10м: | полная модель (50 000 полигонов) |
| 10–50 м | упрощенная (5 000) |
| 50–200 м | billboard (импостер, 2 треугольника) |
| > 200 м | не рендерится вообще |
Этот подход до сих пор используется в каждой современной AAA-игре.
MIP-маппинг
Зачем хранить текстуру 4 096×4 096 для объекта, который на экране занимает 16 пикселей? MIP-цепочки — это набор заранее рассчитанных уменьшенных копий текстуры. GPU автоматически выбирает нужный уровень в зависимости от расстояния. Это экономит VRAM и убирает мерцание далеких текстур.
Эпоха «можно, а зачем»
Настало будущее: жесткие диски стали дешевыми, интернет — быстрым, а дедлайны — жесткими. На оптимизацию стали забивать сразу по нескольким фронтам.
К этому добавляется, во-первых, избыточная детализация: современные AAA-игры поставляются с несжатыми 4K-текстурами для объектов, которые игрок никогда не рассмотрит вблизи. Какой-нибудь мусорный бак в переулке, куда игрок не заглянет, имеет 4 096×4 096 пикселей, восемь PBR-каналов и весит 64 МБ.
Во-вторых, ресурсы перестали упаковывать плотно. Вместо одной аудиодорожки и декодинга на лету применяются предекодированные версии для каждой платформы, каждого формата, каждой частоты дискретизации. Для разработчика это экономит время: не нужно писать конверт под каждую платформу (и заодно следить за всеми совместимостями). Платит за это уже пользователь лишними гигабайтами на своем диске.
А Unreal Engine 4/5 при первом запуске компилирует шейдеры локально, создавая гигабайты кеша. Это ускоряет рендер, но вопрос «а может, скомпилировать заранее?» как будто не приходит в голову.
Средний размер AAA-игры вырос в 100–150 раз за 20 лет. Для сравнения: культовая Max Payne 2 в 2003 году занимала на жестком диске около 1,5 ГБ. Call of Duty: Black Ops 6 требует 102 ГБ, но для полной установки с лаунчером CoD HQ и Warzone потребуется уже порядка 150 ГБ. А со всеми компонентами и кэшем размер может переваливать за 300 ГБ. Графика стала лучше, безусловно. Но в 100 раз? Серьезно?
Моя попытка: оптимизация змейки тремя эпохами
Теория — это хорошо, но я хотел пощупать руками, как каждая эпоха оптимизации чувствуется на практике. Поэтому я собрал ассет-пайплайн для классической змейки в трех вариантах. Геймплейную логику писать не стал, нас интересует только вес данных и три философии хранения:
- FAT — как писали бы в 2020-х, не думая о размере;
- OLDSCHOOL — как писали бы в 90-х, экономя каждый байт;
- AI — как пишут в 2026, с ИИ (или как будут писать завтра).
Змейка-FAT
Первая версия нарочито раздутая.
- Графика — 16 отдельных файлов (64×64 пикселя) в формате 32-bit RGBA. На диске PNG ужмет их до ~30 КБ, но в распакованном виде (raw RGBA, как они лежат в памяти при рендере) это 256 КБ — именно этот размер и считается «настоящим» для GPU, так что будем использовать это значение.
- Звук поедания яблока — эффекты в WAV (44,1 кГц; 16 бит), потому что «а вдруг кто-то услышит разницу»;
- Карта уровня — полный JSON-файл с подробным описанием координат и свойств каждой клетки;
- Музыка — полноценный фоновый трек в формате MP3 (три дорожки: eat_sound, die_sound и bgm).
Каждый спрайт — отдельный файл.

Итоговый размер ассетов: ~3.7 МБ.
Змейка-OLDSCHOOL: палитры, RLE, синтез
Теперь перевоплощаемся в программиста, у которого каждый байт на счету.
Шаг 1: Спрайтатлас + палитра
Все 16 спрайтов упакованы сразу в один файл. Вместо 32-битного RGBA будет индексированная палитра на 16 цветов. Змейка и так пиксельная, так зачем ей куча оттенков?
from PIL import Image
def build_atlas_indexed(sprite_paths, palette_size=16):
"""Собираем все спрайты в один атлас с общей палитрой"""
# Склеиваем в одну полосу
sprites = [Image.open(p).convert('RGBA') for p in sprite_paths]
total_w = sum(s.width for s in sprites)
atlas = Image.new('RGBA', (total_w, sprites[0].height))
x = 0
for s in sprites:
atlas.paste(s, (x, 0))
x += s.width
# Квантизация: 16.7М цветов → 16 цветов
indexed = atlas.quantize(colors=palette_size, method=Image.MEDIANCUT)
return indexed

Шаг 2: Процедурный фон
Текстуру травы можно сгенерить из 28 байт:
import numpy as np
from scipy.ndimage import zoom
def generate_grass(size=512, seed=42):
"""Трава из шума — 28 байт инструкций вместо 48 КБ текстуры"""
rng = np.random.RandomState(seed)
texture = np.zeros((size, size))
for octave in range(6):
freq = 2 ** octave
amp = 0.5 ** octave
noise = rng.rand(freq, freq)
texture += zoom(noise, size / freq, order=1)[:size, :size] * amp
# Нормализация → зеленая палитра
t = (texture - texture.min()) / (texture.max() - texture.min())
return np.stack([
(t * 25 + 15).astype(np.uint8), # R — приглушенный
(t * 80 + 40).astype(np.uint8), # G — доминирующий
(t * 20 + 10).astype(np.uint8), # B — почти нет
], axis=-1)

Шаг 3: Звук через синтез
Вместо WAV-файлов у нас будут ноты и команды. Хруст яблока, конечно, в три ноты уместить сложно, мягко говоря, но попробовать можно.
import mido
def create_eat_sound():
"""Звук поедания яблока: 3 ноты = 48 байт вместо 86 КБ WAV"""
mid = mido.MidiFile()
track = mido.MidiTrack()
mid.tracks.append(track)
track.append(mido.Message('program_change', program=80)) # Square lead
for note, dur in [(72, 60), (76, 60), (84, 120)]: # C-E-C октавой выше
track.append(mido.Message('note_on', note=note, velocity=100, time=0))
track.append(mido.Message('note_off', note=note, time=dur))
return mid # 48 байт
def create_bgm():
"""Фоновая музыка: 1.2 КБ вместо 3.2 МБ"""
mid = mido.MidiFile(ticks_per_beat=480)
track = mido.MidiTrack()
mid.tracks.append(track)
track.append(mido.Message('program_change', program=81))
pattern = [60, 63, 67, 72, 67, 63] # Cm арпеджио
for bar in range(32):
transpose = [0, 0, 5, 3][bar % 4]
for note in pattern:
track.append(mido.Message('note_on', note=note+transpose,
velocity=80, time=120))
track.append(mido.Message('note_off', note=note+transpose, time=120))
return mid # ~1.2 КБ
Шаг 4: Карта через RLE
Карта змейки — это в основном пустое поле с границами. Идеальный случай для RLE:
def rle_encode(grid):
"""Run-Length Encoding: 'AAAAABBBCC' → '5A3B2C'"""
flat = [cell for row in grid for cell in row]
encoded = []
i = 0
while i < len(flat):
val = flat[i]
count = 1
while i + count < len(flat) and flat[i+count] == val and count < 255:
count += 1
encoded.extend([count, val])
i += count
return bytes(encoded)
# Поле 30×20 = 600 клеток
# Наивно: 600 байт
# RLE: 112 байт (бордюр из стен + строки по шаблону «1 стена, 28 пустых, 1 стена»)
Итого после олдскула:
| Компонент | FAT | OLDSCHOOL | Сжатие |
| Спрайты (16 шт) | 256 КБ | ~2 КБ | ×128 |
| Фон (текстура) | 48 КБ | 28 байт | ×1 700 |
| Звуки + музыка | 3,4 МБ | ~1,2 КБ | ×2 800 |
| Карта | 2,4 КБ | 112 байт | ×21 |
| Итого ассеты | ~3,7 МБ | ~3,3 КБ | ×1 120 |
По сути, мы получили ту же змейку, но в 1 120 раз меньше.
Змейка-AI: автоэнкодер сжимает спрайты
Олдскул впечатляет, но там все приходилось делать вручную. А что, если нейросеть сделает это за меня?
Я обучил маленький автоэнкодер на спрайтах змейки. Идея в том, чтобы скормить ИИ все 16 спрайтов, пусть выучит «сущность» каждого, а потом восстанавливает их из крошечного латентного кода.
import torch
import torch.nn as nn
class SpriteAutoencoder(nn.Module):
"""
Автоэнкодер для спрайтов — карманный .kkrieger.
Вход: 64×64×3 = 12 288 float (48 КБ)
Латент: 32 float (128 байт)
Сжатие одного спрайта: ×375
"""
def __init__(self, latent_dim=32):
super().__init__()
# Сверхлегкий энкодер
self.encoder = nn.Sequential(
nn.Conv2d(3, 8, 4, stride=2, padding=1), # → 32×32
nn.ReLU(),
nn.Conv2d(8, 16, 4, stride=2, padding=1), # → 16×16
nn.ReLU(),
nn.Conv2d(16, 16, 4, stride=2, padding=1), # → 8×8
nn.ReLU(),
nn.Conv2d(16, 8, 4, stride=2, padding=1), # → 4×4
nn.ReLU(),
nn.Flatten(),
nn.Linear(8 * 4 * 4, latent_dim)
)
# Декодер на ~12 800 параметров (около 50 КБ)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 8 * 4 * 4), # 32 → 128 параметров
nn.Unflatten(1, (8, 4, 4)),
nn.ConvTranspose2d(8, 16, 4, stride=2, padding=1), # → 8×8
nn.ReLU(),
nn.ConvTranspose2d(16, 16, 4, stride=2, padding=1), # → 16×16
nn.ReLU(),
nn.ConvTranspose2d(16, 8, 4, stride=2, padding=1), # → 32×32
nn.ReLU(),
nn.ConvTranspose2d(8, 3, 4, stride=2, padding=1), # → 64×64
nn.Sigmoid()
)
def forward(self, x):
return self.decoder(self.encoder(x))
На обучение ушло 500 эпох и около трех минут работы GPU. В итоге вместо самих картинок мы храним 16 латентных кодов (это ровно 2048 байт) и веса декодера — легкую сверточную сеть объемом около 50 КБ.
Суммарный вес ассетов составил примерно 52 КБ. За это мы получаем 16 спрайтов, которые восстанавливаются на видеокарте меньше чем за миллисекунду и визуально почти не отличаются от оригинала.

Очевидно, что для 16 спрайтов змейки, автоэнкодер — это как забивать гвозди микроскопом. Змейка получилась практически идентичной оригиналу уже на первых шагах (я до сих пор улыбаюсь от осознания того, на каком массиве я пытался обучить сетку). 50 КБ декодера вместе с латентными кодами против 2 КБ атласа — олдскул выигрывает. Нейросеть здесь проигрывает, потому что оверхед весов декодера слишком велик для такого маленького датасета.
Но. Если бы спрайтов было не 16, а 2 000 (как в нормальной инди-игре) — расклад бы перевернулся:
| Количество спрайтов | Атлас с палитрой | Автоэнкодер (декодер 50 КБ + латенты по 128 байт) |
| 16 | 2 КБ | 52 КБ |
| 200 | 25 КБ | 75 КБ |
| 2 000 | 250 КБ | 300 КБ |
| 20 000 | 2,5 МБ | 2,55 МБ (тут они сравнялись) |
Да, на малом объеме нейросеть проигрывает, но прелесть нейросети в том, что размер декодера фиксирован. Чем сложнее и разнообразнее ассеты, тем меньше 50 КБ декодера стоят относительно размера данных.
На сложных PBR-текстурах с коррелированными каналами латент реально компактнее палитры — там AI уже выигрывает, а не просто догоняет.

Три змейки — финальный счет
| FAT | OLDSCHOOL | AI | |
| Спрайты | 256 КБ | 2 КБ | 52 КБ |
| Фон | 48 КБ | 28 байт | 28 байт |
| Звуки + музыка | 3.4 МБ | 1,2 КБ | 1,2 КБ |
| Карта | 2.4 КБ | 112 байт | 112 байт |
| Итого | ~3,7 МБ | ~3,3 КБ | ~53,3 КБ |
| Сжатие | — | ×1 120 | ×70 |
На масштабе змейки олдскул сделал всех. Это честный результат, и я не собираюсь его приукрашивать.
Но вот что важно: олдскул не масштабируется. Я потратил полчаса (я серьезно), вручную рисуя спрайты. Для 2 000 спрайтов мне бы потребовалось… 62,5 часа ручной работы — и это если рисовать также, как я рисовал змейку. А нейросеть обучится за те же четыре минуты.
Круг замкнулся: нейросети как новые демосценеры
Мы за статью прошли путь от ручной побайтовой оптимизации (80-е) через эпоху «кому вообще нужна оптимизация» (2010-е) и вернулись обратно, но теперь с нейросетями на борту.
Neural Texture Compression: текстуры через нейросеть
В апреле 2025 года NVIDIA представила обновленный NTC, а Intel следом показал собственный TSNC. Идея одна: вместо того, чтобы хранить текстуру попиксельно и сжимать блоками (BC1/BC7), мы обучаем маленькую нейросеть воспроизводить эту текстуру.
При рендере GPU запускает крошечную нейросеть прямо в шейдере, которая из латентного кода восстанавливает текстуру на лету. Intel заявляет о сжатии до 18× по сравнению с BC-форматами, а NVIDIA показала снижение потребления VRAM на 85% при сохранении визуального качества.
Мой автоэнкодер для змейки — это детская версия того же принципа. Разница в масштабе: Intel и NVIDIA обучают нейросети на наборах из тысяч PBR-текстур в семи слоях — нормалями, шероховатостью и тенями (окклюзией). А я — на 16 спрайтах змейки. Но математика одна.
Нейросетевой суперсэмплинг
DLSS, FSR, XeSS — все они позволяют рендерить картинку в низком разрешении и «дорисовывать» нейросетью.
- Без DLSS — рендер в 4K (8,3M пикселей) → вывод;
- С DLSS — рендер в 1080p (2,1M пикселей) → нейросеть → вывод в 4K.
Четырехкратная экономия. Кармак бы одобрил.
Нейросеть использует приемы разработчиков из 2004
Еще раз вспомним .theprodukkt Просто сравните:
- 2004 год, .theprodukkt — программист вручную пишет генератор текстуры камня: «возьми шум Перлина, наложи 3 октавы, примени gradient map». На это уходили сотни часов работы на каждый материал;
- 2025 год, Intel TSNC / NVIDIA NTC — нейросеть смотрит на готовую текстуру камня и сама выводит алгоритм ее воспроизведения, обучаясь за минуты, а не за сотни часов.
Да, нейросеть делает буквально то же, что и разработчики из 00-х. Только она масштабируется на десятки тысяч ассетов без человеческого участия.
Что это значит для разработчиков (и для ваших SSD)
Мой эксперимент со змейкой — это микрокосм всей индустрии. FAT-версия — как современная AAA-игра, где ресурсы не важны. OLDSCHOOL — как NES-эра, когда дорог каждый байт. А AI-версия — то, к чему мы идем.
Видите паттерн? Ручная оптимизация → Изобилие ресурсов → Раздувание → Снова оптимизация (но уже с ИИ).
Краткосрочно (2026–2028). Neural Texture Compression станет стандартом в движках. DirectX Cooperative Vectors уже в preview. Размеры игр начнут уменьшаться впервые за десятилетие.
Среднесрочно (2028–2030). Процедурная генерация ассетов с помощью ИИ вернется на уровень движка. Вместо 300 ГБ текстур — генеративная модель на 5 ГБ, которая создает все ассеты на лету. Инди-разработчики получат инструменты, которые раньше были доступны только демосценерам.
Долгосрочно. В 1985 году Шигеру Миямото уместил целый мир крохи водопроводчика в 40 КБ, потому что у него не было другого выбора. В 2004-м Farbrausch уместили шутер в 96 КБ, потому что могли и хотели доказать это миру. В 2020-х индустрия раздула игры до сотен гигабайт, потому что «а зачем экономить».
История повторяется: когда ресурсы заканчиваются, разработчики начинают думать. Сейчас ресурсы снова заканчиваются, только теперь кончается терпение пользователей, которым надоело качать 150 ГБ ради пятичасовой кампании.
P.S. Пока писал эту статью, Chrome снова обновился. Еще +15 МБ. Это 375 копий Марио.