When I started my web site, we didn’t have static site generators or WordPress or GitHub Pages or much else. People wrote HTML1 by hand. Over the years, I’ve built my own setup to manage my site. My current setup involves writing XHTML that gets transformed into HTML using XSLT.
My XSLT template and CSS styling are global. They apply to the more than 30 years of pages I’ve written. That means whenever I change the XSLT or CSS, I need to make sure the change works for the entire site, over 800 articles. Until now I’ve been doing that manually by spot checking the popular articles.
While working on my SDF font guide, I noticed an issue with the white space. There were some spaces missing. It’s easy to work around, so I did — I added in a few places. This has been a problem for a while and I just work around it each time. After I finished the project, I decided to dig into the root cause.
Part 1: de-optimizing
For a long time I’ve been removing as many spaces as I can from the final HTML, to make the page smaller and faster to load. But it makes the pages hard to read and debug. When I write this:
<ol> <li><i>Wilma</i> <b>Fred</b></li> <li>and Dino</li> </ol>
my transform rules turn it into:
<ol><li><i>Wilma</i> <b>Fred</b></li><li>and Dino</li></ol>
In this example it’s ok to remove the spaces between </li><li> but it’s not ok to remove the spaces between </i> <b>. And it’s often ok to collapse multiple spaces into one, but not inside <pre> or <script>. It’s tricky. I looked through my XSLT and found that I had kept adding more rules over the years:
- Remove a bunch of spaces.
- Add a bunch of spaces to undo some of step 1.
- Remove more spaces to undo some of step 2.
- Add a bunch of spaces to undo some of step 1 and 3.
- Remove more spaces to undo some of step 2 and 4.
What a mess! But each time I had added a rule, I hadn’t been looking at the whole process. I was fixing one situation. If I did that again, I could add rule 6 to add more spaces to partially undo a previous step.
Why am I doing this at all?
When I started my web site in 1994, compression wasn’t common, so removing 1000 spaces from the input would make the page load 3 seconds faster on a 2400 baud modem. The spaces mattered a lot to me. But in 2025? It’s much less important, both because of compression and because internet speeds are much higher now.
These rules are tricky and I was still getting it wrong sometimes. I’ve written before about de-optimizing to make something easier to work with. I decided to de-optimize here by removing these whitespace rules. Not only will make it easier for me to debug my pages, it also makes it easier for anyone using View Source to see how I wrote the page.
Since browsers collapse multiple spaces in HTML into one, I expected only minor visual differences. An extra space here or there is hard to spot by quickly skimming a page. White space changes are subtle and hard to catch with manual inspection. I decided the best approach would be to break up the change into small steps, and take screenshots before/after each step.
I’m not the only one who wants to test the effect of a CSS change across an entire site. I used Playwright, as recommended in Florian Marending’s blog[1]. It can take screenshots using the three major rendering engines (Chrome’s Blink, Firefox’s Gecko, and Safari’s WebKit). Instead of using their output format, I wrote my own, using pixelmatch[2] to create a diff image:
for f in /tmp/yes/*firefox*.png do node_modules/pixelmatch/bin/pixelmatch $f /tmp/no/$(basename $f) /tmp/diff-$(basename $f) 0.1 || \ echo "<div style='display:grid;grid-template-columns:repeat(3,1fr)'><img src=diff-$(basename $f)><img src=yes/$(basename $f)><img src=no/$(basename $f)></div>" \ >>/tmp/diff.html done
Usually a one space difference will cause the rest of the line to shift, showing up as a red bar. In this change both outputs could be considered correct:
In this change the whitespace rule had mangled the output in a way that I hadn’t noticed:
I found no examples where the whitespace rules made things better. I found plenty of examples where they made things slightly worse, and a few that made things much worse, like this:
After testing my site locally, I made the change and pushed to production.
Part 2: re-optimizing
I had mentioned the whitespace experiments on social media and got a suggestion to switch from Gzip to Brotli[3]. I knew Brotli existed but I hadn’t looked into it. And wow, it is great! I ran Paul Calvano’s compression tester[4] to get a sense of how it would perform on my site. Here’s the compression ratios for the hexagons guide:
| page | gzip dynamic | brotli dynamic | brotli static | zstd static |
|---|---|---|---|---|
| theory page | 3.4 | 4.5 | 5.1 | 4.7 |
| implementation page | 3.6 | 4.4 | 5.0 | 4.6 |
| diagrams javascript | 3.8 | 4.8 | 5.3 | 4.9 |
| vue.js minified | 2.4 | 2.9 | 3.1 | 3.0 |
The dynamic version makes the web server compress each time the browser requests the page. It has to balance compression time and compression ratio. The static version compresses ahead of time, and can use a slower algorithm with a better compression ratio. I also looked at Zstd. Caddy supports Zstd dynamic and static, and Brotli static only. Nginx has third party modules for Zstd dynamic and static, and Brotli dynamic and static.
Switching from Gzip dynamic to Brotli dynamic is a far bigger improvement than the small amount I saved on whitespace. I may try switching to Brotli static at some point, but I need to restructure my build step a bit.
So I now have a smaller faster web site with better formatting, and also learned a tool that will help me test future global changes. It was a good week!