Some tips to boost your new website

Posted on February 28, 2018

When I release a new website, I like to run a performance test using the WebPageTest tool. It provides a good analysis and gives some clues to boost your app.

So, this blog doesn't have to be an exception and the results were pretty clear: I got a C on "First-Byte time" and a F on "Cache static assets" and "Compress images". Also, my Speed Index was around 1400: not so bad but still bigger than the max recommended (1000).

It looks like we can improve some stuff here.

Compress images

Images are always a critical point when dealing with performance, because they're often heavy. However, it's easy to drastically reduce their size. On this blog I only have one picture, in the header. Before any optimization it was ~1.1mb ... more than 90% of my total assets weight !

On my Mac I use imagemagick to compress my pictures following some basic rules :

 magick convert INPUT.jpg -sampling-factor 4:2:0 -strip -quality 85 -interlace JPEG -colorspace Gray OUTPUT.jpg

Setting the quality to 85 will decrease a lot the file size, while keeping a visual rendering as good as full quality (100). On my image I even decided to go to -quality 50.

By using this, the picture final size is 173kb. Neat !

Beyond compression, you should probably take care of responsive images.

Reduce CSS/JS sizes

When talking of reducing CSS/JS sizes, a frequent thought is minification, which is really helpful. If you are using Laravel mix as me, this is very simple:

# yarn or npm
yarn run production

This will run the production task, which will perform minifying on your assets under the hood. Smooth.

However, you should take care of what you are including in your CSS or JS. On big libraries, like Bootstrap, you can generally import modules individually. This can help to reduce your assets size, by importing only what you need.

On this project, I used TailwindCSS, a recent utility-first CSS framework, and I really enjoyed it. It makes all your CSS as utility classes to apply in your html. I was really sceptikal when I first heard about it, but now I feel like it was the missing part in my front-end workflow. As an introduction, I recommend this post by Adam Wathan, one of the creator of TailwindCSS.

The main "downside" of TailwindCSS is the output file size. It's mainly because TailwindCSS provides a lot of utilities in its default config like breakpoints (5 screen sizes), colors (10 colors with 7 shades for each), etc. Fortunately, it's easily customizable and the documentation has a dedicated page about controlling file size.

One particular point is the use of PurgeCSS. PurgeCSS is a tool to remove unused CSS from your project. The good news is that we can easily add it to our Laravel Mix workflow as explained in the above page. Better, there is a Laravel Mix extension by Spatie to handle this:

yarn add laravel-mix-purgecss

Then, all you have to do is:

const mix = require('laravel-mix');

require('laravel-mix-purgecss');

// Here are your build task like mix.sass or mix.js

mix.purgeCss();

If you're not using external libraries, then you're done ! On the contrary, you'll probably need a little more config. By default, the plugin will look for classes only in app and resources directories. Which means that all CSS classes used in your node_nodules, will be purged from your output file. To avoid that, you can explicitly tell to PurgeCSS what you want to keep:

mix.purgeCss({
    globs: [
        // Keep all classes found in JS files in 'node_modules/simplemde'
        path.join(__dirname, 'node_modules/simplemde/**/*.js'),
    ],
    // Keep all classes matching these patterns
    whitelistPatterns: [/hljs/, /noty/],
})

On this website, I was able to reduce the file size from 240kb to 16.5kb 💪. You can find my config on Github.

Fonts

As a lot of people, I like to use Google Fonts on my projects. However, there are some easy tips to know when using them.

First, try to use as less fonts as possible, but also as less weights as possible. This will make your import much lighter. I tried to use only regular and bold, and use shades of colors instead to define importance (lighter = less important, darker = more important).

A trick I just learned is that you can import all your fonts at once by using the | operator:

<!-- Before -->
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Satisfy" rel="stylesheet">

<!-- After -->
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700|Satisfy" rel="stylesheet">

By the way, I'm using the Satisfy font only for my logo. I'm thinking about replacing it with a SVG, but I don't know which one is better.

Final tip to boost Google Fonts: the preconnect link. This is very well explained here.

The preconnect link relation type is used to indicate an origin that will be used to fetch required resources. Initiating an early connection, which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allows the user agent to mask the high latency costs of establishing a connection.

Just add this line in your head:

<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>

The preconnect link would also be helpful if you're importing other assets from external domains, like CDNs.

Server-level optimizations

Now, our assets are ready, so let's tune our webserver. I'm using Nginx but same things can be done with Apache.

Enable GZIP compression

First, check if GZIP is enabled, and if not, enable it. On my server it was already set up, but it's easy to do. Just open /etc/ngninx/nginx.conf, look for GZIP section, then uncomment the default settings:

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 application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

As you see, you can explicitly choose which MIME types you want to compress. Images (as music, videos, PDFs, ...) are already compressed so we do not have to GZIPed them. In fact, it could even be worse as you can read here.

Restart Nginx:

sudo service nginx restart

Check it works:

curl -H "Accept-Encoding: gzip" -I https://yourdomain.com/path/to/css

# Response should have the following header
Content-Encoding: gzip

Enable HTTP/2

HTTP/2 was out for a while, is supported by all modern browsers, and is considered to be faster than its previous version. Let's enable it.

HTTP/2 will require your site to use HTTPS with a valid SSL certificate. Enable HTTP/2 is easy in Nginx, you only have to do this in your site's server block:

server {
    listen 443 ssl http2;		
    
    ...
}

If you're unlucky like me, you may encounter some troubles:

  • HTTP/2 is supported on Nginx >= 1.9.5
  • Nginx should be compiled with OpenSSL >= 1.02

These links were helpful:

Cache static assets

Last but not least, let's cache our static assets (CSS, JS, images, ...). Once again, easy with Nginx. Add a location block inside your server block:

# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 30d;
}

This will autorize browser cache for JP(E)G, PNG, GIF, ICO, CSS and JS files for 30 days. Nice !

However, there is one issue if you are using Laravel Mix versionning. This will add a query string to your CSS and JS files. The goal is to invalidate cache in browsers when file changes. The problem with our config is that these files never get cached, exactly because of that query string !

Let's fix this (note the ? at the end of the pattern):

# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js)?$ {
    expires 30d;
}

Of course, you can use different strategies depending on the filetype:

# Cache image for 30 days
location ~* \.(jpg|jpeg|png|gif)?$ {
    expires 30d;
}

# Cache css and js for 7 days
location ~* \.(css|js)?$ {
    expires 7d;
}

Final thoughts

After all these optimizations, I ran another test on WebPageTest. The results were satisfying: all grades to A and a speed index around 800. Even if I got some little issues during some steps, it was definitely worth it.

I think there is still place for improvements, and one future step I'll probably explore is server cache. There are good solutions like Varnish or nginx_fastcgi_cache. Also, I should give a try to spatie/laravel-responsecache. Good topic for another future post !