Почему 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:

  1. Клиент шлёт SYN
  2. Сервер отвечает SYN-ACK
  3. Клиент добивает 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 без сжатия.

Но философия остаётся: чем меньше - тем быстрее. И чем меньше говна тащишь на сайт, тем больше уважения к пользователю.