How We Rebuilt the Hege Website to Get a Perfect PageSpeed Score. Part 4: Optimize HTML and JavaScript
In this next installment on the path to the promised land of lightning-fast websites, it’s time to get HTML and JavaScript bloat out of our way to PageSpeed bliss.
The Journey to PageSpeed Perfection Continues
Welcome to the fourth part in our series on perfect website performance.
If you haven’t read the previous parts, here you go:
You don’t have to read them to follow along in this article. But they’re full of useful details, so do check them out later if you haven’t.
As for this article, we’ll pick up where we left off in part 3, and turn to the dynamic duo of HTML and JavaScript.
Hold on to your hat!

How Much Faster Can You Get by Optimizing HTML and JavaScript?
There’s a lot to be said about optimizing HTML and JavaScript. But first off, let’s just see how much you can save in terms of download size. This will give a rough idea of how important this is.
Obviously, how much you can save is going to depend on a whole lot of things, and it will vary greatly from page to page. Which means it’s impossible to give a universal answer. But what we can do, is take the front page from our old unoptimized site, and compare it to the front page of our new site. (They look the same and have the same functionality, so it’s a fair comparison.)
Our old site wasn’t terrible by any means, but because it was hosted on Webflow, there was a lot of crap being pulled in that we didn’t need, but had no control over.
Here are the hard numbers:
| Old front page | New front page | Improvement | |
|---|---|---|---|
| HTML + inline JS | 19.3 kB | 8.5 kB | |
| External JS (sync) | 95.6 kB | 0 | |
| Total HTML + JS | 114.9 kB | 8.5 kB | 13.5x |
The table above shows the sizes of HTML plus render-blocking (synchronous) JavaScript (with standard gzip compression). This is data that the browser has to download before it can even start rendering anything on the screen. So cutting this down as much as possible is extremely important for high-performance.
On a slow mobile connection (1,638.4 kb/s), downloading the 114.9 kB of the old site takes around 560 milliseconds (ms). And that’s even before considering the extra 150 ms or so of TCP roundtripping needed to load the external JavaScript files. In comparison, the 8.5 kB of the new site can be downloaded in only 42 ms, with zero overhead from extra roundtrips.
That’s a huge improvement.
Let’s see how we did it.
Level 1: No Frameworks, No Render-Blocking JavaScript
This initial level is where most of the gains will be done.
Write Lean Semantic HTML
First off, write structurally sound HTML.
If you have no idea what that means, that’s okay. But you do have some learning to do. There isn’t nearly enough space to explain it here in this article, but there are tons of good resources online. Start with this HTML course, for example.
Writing good HTML has two main benefits:
- The page download size will be smaller (= faster).
- The browser will parse and render the page faster.
(As an additional benefit, it will also give you perfect PageSpeed accessibility and SEO scores.)
Don’t Build a Single-Page Application
For a normal website, you don’t need React. You don’t need Svelte. You don’t need Next.js. Forget about those, and forget about all other kinds of frameworks or libraries.
There is no reason to build your simple website as a single-page application. What you possibly win in subsequent page loading time, you lose on the initial load time (and on increased complexity). Just write custom HTML, and then sprinkle some JavaScript on top for additional functionality. If you optimize everything correctly, your site will be so fast it feels like a single page app anyway (thank you paint holding).
Don’t get caught in the let’s-use-a-framework-for-everything mentality. It will not serve you in the long run. Instead, learn the fundamentals and write lean semantic HTML, using JavaScript only when necessary.
Avoid All Render-Blocking JavaScript
As it turns out, avoiding frameworks and using modern HTML features, you need surprisingly little JavaScript. In our case, no more than 2.4 kB.
But whatever amount you end up with, make sure to use the async attribute to load it in the background, without blocking rendering:
<script
type="text/javascript"
src="/r/70001869.min.js"
async
fetchpriority="low">
</script>
Level 2: Use Native HTML Features
In the past, if you wanted more sophisticated interactive elements like dialogs, popovers, and swiping, you had to resort to JavaScript.
As of 2026, that is no longer true.
Use HTML Dialogs
Up until a couple of years ago, if you wanted to build any sort of popups, like cookie banners or hamburger menus, you had to use JavaScript. This was messy to get right across different browsers and devices, and the go-to solution would be to let some JavaScript library handle it for you.
Fortunately, since a few years back, that’s no longer needed. With HTML now natively supporting dialogs using the <dialog> element, all these things can be done with very little JavaScript. And not only that, being native, they work much more predictably and smooth, too.
As an example, the hamburger menu on our mobile layout is built with an HTML dialog element:
<dialog closedby="any" id="nav-c">
... menu content here ...
</dialog>
To open and close the menu, we use exactly two lines of inline JavaScript, attached to two buttons.
One button to open the menu:
<button onclick="document.getElementById('nav-c').showModal()">...</button>
And one button to close it:
<button onclick="document.getElementById('nav-c').close()">...</button>
That’s it. A perfect hamburger menu, with two lines of JavaScript.
Use HTML Popovers
An even newer feature in HTML is the popover attribute, which lets you show all kinds of “popovers”.
A popover is very similar to the dialog described above, with the difference being that a popover is always “non-modal”, meaning the user can still interact with the page behind the popover. The exact details don’t matter for the purpose of this article. The point is that whenever you have some content that you want to pop out and show in a box on top of the rest of the page, use native popovers instead of some janky JavaScript library.
We use these popovers on the League of Hegends page. With the popovertarget and popovertargetaction attributes, no JavaScript is needed at all.
All we need is this HTML for the actual popover:
<div id="info-popover" popover="auto">
... popover content ...
</div>
And then buttons to open and close it:
<button popovertarget="info-popover" popovertargetaction="show">Open<button>
<button popovertarget="info-popover" popovertargetaction="hide">Close</button>
Perfect popovers. Zero JavaScript bloat.
Use Scroll Snapping For Carousels
Thirdly, we have two classic carousels on our front page. On the phone, they can be swiped back and forth, and on the desktop there are two buttons below for navigation.
These carousels are built with CSS scroll snapping, plus a total of 7 lines of JavaScript. Yes, 7 lines.
We’ll cover all the details of this setup along with other CSS optimizations in the next article in the series.
Level 3: Minify, Shorten Fingerprints
With the native HTML features covered, we’re now entering the realm of micro-optimizations, where the fun starts!
Minify JavaScript
First off, minify all JavaScript.
How much this will save depends on how much JavaScript you have. Since we have so little, we actually don’t save anything by doing this. But it’s still a good practice, so we do it anyway.
Exactly how to minify your JavaScript will depend on how your website is built. It’s usually not too complicated. In Hugo, we can just pipe the resource through the minify filter:
{{ with .Resources.Get "js/async.js" | minify }}
<script type="text/javascript" src="{{ .RelPermalink }}" async fetchpriority="low"></script>
{{ end }}
Minify HTML
HTML can also be minified, mostly to remove whitespace. Honestly though, this isn’t going to matter much, because of gzip compression.
That said, we still do it. Not to save space, but to make the HTML pretty. Because yes, that matters to us. (View the source of this page and see for yourself!)
Here’s how we do it: after building the site with Hugo, the last step of our build process runs the output through Tidy, with the following options:
# Make output HTML pretty
find build-output -path "*.html" -type f \
-exec tidy \
--quiet yes \
--indent no \
--show-filename yes \
--drop-empty-elements no \
--fix-style-tags no \
--warn-proprietary-attributes no \
--wrap 0 \
--tidy-mark no \
--omit-optional-tags no \
-o {} {} \;
(If you’re even more hardcore than we are, you could change --omit-optional-tags to yes, to make the HTML a tiny tiny bit smaller.)
Shorten Asset Fingerprints
Now to the final micro-optimization. To be honest, this one is probably not going to make a difference. But it was interesting, so we’ll cover it just for that.
In the first article in the series, we explained how to use asset fingerprinting to optimize HTTP caching. In Hugo, this is easy, just add the fingerprint filter:
{{ with resources.Get "img/apple-touch-icon.png" | fingerprint }}
<link rel="apple-touch-icon" href="{{ .RelPermalink }}">
{{ end }}
This automatically fingerprints the asset and produces this HTML:
<link
rel="apple-touch-icon"
href="/img/apple-touch-icon.a3ba4e3e5ee46d71e8430e5e22f36f6d490a2c36e5da38c5f5cb46bbbee46ac2.png">
Okay. So what’s the problem here?
The problem here is that awfully long hash in the filename.
By default, Hugo uses the sha256 hashing algorithm, which gives 256 bit hashes. Encoded as 64 character hexadecimal strings, this means that every fingerprint adds 64 bytes to the HTML. And because these hashes are irregular, gzip compression can only help so much.
Now, 64 bytes might not seem like a whole lot, and to be honest… it isn’t. But, the question is, can we do better?
Yes, we can.
A first improvement would be to use the md5 algorithm instead, which cuts the hash size in half, down to 32 characters.
But we can do even better than that.
Let’s use the 32-bit FNV hash. This is good enough for us, and even when represented by an integer, it’s just 10 characters. And while we’re at it, let’s also put all the fingerprinted assets under /r/, without including their original filenames, to make things neater and save a few bytes more.
Putting this together, we can make a partial like this:
{{ $res := resources.Get . }}
{{ $fp := $res | fingerprint }}
{{ $tgt := printf "/r/%v.%s" (hash.FNV32a $fp.Data.Integrity) $fp.MediaType.FirstSuffix.Suffix }}
{{ return resources.Copy $tgt $res }}
And then use it like so:
{{ with partial "res.html" "img/apple-touch-icon.png" }}
<link rel="apple-touch-icon" href="{{ .RelPermalink }}">
{{ end }}
This fingerprints the asset, and produces this HTML:
<link rel="apple-touch-icon" href="/r/3923634518.png">
Neat.
Did this make an actual difference? No. All in all, it saved us around 2 kB, or 10 milliseconds of download time on a slow connection. But it made us feel good.
Conclusion: No Easy Tricks
And that concludes all the optimizations we did relating to HTML and JavaScript.
As you saw from the numbers in the beginning of the article, taken together, these optimizations had a huge impact on cutting the time to start rendering, saving hundreds of milliseconds. That has a real impact on user experience.
What’s important to understand here is that these gains mostly came from avoiding things, rather than fancy technical tricks.
The most impactful things by far were these:
- Write lean well structured HTML.
- Don’t use JavaScript frameworks.
- Use native HTML features instead of JavaScript.
On top of that, make sure to also:
- Load all JavaScript asynchronously.
Minifying JavaScript is a good idea, too. But if you do all the above, you might end up with so little JavaScript that it doesn’t really matter anyway.
Finally, a few optimizations were neat, but had little effect on actual performance:
- Minify HTML.
- Shorten fingerprint hashes.
The Next Step: Optimize CSS
Well. That was it for this article. Prior in the series, we’ve covered how to optimize both fonts and images. With this article, we’ve now also covered HTML and JavaScript.
That only leaves one big piece left to cover before we reach PageSpeed perfection: CSS.
This will be the topic of the next article, where we’ll cover how to use CSS animations, scroll snapping, and how to minify and inline everything to really make things move fast.
Until then, have a great week, and make sure to hop into our Telegram and say hi!
— Team Hege ✌️
PS. Check out the piece “BLUE AETHER” below. This collectible is up for sale on our DRiP!