the 14kb ceiling: tcp invented the small web branch
#it #web!! this page was AI translated and may contain inaccuracies !!
#it #web!! this page was AI translated and may contain inaccuracies !!
here are some real requests as an example.
here’s a site that fits in 14kb:

and here’s one that doesn’t:

download - that’s the site’s content: 1 ms versus 63 ms, i.e. a 63-fold speed difference, with only a 7-fold size difference.
this scribbling skims the guts of tcp, dns, tls, slow start, the congestion collapse of 1986, and small web philososhit while we’re at it: why a small site means respecting the user, not jerking off to numbers.
this article is partly pop larping, since people have already written about this… but i kinda wanna lay it out in my own words, with the info i managed to dig up, and maybe some of my own thoughts, experience and attempts.
before the server sends even a byte of html, a fuckton of stuff happens
every tcp connection starts with three handshakes:
SYNSYN-ACKACKthree messages back and forth just to say hello, and only then can you send data (+1rtt)
there’s tcp fast open (TFO) - lets you stuff data into the SYN packet, but only works for repeat connections and needs support on both ends.
in early 1986 the internet tried to die: the node count passed 5000, bandwidth was uneven, tcp without congestion control just flooded the channel with packets, so routers dropped them, hosts retransmitted, even more got dropped - a snowball effect where throughput fell by a factor of 1000.
that’s what they called congestion collapse.
In 1988 Van Jacobson published the paper “Congestion Avoidance and Control” describing four algorithms that saved the internet: slow-start, congestion avoidance, fast retransmit, fast recovery. - these 4 things became a mandatory part of tcp, basically preventing the internet from collapsing while traffic grew exponentially.
the idea is that the server doesn’t know the bandwidth of the channel to the client - blast 100 packets right away and you could clog the network and get that very congestion collapse, so we start small and ramp up.
the server keeps a variable cwnd(congestion window) - how many packets it can send without waiting for an ACK.
initcwnd - the initial window size:
// google in 2013 pushed through the increase to 10 with experiments showing it didn’t make things worse, some cdns (cloudflare, fastly) set it to 30 or more.
then comes exponential growth:
: each cycle like this is one rtt.
and if the response fits in initcwnd - it flies out in one packet without waiting - that’s one round trip.
with big files (pictures, vids) this isn’t a problem - tcp has time to ramp up over a few rtts, but most files on websites are under 100kib - here comes the pain…
for example: a 100mib file spends the first couple of rtts ramping up, then pours out at full speed - those first rtts are a drop in the ocean, since there’s a shitload of them in total.
but a 30kb file lands right in the zone where slow start has only just begun ramping up, but hasn’t made it yet, i.e. if 1rtt took 50ms, two will take 100ms and so on, if there are umpteen files like that and each uses a new connection - each goes through it all over again.
a whole ritual kicks off before the server gets around to sending html, where each stage is an rtt:
dns : +1rtt : domain resolution is usually cached, so 0ms in most cases, but a cold lookup is an honest rtt.
tcp handshake : +1 rtt : SYN, SYN-ACK, ACK, no way around it.
tls : +2 rtt (or +1 with tls 1.3) : with zero-rtt resumption it can even be 0, but that’s for repeat connections.
Btw, the certificate alone can weigh several kilobytes and not fit in initcwnd - Simon Hørup Eskildsen looked into the Danish newspaper information.dk - a 6908-byte certificate, initcwnd was 3 (old linux): the cert didn’t fit in the window, and the tls handshake took 3 rtts instead of 2, simply because the server waited for an ACK halfway through sending the certificate.
Total: at least 3-4 rtts before the first byte of html, before content starts being sent, and then slow start kicks in.
a cool example from Nathaniel : most sources on this topic nicked their info from his article - me included :D
oil rig: some dudes forgot their d&d dice and want to visit missingdice.com over satellite internet.
the packet’s path:
phone -> wifi-router -> dish -> satellite (35786 km) -> ground station -> server
1rtt:
1ms (router) + 120ms (ground-satellite) + 120ms (satellite-ground) + 60ms (ground-server)
: about 300ms one way, and a full round trip is 612ms (just for 1rtt)
with 4 rtts (dns + tcp + 2x tls) it’s 2.5 seconds to the first byte, with slow start over several rounds - a full 4-5 seconds. - all that for a simple dice site whose js would’ve fit in 14kb if the dev had thought about optimization and stuff.
with a vpn connection and so on mtu can be lower due to extra overhead - like 1320-1350b, or thereabouts (in my experience these values usually get through)
this is a very common problem in complex closed networks, like corporate ones - packets can just get dropped.
you can quickly check what size packets reach the server with this command:
// on linux
ping -f -s 1472 -c 10 google.com
// on windows
ping -f -l 1472 google.com
a whole rtt gets added every time you jump past the window boundary, i.e. the difference between 14kb and 15kb can be bigger than between 15kb and 40kb.
another fun bit is that html gets compressed with gzip, where 27kb raw easily turns into 12kb gzip, which still fits in initcwnd, i.e. 1rtt (example: sccl.cc dunno how current this is, it’s current right now)
14KB Club counts uncompressed - i.e. club rules say you have to fit in 14kb of raw html (though i think that club’s dead already, not a damn thing gets updated)
but in reality a 20-30kb raw site with gzip is almost as fast as a “pure” 14kb site - the difference is nanoseconds of decompression.
though counting compressed would kinda make more sense, oh well.
some say multiplexing kills the 14kb rule.
http/2 lets you send multiple requests over one tcp connection, but tcp slow start doesn’t go anywhere - it’s a property of tcp, not http - the first request still goes through slow start.
http/3 (quic) runs over udp, but the quic specification recommends the same initcwnd of 10 packets.
+ the httpS handshake and http/2 preface eat up part of the initial window - Barry Pollard broke down how by the time html is sent the window might already be bigger than 10 - ACKs were being sent during tls negotiation, so “exactly 14kb” is a simplification.
same mechanics - understanding them is more useful than jerking off to numbers.
the small web has bloated unoptimized sites just like everywhere else, and geekier, dead simple ones like plain .md text - but they’re all more about the philosophy of simplicity than numbers.
1mb club, 512kb club, 250kb club, 14kb club, and stuff like no-js.club, nocss.club.
512kb club says it outright on the homepage:
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.
and it’s true, there’s a great NYT example right there - a newspaper layout, text and pictures - weighs 9 megabytes across 891 requests, half of them js, which is completely fucked, a site that by its nature can’t be heavy somehow weighs as much as a simple game.
against this backdrop motherfuckingwebsite.com was born - just html: no css or js, 5kb
then came bestmotherfucking.website - minimal css, 78 bytes inline.
then thebestmotherfucking.website - now with a dark theme, but 132kb.
there’s also justfuckingusehtml.com - 29kb just html.
you could call this whole lineup a manifesto about how the internet is full of shit.
there was that satellite example above - 612ms per rtt - someone on an oil rig wants to visit a site and it takes 5 seconds to load - not because the content is heavy - just because the dev didn’t give a fuck.
a small site means respecting the user: their time, traffic, battery(if it’s a wearable device), the fact that not everyone has fiber and 5G.
a quote from Ilya Grigorik in HPBN:
No bit is faster than one that is not sent.
the point is you don’t fucking need to send the user stuff they really don’t need. a nice layout is something the user needs to an extent, but 50kb of scripts for button animations - probably not.
i have a little site sccl.cc, built with zine.
before moving to Zine it was Astro and the site weighed 150+kb, see migration-from-astro-to-zine
what we had at the start:
already a bit optimizified in the screenshot, it used to be worse :skull:
4 requests for a page with content that actually weighs fuck all, and that’s with keep-alive and http/2 - setup once, but every new connection, every tab - all over again.
my plan: everything in one html, base64 inline everything, 0 external requests, keep js: fit into initcwnd with gzip compression.
fonts are the fattest bit - i had 2 fonts + bold versions = 40kb.
really, custom fonts aren’t fucking needed, but aesthetics won - decided to keep one, ripping everything except ASCII out of the file.
tool: pyftsubset from fonttools - strips non-latin ranges, characters that aren’t on the site, etc.
result: 4.2kb, in base64 - 5.7kb of text in html.
upd: later it became 2kb per font, can’t give exact numbers, just had to cut out more characters.
pixel art rendered at 400%: png with an alpha channel - actually bloated and useless.
managed to crunch it down to 3 colors with a shitifier, png - 1.9kb. Conversion to webp - 702 bytes.
base64: 940 bytes.
same tricks: 199 bytes in PNG (for tiny images it’s better than webp).
base64: 268 bytes.
Animated buttons, animated background, an easter egg with animation. All for looks. Actually useful stuff - next to nothing.
But i wanted to keep the functionality. Everything went inline. ~4kb of scripts the site would work just as well without. But pretty.
before: 4 requests - ~50kb
after: 1 request - 12kb gzip / 27kb raw
fits in initcwnd - one tcp data packet after the handshakes.

inlining everything is a compromise with its own price
a base64 font inside html doesn’t get cached - so when you go to another page the browser downloads it again, because it’s part of the html. with a separate file the font would be cached after the first time and, say, the second page would load without it.
everything in one html (base64 fonts)
html + separate font
on the actual internet you won’t feel the difference between 14kb and 8kb - both fit in one initcwnd, but the delay between requests - you sure as hell feel that.
if the html is really big, or there’s lots of navigation between pages - it’s worth caching the font.
inline weighs slightly more than an external dependency - base64 adds ~33% to binary data: if you’re already within initcwnd with external resources - you don’t fucking need inline.
it makes sense when the difference between fitting and not fitting in 1rtt is critical, in sccl.cc’s case it was worth it.
14kb isn’t a magic number, it’s a consequence of tcp’s architecture, which was designed for reliability.
understanding the transport layer gives you more than blindly following the 14kb rule: sometimes you need inline, sometimes separate cached files, sometimes 30kb raw with gzip is almost the same as 14kb uncompressed.
but the philososhit stays: smaller means faster, and the less shit you drag onto a site, the more respect you show the user.
comments