Please, Keep your Blog Light

TL;DR: keeping your blog lightweight is important, I show you how to design a blog fitting in less than 10kB.

You’re already convinced weight really matters when it comes to web pages? You can skip the introduction and directly see how you can reduce your blog’s weight through a practical example.

Why Does Size Matters?

I grew up in a French isolated village during the early 2000’s. Living in an isolated place during the early 2000’s meant no public libraries, no technical bookstore, no access to computer-related knowledge or any kind of technical expertise. When my parents decided to subscribe to an RTC connection, everything changed. All of sudden, I had access to virtually an infinite amount of information about computers, RC models, and other technical fields.

By reading blogs and forums, I had access to everything a geeky kid could dream of: I learned to write websites using table-based HTML, I learned to fly RC models, I learned to read English, I learned to write PHP 4 using l33t-sp3ll3d identifiers, I learned to exploit an XSS in order to bypass my monthly hosting quota, I learned that doing this would lead me to unpleasant consequences.

I learned all of that using a 3.5kB/s crappy RTC connection. I was able to access these information with such ease because the internet was then mostly text pages, because web-pages were still relatively light.

Fast forward 15 years, I do not use l33t identifiers anymore, I don’t fly RC-Models anymore but I still love to write computer programs! Like almost all western programmers, I spent most of my adult life in big cities where internet is cheap and fast.

Eventually, I decided to take a sabbatical year. The first thing you do in such a case is cutting unnecessary expenses to make it as long as possible. So, as a grown-up man, I downgraded my mobile data plan to the cheapest one, the one allowing me to download 25MB monthly. I mostly use my phone for chatting and reading blog posts, I thought 25MB per month was more than enough.

As I often am, I was wrong. Four days after subscribing to this plan, I received the - now familiar - “out of data” notification.

What the hell!?

Well, it turns out the web became heavier than I thought.

Let’s load a medium post:

3.26 MB Transferred total
1.2  MB Html + JS + CSS + Fonts

3.26MB per post!?

At this rate, I am able to read 7 posts per month… Do this article really worth a seventh of my monthly data plan? I highly doubt it.

This page would take around 9 minutes to load using my RTC childhood connection.

I took medium as an example as it seems to be the most popular blogging platform nowadays; keep in mind this is not an exception, this is a trend. A lot of popular blogs tends to be really heavy!

You may say “it’s not a problem: RTC connections became quite rare and time travel devices are still primitive”. You’re right, but look at the latest akamai speed report. The global internet median speed is ~8Mbps: that’s around 1MB/s.

This medium article, on average, will take 3 seconds to load on a device. But that’s even worse! The website is full of asynchronous requests, on a 2017 dell xps laptop, a full render of the page takes approximatively 2 seconds. Yup, this page will take 5 seconds to load on a recent high-end laptop using an average internet connection!

This is a huge latency to display a simple text page on a pretty fast device!

Are we doomed to increase the global web bandwidth and experience high latency for the rest of our lives? Is making something light so complicated? Let’s figure that out by making this blog as light as possible.

A Practical Example of Lightweight Blog

Luckily for us, making a blog fast is more about avoiding to add unnecessary dependencies than profiling and optimizing the rendering. This game is all about being minimalistic without altering the reading ergonomics.

Let’s take a pragmatical approach and think about what we need here:

  • Static website: we want to deliver the blog as quickly as possible. No need to be delusional here, you won’t post regularly enough to make a dynamic website appealing :).
  • Responsive design: we want the text to be as comfortable as possible to read regardless the device used. We should at least provide a desktop, a mobile, and a tablet version of the design.
  • Lightweight: The total weight of the page (HTML + assets) should not exceed 20 kB. We also should leverage browser caching.
  • Pleasing design: alright, this one is totally objective. But at least, I want something appealing to me.
  • JavaScript and CDN free: we don’t want to use any javascript rendering nor use any external CDN to serve the assets.
  • Minimal Effort: we don’t want to spend more time designing the blog than actually writing in it. We don’t want to re-invent the wheel here. The blog should setup should take us less than 4 hours (ie. a Sunday afternoon).

Generating your Blog’s Pages

First, we need to pick a static website generator. There is no lack of such software, just pick one. I chose Hugo: its documentation is nice and it supports server-side syntax highlighting.

We then need to write a theme for our blog. I would advise starting one from scratch, it’ll be quicker: templates tend to be quite bloated and having many dependencies.

I’m not going to dive into the details for the whole HTML/CSS part, but rather just give you an overview of what you need to write your design without loading tons of external libraries. If you have any problem related to web-design, MDN is usually a good starting point.

If you did not have a look at web dev for the last few years, I have a good news for you: you don’t really need a CSS framework anymore! You can thank CSS’s flex boxes; this feature is now supported by almost all active browsers and is basically all you need to organize your HTML elements. If you don’t know how to use them, the introduction written by Mozilla is what you’re looking for!

In order to adapt the design for smartphones and tablets, we need to use media queries. Contrary to popular belief, they are pretty simple to use: three rules according to the screen width will get you covered for most usual devices:

  • > 1000px: desktop and big screens.
  • 880 > width > 1000px: tablets, netbooks and small screens.
  • < 880: smartphones.

You can easily debug your media queries related code using a device emulator. It’s included in both firefox and chrome web-dev tools.

Setting up an appropriate text width is primordial. Reading a text being too wide or too narrow is really uncomfortable. The desired width of the text will depend on the device; on a mobile phone, you want to use as much space as possible, while on a desktop, you want to display your text in a narrow centered column.

I used the following rules for this blog (vw being the viewport width):

@media (min-width: 1000px){
    width: 50vw;

@media (min-width: 800px) and (max-width: 1000px){
    width: 70vw;

@media (max-width: 800px){
    width: 95vw;

Regarding fonts, I made the heavy opinionated choice to only use the sans serif one embedded in the reader’s system.

body {
  font-family: sans-serif;

On one hand, the font will differ depending on the system, on the other hand, it will reduce the assets size. But as I said earlier, it is a quite heavy opinionated choice, default fonts can be quite uncomfortable to read, especially on old Windows systems.

[2022 edit]: I ended up reverting that decision and serve a web font (IBM Plex Sans). The default sans system font is sadly severely out of date wrt. modern visual standards on a lot of systems :(

I avoided using images as much as possible. I actually ended up having one image for this theme: the small icon displayed in your browser tab.

To illustrate the article, diagrams often help. Using SVG is a good way to keep everything light without sacrificing diagrams. Using Inkscape is a good way to edit those diagrams. Don’t forget to export the project to “standard SVG” or you will dump some private metadata along with your image.

Having the whole article included in the RSS feed is quite convenient: it gives the reader the option of not using your website’s stylesheet. Sadly, Hugo’s default RSS feed only renders the first ~100 words of each article. Here’s a small template overriding this behavior.

% cat /layouts/posts/rss.xml

<rss version="2.0" xmlns:atom="">
    <title>{{ .Title}} </title>
    <link>{{ .Permalink }}</link>
    <description>Recent posts</description>
    <generator>Hugo --</generator>{{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with }}
    <managingEditor>{{.}}{{ with $ }} ({{.}}){{end}}</managingEditor>{{end}}{{ with }}
    <webMaster>{{.}}{{ with $ }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
    <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
    <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
    {{ with .OutputFormats.Get "RSS" }}
        {{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
    {{ end }}
    {{ range .Data.Pages }}
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with }}<author>{{.}}{{ with $ }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ .Permalink }}</guid>
      <description>{{ .Content | html }}</description>
    {{ end }}

Don’t forget to add a link to the RSS feed both in the footer and the <head> section:

<link href="/posts/index.xml" rel="alternate" type="application/rss+xml"/>

Alright, now we designed everything, we are almost done.

We need to give our assets a unique name. We’ll need to reflect any change to their name to properly leverage the browser cache. You have two options: either you fire npm and setup an asset minifier, either you simple prepend DDMMYYYY on each filename like me :).

Let’s not kid ourselves, we are going to alter this theme twice a year, automating that won’t gain any time.

Distributing the Content

Now your blog is generated on your computer, it is time to publish it on the web. You have two major choices here:

  1. You do not want to spend any money on it: just push it on GitHub pages and you’re done. You can skip this and directly read the next section.
  2. You want to deliver it from your *nix server.

First of all, you want to serve the blog on a TLS-only link. Maybe your ISP/State does not alter your HTTP traffic, but it’s not the case everywhere. Please, think about your potentially endangered readers and protect them from a nasty rogue javascript injection.

Let’s Encrypt is a free and entirely automated TLS certificate provider. The first issuing will need some manual configuration, but afterward, all you’ll need for a cert renewal will be a simple cron rule fired twice a month.

We then need to configure Nginx to:

  • redirect HTTP requests to the HTTPS endpoint.
  • include the HSTS policy header.
  • support IPv6 and HTTP2.
  • deliver the static content with a specific HTTP header telling that clients should forever cache the content.
  • GZIP the text-based content.

Here’s a quick dump of my configuration. You can use it as a starting point:

server {
     listen         80;
    location '/.well-known/acme-challenge' {
        default_type "text/plain";
        root        /tmp/letsencrypt-auto/;
     location / {
        return         301$request_uri;

server {
   listen 443 ssl http2 default_server;
   listen [::]:443 ssl http2 default_server ipv6only=on;
   ssl_certificate $PATH_TO/fullchain.pem;
   ssl_certificate_key $PATH_TO/privkey.pem;
   client_max_body_size 100m;
   autoindex off;
   root $PATH_TO_FILES;
   add_header Cache-Control "no-cache, no-store, must-revalidate";
   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
   location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
       add_header Cache-Control "public, max-age=31536000";
       add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

   # Gzip black magic
   gzip on;
   gzip_disable "msie6";
   gzip_vary on;
   gzip_proxied any;
   gzip_comp_level 6;
   gzip_buffers 16 8k;
   gzip_http_version 1.1;
   gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;



Please, don’t.

You Now Can Open your Text Editor

So, is this mission successful? Let’s dig into the metrics.

All the following tests have been conducted on the very same page hosted on the very same HTTP server you are currently reading this post.

Let’s first test the first page loading (no browser caching involved).

Transferred: ~9kB
Unzipped Size: ~25kB
Finish: ~200ms

Yup, the whole content + assets fits just under 9kB and takes less than 500ms to be both downloaded and fully rendered on my laptop.

Let’s load the same page a second time:

Transferred: ~7.3kB
Unzipped Size: ~25kB
Finish: ~200ms

Now browser caching is leveraged, only 7.3kB has been transferred!

It would take ~2 second to download this long page from my childhood RTC line! The latency is way lower than on a typical medium post; I guess we can say this experiment is a success!

As you can see, making a light low-latency blogging system is quite easy. All the above experiment has been implemented on a Sunday afternoon. Less than 3 hours if we exclude the NGINX configuration black magic.

If you have a blog and are actively using it, please, invest those 3 hours and contribute to make the web a better place :)