A Hidden Relaunch - Replacing a lot of jQuery code with some CSS

Developing matthias-kainer.de in vscode

It’s not entirely wrong to say I had a website all my life. My first baby steps in the web I took many years back with a tool called Microsoft Publisher. Pretty soon I learned that such a program would constrain me too much, and I started to write my HTML tables in notepad. Creating a new version of my homepage escalated quickly, and eventually, I even had my own website-publishing startup.

The website you see now is an extended version of one of the templates from html5up. The technology stack was always pretty simple.

The template is rendered from a jade/pug template from an express application. The application was basically only needed for the contact form, the rest could have been a static HTML as well. When this website started to exist, things like flex, CSS animations and so on where to be expected in a distant future, but jQuery was still a big thing. It used version 1.11.0, which was released somewhere in 2014, mostly for the animations.

For having a responsive grid, I used skel, a library that is deprecated, too.

It was time to move on.

I wrote down features that I wanted to keep, and came up with the following shortlist

I also wanted a few new features:

To achieve the value, I planned to play the following stories:

CSS instead of JS

The old website had 1MB of compressed javascript, and 20 kb of CSS.

The first thing I did was to remove all javascript and checked what’s still working functionally.

Everything.

Well, actually that was expected. I had my website set up such that it would also work without javascript. It still felt weird dropping 1MB of code, and everything was still as before. Only the animations and the grid were missing.

The following animations are on the page:

The whole grid part (the skel source and initialization, quite a bit of javascript) I replaced with the following two lines of css:

.row { display: flex; }
.row div { flex-grow: 1; }

While the elements still have the u4, u6, u12… classes to specify the columns in a row, it’s no longer needed. Another refactoring on my list.

The animations weren’t more difficult

Smooth scrolling with CSS

Every anchor on the start page was smoothly scrolling via a jquery plugin. With javascript, it worked in every browser. Smooth scrolling is nice, but no absolute must-have in every browser. Most modern browsers support scroll-behavior: smooth;, which does the same thing.

Adding

html {
    scroll-behavior: smooth;
}

was, therefore, all it took. Except for non-chromium Edge, IE and Safari, all browsers are supported.

Fading out an absolute positioned element

That was more tricky. CSS Transitions allow you to have a lot of nice effects, but unfortunately, they will not be working when you want to toggle display. As the loading window is the topmost element, simply changing the opacity will not be enough, as no link could be clicked on the page, as an invisible element is still on top.

To work around this, I came up with a new animation that would reduce the opacity, and also change the top/bottom position

.initloading {
    display:block;
    opacity: 1;
    transition: all 1s ease-in-out;
}
.initloading.inactive {
    opacity: 0;
    bottom: calc(50%);
    top: calc(50%);
}

The triggering of the inactive class still needs some javascript, because the CSS will not know when the page is loaded. In the noscript.css the loading will, therefore, be invisible from the start.

The first new lines of javascript are then to add the inactive class to the loader.

function loadingCompleted() {
    document.querySelector('.initloading').className += " inactive";
}

if (document.readyState === 'complete') {
    loadingCompleted();
} 

document.addEventListener('readystatechange', function () {
    if (document.readyState === 'complete') {
        loadingCompleted();
    }
});

Refresh the start page to see the effect.

Fly-In

Flying in when the user scrolls to a position cannot be done with CSS alone. As it is only eye candy it’s fine to have no-script browsers not have it. The script is slightly more advanced though.

First I have to find all elements and mark them as inactive

// possibly dangerous and stupid, but pretty convenient and works quite well
NodeList.prototype.forEach = Array.prototype.forEach;
var animatedElements = [];

function hideInactiveElement(el) {
    if (el.className.indexOf("inactive") < 0) {
        el.className = el.className + " inactive";
    }
}

// This is the list of elements that should be animated
[".blog-card", ".right", ".left", ".row.images", "#contact"].forEach(function(className) { 
    document.querySelectorAll(className).forEach(hideInactiveElement);
})

document.querySelectorAll(".inactive").forEach(function(el) { animatedElements.push(el) });

In the stylesheet, we need two classes. One that shows, the other one that hides the elements. All of the animations are done via CSS.

.active {
    position:relative;
    left: 0;
    opacity: 1.0;
    transition: all 2s ease;
}
.inactive {
    opacity: 0;
}
/* to control coming in from the sides */
.inactive.left {
    left: -65%;
}
.inactive.right {
    left: 100%;
}

When we open the page now, all elements will be hidden. To show them, we’ll have to attach a handle to the document scroll method that adds the active class when it comes into view. The full code looks like this

NodeList.prototype.forEach = Array.prototype.forEach;
var animatedElements = [];

function isScrolledIntoView(el) {
    var rect = el.getBoundingClientRect();
    return rect.top < window.innerHeight && rect.bottom >= 0;
}

function scrollEventHandler() {
    animatedElements.forEach(function(el) {
        return (isScrolledIntoView(el))
            ? showInactiveElement(el)
            : hideInactiveElement(el);
    });
}

function showInactiveElement(el) {
    el.className = el.className.replace(/\b inactive\b/g, " active");
}
function hideInactiveElement(el) {
    if (el.className.indexOf("inactive") < 0) {
        // replace the active class now in case it's already there
        el.className = el.className.replace(/\b active\b/g, "") + " inactive";
    }
}

[".blog-card", ".right", ".left", ".row.images", "#contact"].forEach(function(className) { 
    document.querySelectorAll(className).forEach(hideInactiveElement);
});

document.querySelectorAll(".inactive").forEach(function(el) { animatedElements.push(el) });

document.addEventListener('scroll', scrollEventHandler);

When we scroll now, the elements will fade in. With javascript disabled, the elements will simply be there.

Lazy loading images

A lot of images are “below the fold”, so not visible when the user opens the page. Therefore, they should not block the loading of the website. I followed the idea to specify a default image to load initially (and only once, as all images would be the same) and set the URL of the others in a data attribute.

<img src="/placeholder.png" data-src="/real-image.jpg" />

To load the image, I call the line

document.querySelectorAll(".lazy").forEach(function(el) { el.src = el.dataset.src; })

at a place, I see fit. My first idea was to use the scrollEventHandler I defined previously. Whenever the element would become visible, then I would call the function. While this makes it very explicit what is happening, and people could be like “Oh look lazy loaded images! Neat!” what really happens is that there is a lot of movement on the page on top of the animations. Too much, even for me. So I moved the call inside a setTimeout with 500ms in the loadingCompleted function I defined previously.

For people browsing my page without javascript, every lazy loaded image has a noscript element attached to it with the image loaded as usual.

Basically the same I’m also doing for background images. The noscript.css file loads the background images normally, while for script-enabled users all other background images are loaded after 500ms.

Result

On my MacBook Pro in chrome, it took about 6 seconds until the page fired the completion event, loading 4MB of data. In Firefox it took even longer, up to 8 seconds.

Now, the page is loaded after less than 1MB (quite a lot of that is coming recaptcha), and after everything is loaded we are only at 2.4 MB. That’s the ~1MB for the javascript, and ~500kB for stylesheets I could remove that were coming with 3rd party libraries.

The javascript I had to maintain - in the old days that would initialize the jquery plugins, wire them together, attach them to the correct element, setting up the animations - was ~500 LOC. This has been reduced to 50. According to the book Code Complete, this would mean that I reduced the number of defects from 7-25 to 0.7-2.5. Having a zero-bug website is much easier like that.

The page is now completely loaded on my MacBook Pro in chrome in less then 2 seconds, same goes for Firefox. Looking at the waterfall of the webpage, the difference of loading in under 1 second and now is recaptcha. Optimizing this for speed will be the next task, for now, I’m happy.

old new
Downloaded until loaded 4MB 2.4MB
Time until loaded 6-8 seconds 2 seconds
Predicted number of defects in js code 7-25 0.7-2.5

Was worth the effort it seems :)