Optimization

Our clients love to have a look at PageSpeed Insights and tell us that the website is too slow.

And often, they’re right. But PageSpeed Insight’s advice is very generic and poorly explained. So we need to get a high score and not follow their advice? How exactly?

Core Web Vitals

Well, specifically we need to have Core Web Vitals. Because that’s what is impacting the SEO. Many other things can be optimized and probably need to be but those Web Vitals are not too poorly designed and will hint strongly at weak points we might have.

What are they?

  • Largest Contentful Paint (LCP) — How long does it take to have the largest paint done, aka how fast the content above the fold is displayed. By example if you have a huge image loading on the first carousel of the page this might impact that metric very strongly. Recommended value: 2.5s or less.

  • First Input Delay (FID) — How long is the CPU so busy parsing your JavaScript before the user can actually use the page. Recommended value: 100ms or less.

  • Cumulative Layout Shift (CLS) — How many images or pieces of content appeared after the page load and triggered layout re-computation (which is a fairly expensive operation). Recommended value: 0.1 or less.

Now that we’ve got the goal in mind, let’s see the different things that we can do to avoid performance issues.

Front-end best practices

Those are guidelines to know what practices you can use in priority to have the greatest impact on the website’s performance.

Optimize images

This affects LCP

It’s 2021 and it’s the year of using WebP. Most browsers support it, it’s more efficient than JPEG and PNG combined and also manages transparency. Basically, it’s perfect.

Now, most browsers support it. We still need to support older legacy browsers like Safari.

The basic strategy is:

  • All images should be available in WebP

  • But should have a fallback as PNG

This is done by using the <picture> tag with several sources instead of using <img>. It doesn’t mean that you have to be responsive but it’s highly advised to be.

Using the <picture> tag is not possible when doing a CSS background, so please refrain from using the background-size: contain technique. Instead you can use the more modern object-fit property. Of course some browsers don’t support object-fit so depending on what is your compatibility target you can have a fallback that uses the background technique and serves the PNG image but this must only be activated when object-fit doesn’t work.

It is also very important to note that users are not expected to upload optimized or properly-sized images. They can upload whatever they damn want. It’s the job of the website to make the best out of what was uploaded, including:

  • Resizing the image to a decent size

  • Converting it to WebP and PNG

  • Cropping it smartly (by example using Wagtail’s focal point feature)

In that matter, I recommend you have a deep read of Wagtail’s images documentation.

As a shortcut for certain use cases, there is image template tags available in Wools. Don’t use it blindly though.

Minimize the amount of JS to parse

This affects LCP and FID

By far the most CPU-intensive operation of displaying a page is parsing and compiling the JS code. It’s not even running it, its mere existence is enough to clog the main thread.

What can you do about it?

  • Minimize the amount of dependencies you use. Always prefer write a small, specific bit of code rather than importing a huge do-it-all dependency that will weight a ton. Once in a while, you can analyze your Webpack bundles to know if you’ve embedded something too big in your project. Don’t wait for the end of the project to do it, it will be too late!

  • Produce different smaller chunks and serve them separately. The browser will have several small files to parse instead of a big one. This gives it the opportunity to handle some events in between instead of being blocked for a long time. If you’re using Nuxt, this is done automatically.

Define all image/object sizes

This affects CLS

You need to specify the sizes of all the items present on your page so that the browser doesn’t need to load them to know their size.

This mostly applies to images. Always specify a width/height to image elements, or at least specify the size in CSS. Otherwise the browser will have to download the image in order to know what is the size of the file and then compute how big this image is going to be on the page.

Every time this kind of computation happens, the browser needs to re-compute the whole layout, and that’s fairly heavy for the CPU.

Cache content

This affects LCP

If your website is, by example, powered by Wagtail, then it might be that with the gazillion StreamFields you’ve had to put in there the generation time is not completely optimal. Could be around 1s to generate the whole page by example. This is a lot of time. Fortunately, since 100% or 99% of the page is the same for two visitors, the easiest way to deal with that is just to cache the whole page and serve the same thing from cache every time.

If you’ve got specific modifications to do, like display the user’s name on the corner of the page, this is often better done on the client-side using Nuxt. If your website is not a headless Wagtail chances are that the content is 100% static.

Inlining CSS

This affects LCP

The CSS from the first page is best inlined rather than loaded separately: you need it to display the page no matter what.

If you’re using Nuxt it’s done automatically. If not you’re probably going to need to work a little bit.

Bad ideas

Here are a few recommendations from PageSpeed insight that are probably counter-productive and should be avoided. Most of the performance issue should be resolved by the advise from above. Please avoid all PageSpeed advice until you’re sure that you have completely covered the points advised in this page.

Lazy-load images

PageSpeed says “Defer offscreen images” but it’s easy to misunderstand it and think “ok let’s lazy-load all images”.

Here is the thing: loading images is not that heavy. It’s actually very easy for the browser. The only impact it has is on the bandwidth of the user but if your goal is to load those images anyways it doesn’t change much.

What is heavy is preparing a frame for an image, noting down to load it later, wait for a bunch of stuff to load and then load the image.

If you lazy-load something that is above the fold you are going to completely kill the LCP metric of your page.

So, given the complexity of lazy-loading and the fact that you will most likely just end up degrading performance, it is not recommended to lazy-load images at all.

Enable text compression

Google will advise you to gzip your HTML code when serving it to the browser. This is a particularly bad idea because with the current state of things you need to choose:

  • You can encrypt things

  • Or you can compress things

But if you encrypt things that are compressed then you have a security issue. As it happens, there is a high chance of having some kind of security token embedded in the HTML code (like a CSRF token).

So, given the little added-benefit of compressing HTML output, it is not advised to do it. At least not for pure Django projects.

Back-end best practices

On the back-end, you need to be aware that your bottleneck is your database. To parody, you can only write one thing at a time and you also can’t read things that are being written.

Use bulk inserts/upserts

If you need to insert several rows at the same time, make sure to use Django’s bulk_create.

Note

You can’t use it if your model uses table inheritance. So let’s stay away from table inheritance :)

Pro-tip: if you want to use inheritance to avoid repeating field names but you don’t actually want the data to be inherited, you can use abstract = True in your parent model’s Meta.

If you need to insert OR update if already present, then we’re talking about upserts. This is made possible in different flavors by django-postgres-extra.

As a rule of thumb, you can insert/upsert 1000 rows at a time. Above this number the performance gains are negligible. But if you bulk_create 1000 rows the performance will be about 100x superior to what you’d get by doing 1000 separate create() calls. This is a very, very strong optimization.

Avoid locking

If you know that you’re going to have massive amounts of data, you’ll need to think the data model and the way your code works so that you can naturally update the database with locking it as few as possible.

This is the condition to meet if you want your application to be working concurrently on several processes and thus that it is able to scale it easily.

PostgreSQL optimizations

You’ll understand this from what was said above, it is recommended to handle as much data fetching and transformation with SQL. If you run a query and you see that Python CPU is 80% while PostgreSQL CPU is 20% then you probably need to offload a lot of the processing to PostgreSQL.

This is because PostgreSQL has all the data it needs at hands and is able to process it much faster than Python would be. After all it’s coded in C, while Python is still largely interpreted. And on top of that, all the data is on the server so it doesn’t require to go through the network before being processed.

In some cases however, a big SQL query can be slowing down the execution of a page. Worst, if the query is too complex it’s possible that the performance looks fine at first but gradually becomes disastrous.

Fortunately, there is a lot of buttons to push in PostgreSQL that help you reach the most optimal possible query execution. Some are obvious (indices on foreign keys that are being created automatically anyways) while some are not (the size of specific memory buffers, etc).

To help you find that, DB engines in general and PostgreSQL specifically have a feature named EXPLAIN. It will run the query and instead of giving you the results of that query it will give you the list of operations that have been done and the time spent on each of them.

The output of this command is very dense and hard to read, however there is other tool that will help you in this task and give you advices. PgMustard is one of such tools and it is highly recommended that you use to help understanding complex queries. We have a license, ask Rémy for it.