потолок 14kb: tcp придумал ветку small web

#it #web

# почему 14 KB?

вот в пример реальные запросы.

вот сайт который влезает в 14кб:

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

download - это и есть контент сайта: 1 ms против 63 ms, т.е. разница скорости в 63 раза, при разнице веса всего 7 раз.

ета писанина по верхам о потрохах tcp, dns, tls, slow start, congestion collapse 1986 года, да заодно филососия small web: почему маленький сайт - это уважение к пользователю, а не дрочка на циферки.

статья отчасти ларпинг попс, ибо об этом уже писали… но я чет хочу расписать своими словами, да с той инфой, что удалось откопать мне, да может с какими-то своими мыслями, опытом и потугами.

# чотамкак tcp

прежде чем сервер отдаст хоть байт html, происходит овердохуя всего

## three-way handshake

любое tcp-соединение начинает с трёх рукопожатий:

  1. клиент шлёт SYN
  2. сервер отвечает SYN-ACK
  3. клиент добивает ACK

три сообщения туда-сюда на поздороваться, и только потом можно слать данные (+1rtt)

есть 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. - эти 4 штуки стали обязательной частью tcp, по сути предотвратили коллапс интернета, пока трафик рос экспоненциально.

## 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.

a если ответ влезает в initcwnd - улетает одним пакетом без ожидания - получается one round trip.

на больших файлах (картинки, видево) это не проблема - tcp успевает разогнаться за несколько rtt, но в большинстве случаев на сайтах файлы меньше 100kib - начинается боль…

например: файл на 100mib первые пару rtt разгоняется, потом льёт на полной скорости - эти первые rtt - капля в море, ведь всего их получается дофига.

а вот файл на 30kb как раз попадает в зону, где slow start только начал разгоняться, но не успел, т.е. если 1rtt выполнялся за 50ms, то два уже будет 100ms и т.п., если таких файлов дцать и каждый в новом соединении - каждый проходит этот путь по новой.

# кто сколько жрёт

стартует целый ритуал, прежде чем сервер дойдёт до отправки html, где каждый этап это rtt:

dns : +1rtt : резолвинг домена обычно кешируется, т.ч. в большинстве случаев 0ms, но cold lookup - честный rtt.

tcp handshake : +1 rtt : SYN, SYN-ACK, ACK без вариантов.

tls : +2 rtt (или +1 с tls 1.3) : с zero-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 : с его статьи бол-во источников на эту тему слизали инфу - я в т.ч. :D

oil rig: чуваки забыли кости для d&d и хотят зайти на missingdice.com через спутниковый интернет.

путь пакета:

телефон -> wifi-router -> тарелка -> спутник (35786 км) -> наземная станция -> сервер

1rtt:

1ms (роутер) + 120ms (земля-спутник) + 120ms (спутник-земля) + 60ms (наземный-сервер)

: примерно 300ms в одну сторону, а полный круг 612ms (лишь на 1rtt)

с 4 rtt (dns + tcp + 2x tls) до первого байта 2.5 секунды, с slow start на несколько раундов - все 4-5 секунд. - всё это ради простенького сайта с костями, js которого бы влез в 14кб, если бы разраб задумался над оптимизоном и т.д.

# 14 600

  • mtu ethernet: 1500 байт
  • заголовок ip: 20 байт
  • заголовок tcp: 20 байт
  • полезная нагрузка: 1500 - 40 = 1460b на пакет
  • initcwnd = 10 пакетов
  • 10 x 1460 = 14600 байт ~= 14.25 KB

при vpn подключении и т.д. mtu может быть ниже из-за доп оверхеда - типа 1320-1350b, иль около того (на моей практике обычн такие значения проходят)

это очень частая проблема в сложных закрытых сетях, типа корпоротивных - пакеты могут просто дропаться.

быстро прочекать какого размера пакеты долетают до сервера можно командой:

// на linux
ping -f -s 1472 -c 10 google.com

// на windows
ping -f -l 1472 google.com

# 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, что всё ещё в initcwnd, т.е. 1rtt (пример: sccl.cc не знаю насколько актуален, щас актуален)

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

в small web-е встречаются как и жирные неоптимизированные сайты, как и везде, так и более гиковские, максимально простые, типа plain .md text - но они все больше не про циферки, а про философию простоты.

## клубы

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 запроса, половина из которых js, что полный пиздец, сайт, который по природе не может быть тяжёлым, зачем-то весит как простенькая игра.

## motherfucking website

на фоне всего этого родился motherfuckingwebsite.com - один html: без css и js, 5kb

потом появился bestmotherfucking.website - минимальный css, 78 байт inline.

потом thebestmotherfucking.website - уже с тёмной темой, но 132kb.

есть ещё justfuckingusehtml.com - 29kb just html.

всю эту линейку можно считать манифестом того, что интернет в говне.

## уважение

выше был пример со спутником - 612ms на rtt - человек на нефтяной платформе хочет зайти на сайт и грузится 5 секунд - не по той причине, что контент какой-то тяжелый - просто разрабу было похуй.

маленький сайт - это уважение к пользователю: к его времени, трафику, батарее(если это носимое устройство), к тому что не у всех оптоволокно и 5G.

цитата Ilya Grigorik в HPBN:

No bit is faster than one that is not sent.

суть в том, что нахуй ненад слать пользователю то, что ему реально ненужно. красивый лейаут пользователю в какой-то степени нужен, но 50kb скриптов на анимации кнопок - вряд ли.

# оптимизация моего сайта

у меня есть сайтец sccl.cc, что запилен на zine.

до переезда на Zine был Astro и сайт весил 150+кб, см migration-from-astro-to-zine

че имели по началу:

  • html (~15kb)
  • space mono + martian mono woff2 (~40kb)
  • аватарка png (~2mb)
  • фавиконка png (~1mb)

на скрине уже немного заоптемайзенный, было хуже :skull:

4 запроса на страницу с контентом, который по факту весит нихрена, и это с keep-alive и http/2 - setup один раз, но каждое новое соединение, каждая вкладка - всё заново.

мой план: всё в один html, base64 инлайн всего, 0 внешних запросов, оставить js: влезть в initcwnd со сжатием gzip.

## шрифт

шрифты это самое жирное - у меня было 2 шрифта + bold-версии = 40kb.

по-хорошему кастомные шрифты нахуй не нужны, но эстетика победила - решено оставить один, выпилив из файла всё кроме ASCII.

инструмент: pyftsubset из fonttools - выпиливаются non-latin диапазоны, символы которых нет на сайте и т.д.

итог: 4.2kb, в base64 - 5.7kb текста в html.

upd: потом стало 2kb на шрифт, точных цифр не скажу, просто над было вырезать больше символов.

## аватарка

пиксель-арт зарендерен в 400%: png с альфа-каналом - на деле жирно и бесполезно.

шаколизатором удалось сжать в 3 цвета, png - 1.9kb. Конвертация в webp - 702 байта.

base64: 940 байт.

## favicon

те же манипуляции: 199 байт в PNG (для мелких картинок он лучше чем webp).

base64: 268 байт.

## js

Анимированные кнопки, анимированный фон, пасхалка с анимацией. Всё для красоты. Реально полезного - кот наплакал.

Но функционал хотелось сохранить. Всё ушло в инлайн. ~4kb скриптов, без которых сайт работал бы не хуже. Но красиво.

## итог

до: 4 запроса - ~50kb

пpосле: 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, а вот делей между запросами - очень даже чувствуется.

если html реально большой, иль переходов между страницами много - стоит кешировать шрифт.

## overhead

инлайн весит чуть больше внешней зависимости - base64 добавляет ~33% к бинарным данным: если ты и так не превышаешь initcwnd с внешними ресурсами - инлайн нахуй не нужен.

oн имеет смысл когда разница между влезть и не влезть в 1rtt - критична, в случае sccl.cc oно того стоило.

? чо в итоге

14kb - не magic number, а следствие архитектуры tcp, которая проектировалась для надёжности.

понимание транспортного уровня даёт больше чем слепое следование правилу 14kb: гдет нужен инлайн, гдет отдельные файлы с кешированием, гдет 30kb raw с gzip - почти то же что 14kb без сжатия.

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