simonewebdesign

One request

In an era where 95% of sites are bloated with cookie banners and behavioral tracking scripts, here’s a refreshing approach to web development.

Nowadays, loading a website on your device of choice can be painful — both for the browser, which has to parse and render all that stuff, and for you, the user, who has to wait for it (and pay for that data plan).

The issue is only exacerbated by the fact that mobile devices can be slow, the network can be slow, and there’s simply too much cruft to download.

There is a better way.

What if you could make your entire website fit into a single page?

A lot of sites are built as a single page (can we have more of those?)— and no, I don’t mean a SPA. I’m talking about something radically different here. Essentially, the challenge boils down to this:

Can I reduce every page down to a single HTTP request?

First of all, why? You may ask. Well, many reasons. I did this to my blog, simonewebdesign.it. I mostly did it just to prove it possible, but it turned out to be a fun challenge that kept me busy for months and resulted in a much faster, more maintainable website, with a very small carbon footprint.

But the real question is, how?

Inlining all the things

This is essentially how I’ve done it. I’ve inlined everything that could’ve possibly been inlined. Keep reading if you’re curious and want to know how I achieved this.

Inline styles

This is where the journey started. I refactored some CSS, got rid of a few superfluous HTML tags, and the stylesheet got so small that I thought, why not just inline it? So I looked into that.

I thought I should be able to inline the compiled style.css’s contents into a <style> tag, somehow. This turned out to be a challenge. I couldn’t find a tool that did exactly this, so I wrote my own — or, to be more specific, I sent a pull request to inline-scripts, which seemed like the closest thing I could find. At first, all it was doing was inlining <script> tags, so I only had to do the same, but for CSS — a simpler job, figuratively speaking.

Anyway, with some clever Ruby scripting I managed to minify all the HTML, CSS and JS in one go. I would first inline the CSS “on the spot” (i.e. without creating a new file):

Dir["public/**/*.html"].each do |file|
  puts "Processing #{file}..."
  system "node_modules/.bin/inline-stylesheets #{file} #{file}"
end

I would then run html-minifier as such:

html-minifier --file-ext html --case-sensitive \
 --collapse-boolean-attributes --collapse-whitespace \
 --minify-css true --minify-js true \
 --remove-attribute-quotes --remove-comments \
 --remove-empty-attributes --remove-empty-elements \
 --remove-optional-tags --remove-redundant-attributes \
 --remove-script-type-attributes \
 --remove-style-link-type-attributes \
 --remove-tag-whitespace --sort-attributes \
 --sort-class-name --trim-custom-fragments \
 --use-short-doctype

And there you have it — a highly optimized, one-liner HTML file with inline CSS and JS — for every page.

Inline manifest.json

The manifest.json is usually pretty small, so it makes sense to inline it.

How did I do this? It was actually pretty simple: I made it into a data URI of type data:application/manifest+json, which is then loaded as a <link rel="manifest">, just as usual. Here it is, in its full one-line glory:

<link href='data:application/manifest+json,{"name":"Simone Web Design","short_name":"SimoneDesign","theme_color":"%23555","background_color":"%23f6f6f6","display":"minimal-ui","description":"A tech blog"}' rel=manifest>

The only catch was that I had to encodeURIComponent.

Analytics

I got rid of client-side analytics. Cloudflare already gives me enough stats, such as the number of requests, unique visits and page views, grouped by visiting country. I don’t really need any more than that.

As an interesting side note, when I check the analytics on Cloudflare, it also shows a Carbon Impact Report, which claims to have saved me 836 grams of carbon (in 2020 vs average data centers) — this is equivalent to turning off one lightbulb for 20 hours, apparently. It sounds an awful lot like greenwashing, if you ask me, but to be fair they do seem to have put effort on reducing the Internet’s environmental impact.

The server

I use Puma. It is a fast server that provides parallelism out of the box. This is what my Procfile looks like:

web: bundle exec puma --threads 8:32 --workers 3 -p $PORT

Basically what this means is that, at any given time, there are 8 threads ready to serve you a request, over 3 separate clusters.

This may be a little overkill, since I also use Cloudflare as a caching layer on top of this server, which is load balanced and globally distributed. I also don’t usually get much traffic, so this definitely achieves the goal of speed.

Hosting and DNS

I recently switched to a new hosting provider, Fly.io. I had to do that since Heroku, my old provider, unfortunately discontinued their free plan. Sad news, I’ve used it for 10 years, but it was time to move on.

Fly has a pretty sound infrastructure with modern features:

  • Edge TLS termination. By handling TLS at the edge, the handshake is handled by the nearest datacenter to the user. As a result, latency is decreased.
  • High performance micro VMs on bare metal servers. The server replies faster.
  • HTTP2 and Brotli compression. The data gets sent faster.

Last but not the least, I was able to remove a CNAME record and use an A record instead. This resulted in one less server roundtrip.

Other minor optimizations

These are loosely related to the “one request” thing, but still worth mentioning.

Moving to modern media formats

Lots of formats have actually been superseded by more efficient ones: PNG to WebP, GIF to MP4, JPEG to AVIFthe list goes on — these are just the ones I’m aware of. I don’t think these should be inlined, but new formats are definitely worth the effort, since they’re much more performant.

The favicon

I went from PNG to SVG for the favicon, which I blatantly stole from Peter Selinger, the guy behind Potrace (it was public domain, technically). What I did on my part was optimizing it even further, using Jake Archibald’s wonderful SVGOMG, powered by SVGO.

As for inlining it, I had to serialize it into a data URI using mini-svg-data-uri. I even ended up making a CLI out of it — it’s something I had to do anyway, and contributing back was the least I could have done.

The fonts

I got rid of the custom Google font I was using and went for system fonts. This is what I have now:

html {
  font-family: "PT Serif", Georgia, Times, "Times New Roman", serif;
}

I don’t actually provide PT Serif, however. If your machine happens to have that, great — if not, it’ll fall back to the next one. I might reconsider this choice in the future, but for now, this is good enough.

The JavaScript

I waited until the end to say this, because you probably wouldn’t have believed me, but this site doesn’t have any JavaScript, the only exception being made for the ServiceWorker registration:

<script>
  navigator.serviceWorker.register("/sw.js")
</script>

The ServiceWorker is actually a separate JS file, because I couldn’t find a way to inline that (if you do know of a way, please let me know). But, other than that (and Disqus, which I’m planning to remove soon), I don’t need JS at all. ¯\_(ツ)_/¯

Conclusion

I hope you liked this article at least as much as I enjoyed writing it. I hope it tickled your curiosity, to the very least, and that - by shedding a light on the importance of performance - I’ve inspired you to take action and improve your own site.

View this page on GitHub