What I Learnt Making A Teeny-Tiny 1kB Webpage

Jun 5, 2023 · 2626 words · 13 minute read

Having bought the domain hen.re on a whim, because it was short and a bit like my name, but with no real plans, I thought it'd be a fun little project to see if I could create small landing/home page. But a very small page, so small in fact, that it could join the 1kb Club, a list of webpages that are smaller than 1,024 bytes.

The Challenge

1kB Club is a list of web pages weighing less than 1 kilobyte (1,024 bytes).

1kB Club Membership Rules

Page size will be adjudicated by the DebugBear Page Speed Analyzer, as named in the submissions page. The important metric here is the 'weight', which is the total amount of data that needs to be pulled to render the page. This can be larger than the HTML file size, as it also includes any other data needed along the way, such as images, external CSS or JavaScript etc.

Not in the rules, but I'm adding it anyway is that it should be valid HTML, as per the W3C Markup Validator. This doesn't guarantee that all browsers will render it correctly, but hopefully it makes sure that it works with most, and have a modicum of accessibility built in.

There are other clubs, like the 1 MB and the 512 kB, but these are focused more on 'real' websites, and would be much more work, in the sense that I could do many more things, and would need good ideas to fill the, and I don't have any. Additionally, depending on the current images on the front page, this site has a weight of only around 150 kB, so that seems less of a challenge.

Why?

Fun! In my case I want to make a page that doesn't obviously look like it's a 1kb page. Many of the other entries are already impressive examples of just how small you can go - I'm not going to beat that, even if there's something to be learnt there.

For reference, I'm not a web designer. I'm really a mechanical engineer by training, working as a data engineering and scientist for the last five. What I'm trying to say is; if it looks rough, go easy on me, and if you've got a good idea or a better way of doing it, try it yourself.

Weight Loss Techniques

Given the tight constraints, I'm going to have to take a hard look at everything that goes into the page, and get some ideas for things that can be saved. The first good resource is looking at the source of members of the club, plus Brad Taunt, the club's organiser, has a nice write up on his page explaining how he made his. Most of the steps are the same ones they took, and are included here only for completeness.

Shorten Links

A link is surprisingly long, depending on where you're linking to, for example:

<a href="https://www.henryleach.com">my website</a>

Brad used Netlify's '_redirects' file, but you can also do it with just a HTML file that contains the following:

<meta http-equiv="Refresh" content="0;
  url='https://www.henryleach.com'"/>

Save the above as a file called 'm.html', place it the same folder as the file with the first linked, and the URL can be reduced to just href="m".

Favicon

Normally a browser expects to find a favicon, taken from a 'favicon.ico' file, this tells it to use empty data, and not to complain, or do any back and forth with costly 'file not found' responses.

<link rel="icon" href="data:,">

Minification & Optional Tags

HTML 5 is surprisingly lenient on which tags you do and don't need. You don't really need things like <head> , <html> or <body>, and if you do have them, they don't need closing. There's an interesting page from Google explaining which tags are optional.

Minification is just taking all the unneeded spaces and newlines out of the HTML. Unlike a compiled language, where such things are discarded from the final binary, with the HTML these would all be transmitted from the server to the browser, so we want to remove them.

Style

Most of the other 1kB club sites are very minimal in appearance, sticking to a very unstyled..erm..style? I wanted mine to be a tiny page that doesn't look like a tiny page.

The biggest impact is giving the site some colour, and I think the biggest 'bang for buck'1 is a gradient, especially a radial one. That allows for two colours, and has movement of colour in two dimensions. Given everything else on the page will be text, it also can't clash with anything and is the single biggest feature. If that style is tasteful is a matter of opinion.

This was achieved with the following:

background-image: radial-gradient(farthest-corner circle at 0% 0%,
				  #95dfea 0%,
				  #ba52ff 100%);

I want this to cover the whole page, which does, unfortunately mean keeping the <body> tag so we can address it; otherwise it will start to repeat once it reaches the end of the text, which is certainly a look, but not the one I'm going for.

First Weigh In

With all the above done, plus a short one sentence personal introduction, my page comes to a trim 594 bytes, but knowing there's some overhead, I want to check how much room to play I've got left. I've also got a buffer as I haven't minified it yet. Push the files to a remote repository, log in to my current static site host render.com, create a site without a build step, so it just spews the index.html at you, and try the speed test.

What, wait 1.0kB, already?! That can't be right, try again…but no, again 1.o kB.

Repeating the test doesn't help, same result. Sadly DebugBear isn't very detailed in where everything that goes into 'weight' comes from (at least not on the free version of the speed test I'm using). There is a page that talks about the importance of weight, but it doesn't clearly define everything they're measuring. Looking around it seems like there isn't really a standard measure of page weight.

Searching for more details I try the Chrome DevTools, but they're not very helpful, at the bottom I just see the resources loaded being 1 HTML document at 594 bytes. What I do learn is that it makes a difference if you go to "www.hen.re" or just "hen.re", the former re-directs you to the latter, a step which adds around 170 bytes in total 'transferred', the value at the bottom of the DevTool's 'network' tab that most closely matches the value the speed test gives. I also can't reliably get the same value as DebugBear doing that. The first time I load the page in Incognito mode I get around 1kB transferred, subsequent refreshes, even with Shift+⌘+R, are distinctly less. Mysterious.

This doesn't make a lot of sense, especially when I compare some sites already on the 1kB list, e.g. https://1kb.andrian.io has a weight of 994 B, which matches to Chrome's 'transferred' measure, but the actual HTML file is only 919 B, meaning there's only an additional 75 B, compared to the additional ~430 B in my page. That amount of overhead I can understand, there's something else going on in my case.

I spent some time struggling to understand everything that's in Chrome's dev tools, but it's effectively a whole IDE of its own, and I don't intend on understanding that for this small project. While maybe I'm not smart enough to understand all the dev tools, what I am smart enough to do is read a list; enter curl.

Total Transfer

My naive assumption is that if I point curl at hen.re in verbose mode, I'm going to get a list of everything that it collects on the way, and then perhaps understand, and count, it.

curl -v https://hen.re
 *   Trying 216.24.57.253...
 * TCP_NODELAY set
 * Connected to hen.re (216.24.57.253) port 443 (#0)
 * ALPN, offering h2
 * ALPN, offering http/1.1
 * successfully set certificate verify locations:
 *   CAfile: /etc/ssl/cert.pem
  CApath: none
 * TLSv1.2 (OUT), TLS handshake, Client hello (1):
 * TLSv1.2 (IN), TLS handshake, Server hello (2):
 * TLSv1.2 (IN), TLS handshake, Certificate (11):
 * TLSv1.2 (IN), TLS handshake, Server key exchange (12):
 * TLSv1.2 (IN), TLS handshake, Server finished (14):
 * TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
 * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
 * TLSv1.2 (OUT), TLS handshake, Finished (20):
 * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
 * TLSv1.2 (IN), TLS handshake, Finished (20):
 * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
 * ALPN, server accepted to use h2
 * Server certificate:
 *  subject: CN=hen.re
 *  start date: May 27 04:54:56 2023 GMT
 *  expire date: Aug 25 04:54:55 2023 GMT
 *  subjectAltName: host "hen.re" matched cert's "hen.re"
 *  issuer: C=US; O=Let's Encrypt; CN=R3
 *  SSL certificate verify ok.
 * Using HTTP2, server supports multi-use
 * Connection state changed (HTTP/2 confirmed)
 * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
 * Using Stream ID: 1 (easy handle 0x7fdbf3009600)
> GET / HTTP/2
> Host: hen.re
> User-Agent: curl/7.64.1
> Accept: */*
> 
 * Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 200 
< date: Sat, 27 May 2023 18:50:50 GMT
< content-type: text/html; charset=utf-8
< cf-ray: 7ce072215f42ca89-HAM
< cf-cache-status: DYNAMIC
< cache-control: public, max-age=0, s-maxage=300
< etag: W/"1647891a6bb02178a4f3a8f475fba6a8"
< last-modified: Sat, 27 May 2023 07:21:46 UTC
< vary: Accept-Encoding
< cache-tag: srv-cfdd8l6a499b3gdp67gg
< cloudflare-cdn-cache-control: public, max-age=300
< x-content-type-options: nosniff
< set-cookie: __cf_bm=IzslwyyEs88t63v6ErOi8invJcsivbmGzAtrgiZPIII1-682513405-0-Ac7GTSSjOkHkWcFkLNpjZXGdeMu+dkbESoEjOXZQ2vM24bYNmlDSR02FlPZbVKORes4xfX0XR+8IghabNYDIxNg=; path=/; expires=Sat, 27-May-23 19:20:50 GMT; domain=.hen.re; HttpOnly; Secure; SameSite=None
< set-cookie: _cfuvid=u9rE_.znPpTm.tDcSMg1eL3gq8CO6tSNxGVSY56ueCQ-1685134250965-0-608040000; path=/; domain=.hen.re; HttpOnly; Secure; SameSite=None
< server: cloudflare
< alt-svc: h3=":443"; ma=86400
< webpage here
 * Connection #0 to host hen.re left intact
 * Closing connection 0

So did you spot it? No, well neither did I, as I didn't know what I should be looking for. Let's compare that to https://1kb.andrian.io, which had a much smaller difference between the HTML and the weight:

curl -v https://1kb.andrian.io/
 *   Trying 5.161.87.215...
 * TCP_NODELAY set
 * Connected to 1kb.andrian.io (5.161.87.215) port 443 (#0)
 * ALPN, offering h2
 * ALPN, offering http/1.1
 * successfully set certificate verify locations:
 *   CAfile: /etc/ssl/cert.pem
  CApath: none
 * TLSv1.2 (OUT), TLS handshake, Client hello (1):
 * TLSv1.2 (IN), TLS handshake, Server hello (2):
 * TLSv1.2 (IN), TLS handshake, Certificate (11):
 * TLSv1.2 (IN), TLS handshake, Server key exchange (12):
 * TLSv1.2 (IN), TLS handshake, Server finished (14):
 * TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
 * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
 * TLSv1.2 (OUT), TLS handshake, Finished (20):
 * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
 * TLSv1.2 (IN), TLS handshake, Finished (20):
 * SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384
 * ALPN, server accepted to use h2
 * Server certificate:
 *  subject: CN=1kb.andrian.io
 *  start date: May 18 07:54:46 2023 GMT
 *  expire date: Aug 16 07:54:45 2023 GMT
 *  subjectAltName: host "1kb.andrian.io" matched cert's "1kb.andrian.io"
 *  issuer: C=US; O=Let's Encrypt; CN=R3
 *  SSL certificate verify ok.
 * Using HTTP2, server supports multi-use
 * Connection state changed (HTTP/2 confirmed)
 * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
 * Using Stream ID: 1 (easy handle 0x7f8bfb80e400)
> GET / HTTP/2
> Host: 1kb.andrian.io
> User-Agent: curl/7.64.1
> Accept: */*
> 
 * Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200 
< server: nginx
< date: Sat, 27 May 2023 07:22:41 GMT
< content-type: text/html; charset=utf-8
< content-length: 919
< 
< Webpage here
 * Closing connection 0

I've cut out the actual webpage response in both, as we know it's different, and is not what we're interesting in for this comparison. In both cases there's the TLS exchange, which comes to something like 60 bytes (adding all the numbers in brackets at the end of the lines together). What's most interesting is the bit that comes after the line Accept: */* in both cases. (If you're not familiar with curl, the lines starting with ">" mean data going from your machine to the server and "<" means incoming data.)

In Andrian's example there's not a lot, a few lines of info that tell us that he's running an nginx server, and then you get the content length (919 bytes) and then the webpage. But in the data from hen.re you get:

 * Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 200 
< date: Sat, 27 May 2023 18:50:50 GMT
< content-type: text/html; charset=utf-8
< cf-ray: 7ce072215f42ca89-HAM
< cf-cache-status: DYNAMIC
< cache-control: public, max-age=0, s-maxage=300
< etag: W/"1647891a6bb02178a4f3a8f475fba6a8"
< last-modified: Sat, 27 May 2023 07:21:46 UTC
< vary: Accept-Encoding
< cache-tag: srv-cfdd8l6a499b3gdp67gg
< cloudflare-cdn-cache-control: public, max-age=300
< x-content-type-options: nosniff
< set-cookie: __cf_bm=IzslwyyEs88t63v6ErOi8invJcsivbmGzAtrgiZPIII1-682513405-0-Ac7GTSSjOkHkWcFkLNpjZXGdeMu+dkbESoEjOXZQ2vM24bYNmlDSR02FlPZbVKORes4xfX0XR+8IghabNYDIxNg=; path=/; expires=Sat, 27-May-23 19:20:50 GMT; domain=.hen.re; HttpOnly; Secure; SameSite=None
< set-cookie: _cfuvid=u9rE_.znPpTm.tDcSMg1eL3gq8CO6tSNxGVSY56ueCQ-1685134250965-0-608040000; path=/; domain=.hen.re; HttpOnly; Secure; SameSite=None
< server: cloudflare
< alt-svc: h3=":443"; ma=86400

Here we can see traces of what appears to be the CloudFlare Content Delivery Network (CDN) that Render's static site service uses, and that my dear Watson, appears to be the culprit. Normally a CDN is a useful thing, it means copies of your pages are closer to your readers for faster loading, and it's protected against being overwhelmed because there are more locations that can server the data. Except in this case it's working against me, as the webpage is so small that the overhead to make this work is a significant amount of the total traffic, as a rough estimation from the above:

WhatBytes
HTML of Page594
TLS60
eTag32
cache-tag24
Cookie '_cfuvid'76
Cookie '__cf_bm'152
Total938

With a couple more bytes thrown in here and there for connections etc., we can see how my 594 byte page expands to a whole kilobyte in size. These cookies also explain why Chrome only showed the larger size, even in Incognito mode, on first load, as even with a page refresh it wouldn't load the cookies again, they would only be loaded if I created a new incognito window.

Once I'd moved the site to Netlify, which doesn't use a CDN (or at least not in the same way), the total transferred dropped to only 808 bytes, giving me a bit more room to play with.

(Afterwards I discovered you can also find these in the Chrome Dev Tools under Applications > Storage > Cookies > $website-name.)

Untold Riches

Now that I'm awash with extra bytes, what should I spend them on? My aim is still to try and make a page that doesn't look like it's a tiny page, so after extending the personal description blurb to be the same as I have in other places, I used my allowance to fix that up a bit.

I treated myself to a <div>, so that I could centre the text in a box on the page, which makes it easier to read, and removes one of the major 'unstyled' tell-tails, along with colouring the links black, and not default blue (saved 1 byte by writing a 3 digit hex code in '#000' instead of writing the five letter work 'black' - winning).

With these changes it also makes sense to have a dedicated <style> tag, instead of writing style="..." inside every tag.

Final Weigh In

And the final result? DebugBear now claims 0.99kB! Using Chrome's transferred measure it says 1kB, but on hovering reveals that to be 1009 bytes with a following wind (there always seem to be a little variation on repeated loads, around ± 5 bytes).

With that I was able to submit my patch, which now accepted means I've joined probably the most exclusive club I've ever been a member of. As Henry V almost certainly didn't say "The fewer the bytes, the greater share of honour".


1

Or perhaps 'character per character'? No? I'll get my coat.