Почему 14 KB?
Вот в пример реальные запросы.
Вот сайт который влезает в 14кб:

А вот который не влезает:

Download - это и есть контент сайта.
1 ms против 63 ms - разница скорости в 63 раза, при разнице веса всего 7 раз.
С хуя ли?
Ета писанина по верхам о потрохах TCP, DNS, TLS, slow start, congestion collapse 1986 года. И заодно филососия small web: почему маленький сайт - это уважение к пользователю, а не дрочка на циферки.
ну и немного про решения на sccl.cc в конце.
Чотамкак TCP
Прежде чем сервер отдаст хоть байт HTML, происходит овердохуя всего.
Три рукопожатия
Любое TCP-соединение начинает с three-way handshake:
- Клиент шлёт
SYN - Сервер отвечает
SYN-ACK - Клиент добивает
ACK
Три сообщения туда-сюда. И только потом можно слать данные.
Это +1 RTT просто на то чтоб поздороваться.
Есть TCP Fast Open (TFO) - позволяет впихнуть данные в SYN-пакет. Но работает только для повторных соединений и требует поддержки с обеих сторон. В реальности редкость.
Congestion collapse 1986
В начале 1986 интернет почти умер.
Количество узлов перевалило за 5000, bandwidth был неравномерным. TCP без контроля перегрузки просто забивал канал пакетами. Маршрутизаторы дропали, хосты перепосылали, дропали ещё больше - лавина.
Пропускная способность падала в 1000 раз. Сеть становилась нефункциональной.
Это назвали congestion collapse.
В 1988 Ван Якобсон (Van Jacobson) опубликовал статью “Congestion Avoidance and Control” где описал четыре алгоритма, которые спасли интернет: slow-start, congestion avoidance, fast retransmit, fast recovery.
Именно эти четыре штуки стали обязательной частью TCP. И по сути предотвратили коллапс интернета в 80-х и 90-х, пока трафик рос экспоненциально.
Slow start
Идея простая: сервер не знает, какая пропускная способность у канала до клиента.
Если сразу вжарить 100 пакетов - можно забить сеть и получить тот самый congestion collapse. Поэтому начинаем с малого и разгоняемся.
Сервер держит переменную cwnd (congestion window) - сколько пакетов можно отправить не дожидаясь ACK.
initcwnd - начальное значение окна:
- Изначально: 1 сегмент
- RFC 3390: 4 сегмента
- RFC 6928 (2013): 10 сегментов
Google в 2013 продавил повышение до 10 через эксперименты, показав что хуже не становится. Некоторые CDN (Cloudflare, Fastly) ставят 30 и больше.
Дальше экспоненциальный рост:
Сервер отправил 10 пакетов - ждёт ACK
Пришёл ACK - окно удвоилось до 20
Отправил 20 - ждёт
Окно до 40, потом до 80…
Каждый такой цикл - это один RTT.
Чем больше ответ, тем больше раундов.
А если ответ влезает в initcwnd - улетает одним пакетом без ожидания - получаем один round trip.
На больших файлах (картинки, видео) это не проблема - TCP успевает разогнаться за несколько RTT. Но в большинстве случаев на сайтах файлы меньше 100kb - и вот тут начинается боль.
Например: файл на 100mb - первые пару RTT разгоняется, потом льёт на полной скорости. Эти первые RTT - капля в море.
Файл на 30kb - как раз попадает в зону, где slow start только начал разгоняться. Вместо одного RTT - два. Вместо ~50ms - ~100ms.
А если таких файлов дцать и каждый в новом соединении - каждый проходит этот путь заново.
Кто сколько жрёт
Прежде чем сервер вообще дойдёт до отправки HTML - целый ритуал. Каждый этап - RTT.
DNS: +1 RTT
Резолвинг домена. Обычно кешируется, т.ч. в большинстве случаев 0ms. Но cold lookup - честный RTT.
TCP handshake: +1 RTT
SYN, SYN-ACK, ACK. Без вариантов.
TLS: +2 RTT (или +1 с TLS 1.3)
TLS 1.2: 2 RTT. TLS 1.3: 1 RTT. С 0-RTT resumption можно и ваще 0, но это для повторных соединений.
Кста, сертификат может сам по себе весить несколько килобайт и не влезать в initcwnd. Simon Hørup Eskildsen разбирал датскую газету information.dk - сертификат 6908 байт, initcwnd был 3 (старый линукс). Сертификат не влез в окно, и TLS handshake занял не 2 а 3 RTT. Просто потому что сервер ждал ACK посреди отправки сертификата.
Итого: минимум 3-4 RTT до первого байта HTML. И это мы ещё не начали слать контент. А когда начали - включается slow start.
Спутник
Есть отличный пример от Nathaniel с endtimes.dev:
Нефтяная платформа. Чуваки забыли кости для D&D и хотят зайти на missingdice.com через спутниковый интернет.
Путь пакета:
телефон -> WiFi-роутер -> тарелка -> спутник (35 786 км) -> наземная станция -> сервер
Один RTT: 1ms (роутер) + 120ms (земля-спутник) + 120ms (спутник-земля) + 60ms (наземный-сервер) - примерно 300ms в одну сторону.
Полный круг: 612ms. И это один RTT.
С 4 RTT (DNS + TCP + 2xTLS) до первого байта: 2.5 секунды.
С slow start на несколько раундов - все 4-5 секунд.
На сайт с костями.
ок?
Магия 14 600
Считаем:
- MTU Ethernet: 1500 байт
- Заголовок IP: 20 байт
- Заголовок TCP: 20 байт
- Полезная нагрузка: 1500 - 40 = 1460 байт на пакет
- initcwnd = 10 пакетов
- 10 x 1460 = 14 600 байт = ~14.25 KB
Вот откуда число.
Step function
Это не линейно.
14kb - 1 RTT.
15kb - 2 RTT.
29kb - 2 RTT.
30kb - 3 RTT.
Каждый раз когда перепрыгиваешь границу окна - добавляется целый RTT. Разница между 14kb и 15kb может быть больше, чем между 15kb и 40kb.
Прикол в том, что HTML сжимается gzip-ом. 27kb raw легко превращаются в 12kb gzip. И 12kb - всё ещё в initcwnd, один RTT.
14KB Club считает uncompressed. Т.е. по правилам клуба надо влезть в 14kb сырым HTML.
Но в реальности сайт на 20-30kb raw с gzip почти не уступает “чистому” 14kb сайту. Разница - наносекунды на decompression.
Хотя кабута логичнее было бы считать compressed. Ну да ладно.
А HTTP/2, HTTP/3?
Есть мнение что мультиплексирование убивает правило 14kb. Это не так.
HTTP/2 позволяет слать несколько запросов в одном TCP-соединении. Но TCP slow start никуда не делся - это свойство TCP, а не HTTP. Первый запрос всё равно проходит через slow start.
HTTP/3 (QUIC) работает поверх UDP. Но спецификация QUIC рекомендует тот же initcwnd в 10 пакетов.
Плюс HTTPS-рукопожатие и HTTP/2-префейс съедают часть начального окна. Barry Pollard детально разбирал что к моменту отправки HTML окно уже может быть больше 10 - ACKи слались во время TLS-переговоров. Т.ч. “ровно 14kb” - это упрощение.
Но механика та же. Понимать её полезнее чем дрочить на циферки.
Small web - это не про килобайты
Тема маленьких сайтов - она не только про скорость. Она про философию.
Клубы
Есть целая экосистема: 1MB Club, 512KB Club, 250KB Club, 14KB Club. И ещё всякие no-js.club, nocss.club.
512KB Club на главной пишет прямым текстом:
The internet has become a bloated mess. Huge JavaScript libraries, countless client-side queries and overly complex frontend frameworks are par for the course these days.
И ведь правда.
NYT - газетный лейаут, текст и картинки - весит 9 мегабайт на 891 запросе, половина из которых JavaScript. Это пиздец.
Сайт, который по природе не может быть тяжёлым, зачем-то превращён в монстра. И ладно бы это был webgl-эксперимент. Это газета. Текст и картинки.
Motherfucking website
Если не видели motherfuckingwebsite.com - сходите. Один HTML-файл, без CSS, без JS. Чёрный текст на белом фоне. 5kb.
Потом появился bestmotherfucking.website - минимальный CSS, 78 байт инлайн. Потом thebestmotherfucking.website - уже с тёмной темой, но 132kb, кабута многовато.
Есть ещё justfuckingusehtml.com - 29kb чистого HTML. Никаких фреймворков, сборщиков, транспайлеров. Just HTML.
Вся эта линейка - не просто мем. Это манифест.
Уважение
Выше был пример со спутником - 612ms на RTT. Человек на нефтяной платформе хочет зайти на сайт - и сайт грузится 5 секунд.
Не потому что контент тяжёлый.
Потому что разработчику было похуй.
Кинул 5 мегабайт JS, три шрифта по 200kb, картинки в PNG вместо WebP, трекинг, рекламу.
Маленький сайт - это уважение к пользователю. К его времени. К его трафику. К его батарейке. К тому что не у всех оптоволокно и 5G.
Ilya Grigorik в HPBN пишет:
No bit is faster than one that is not sent.
Это не про “сделать сайт серым и унылым”. Это про “не тащить лютые килобайты того, что пользователю нахуй не нужно”.
К слову, 512KB Club даёт звание Green Team сайтам до 100kb. sccl.cc со своими 27kb raw влез бы. Но хотелось большего.
Оптимизация sccl.cc
У меня есть сайтец sccl.cc. Запилен на Zine - SSG на Zig. Три страницы: contacts, projects, peripherals.
до переезда на Zine был Astro и сайт весил 150+кб, см Migration-from-Astro-to-Zine.md
Что сервер отдавал изначально:
- HTML (~15kb)
- Space Mono + Martian Mono woff2 (~40kb)
- Аватарка PNG (~2mb)
- Фавиконка PNG (~1mb)

на скрине уже немного заоптемайзенный, было хуже :skull:
Четыре запроса на страницу с контентом, который по факту весит нихрена. И это с keep-alive и HTTP/2 - setup один раз, но каждое новое соединение, каждая вкладка - всё заново.
План: всё в один HTML. Base64 инлайн всего. Ноль внешних запросов. Влезть в initcwnd со сжатием.
Шрифт
Самое жирное. 2 шрифта + bold-версии = 40kb.
По-хорошему кастомные шрифты нахуй не нужны. Но эстетика победила - решено оставить один, выпилив из файла всё кроме ASCII.
Инструмент: pyftsubset из fonttools. Выпиливаются нелатинские диапазоны, символы которых нет на сайте.
Итог: 4.2kb, в base64 - 5.7kb текста в HTML.
Аватарка
Пиксельарт, зарендерен в 400%, PNG с альфа-каналом. Жирно.
Шаколизатор, три цвета, PNG - 1.9kb. Конвертация в WebP - 702 байта.
Base64: 940 байт.
Фавиконка
Те же манипуляции. 199 байт в PNG (для мелких картинок он лучше WebP).
Base64: 268 байт.
JS
Анимированные кнопки, анимированный фон, пасхалка с анимацией. Всё для красоты. Реально полезного - кот наплакал.
Но функционал хотелось сохранить. Всё ушло в инлайн. ~4kb скриптов, без которых сайт работал бы не хуже. Но красиво.
Итог
До: 4 запроса, ~50kb
После: 1 запрос, 12kb gzip / 27kb raw
Влезает в initcwnd. Один TCP-пакет данных после рукопожатий - и сайт готов.

Дилемма инлайна
Инлайн всего - компромисс. Есть цена.
Кеширование
Шрифт в base64 внутри HTML - не кешируется. При переходе на другую страницу браузер качает его заново, потому что он часть HTML.
С отдельным файлом шрифт закешировался бы после первого раза. Вторая страница грузилась бы без него.
Когда что выбирать
Всё в одном HTML (base64 шрифты)
2 страницы по 14kb каждая
Каждая - 1 RTT
Но каждый раз жрёшь полные 14kb, даже если шрифт повторяется
HTML + отдельный шрифт
2 страницы по 8kb + шрифт 4kb отдельно
Первый заход: 2 запроса
Переходы: только 8kb, шрифт закеширован
В реальном интернете разницу между 14kb и 8kb не почувствовать - оба влезают в один initcwnd.
А вот делей между запросами - да.
Overhead
Инлайн весит чуть больше внешней зависимости - base64 добавляет ~33% к бинарным данным. Если ты и так не превышаешь initcwnd с внешними ресурсами - инлайн нахуй не нужен.
Он имеет смысл когда разница между “влез в один RTT” и “не влез” критична.
В случае sccl.cc разница была драматической: 4 запроса против 1, 50kb против 12kb. Оно того стоило.
Чо в итоге
14kb - не magic number. Это следствие архитектуры TCP, которая проектировалась для надёжности и честности, а не для скорости первой загрузки.
Понимание транспортного уровня даёт больше чем слепое следование “правилу 14kb”. Где-то нужен инлайн, где-то отдельные файлы с кешированием. Где-то 30kb raw с gzip - почти то же что 14kb без сжатия.
Но философия остаётся: чем меньше - тем быстрее. И чем меньше говна тащишь на сайт, тем больше уважения к пользователю.