How to boost performance and technical SEO just by tuning NGINX

Stjepan Grego
Undabot
Published in
15 min readJun 9, 2020

After implementing some of these suggestions, you should easily get a perfect score on the Google Page Speed Test. It also shouldn’t take you too much time!

I’ll try to explain some basics of NGINX and how to leverage its features to get the maximum performance out of your apps and websites. Most web developers don’t use NGINX to optimize their websites or applications. I get it, in most cases, and companies, that should be DevOps responsibility. The problem is, most of the NGINX features and optimizations that I’ll mention in this article are closely related to your website’s code. These features may even harm performance if your website is not built to support them. That’s why I think it’s important for developers to at least understand these technologies and terms, so they can implement them, or change the app to work better on the existing server configuration.

I won’t go too deep into some of NGINX features since it’s not possible to fit all of that into one blog post. I will try to explain some basics and show you some of the things I use to boost performances. I will link some additional resources at the end of the article if you need some reminders, or if you want to learn more. To get the most out of this article you should have at least some understanding of Linux and web servers.

Things that I’ll cover in order:

  1. NGINX — Basics and setup
  2. SSL Certificate & HTTPS
  3. HTTP/2
  4. HTTP/2 Push
  5. Client-Side Caching
  6. Compression
  7. Pagespeed Module
  8. NJS

1. Nginx — Basics and setup

Before implementing any of the suggestions you should know how to install the NGINX and at least some of its main principles. I’ll cover these parts in short because there are many resources out there that go into more detail. NGINX has a pretty good documentation too. Also, all installing tips are strictly for Ubuntu 18.04, but they will probably work on some other versions of Ubuntu. For most people, these steps should be enough. Sometimes you need to do additional steps and setups depending on your server. If you are a beginner, please do not do this on the existing servers with apps that are in production. There is a big probability that something will go wrong when trying these things for the first time. :)

Firstly install the NGINX on your Linux machine using the commands below:

$ sudo apt update
$ sudo apt install nginx

After the installation process is finished the NGINX will start to handle all the HTTP traffic. You can test this by typing the server IP inside the browser URL bar, and you should see the default NGINX page. If everything is OK we are ready to add a new server block (virtual host) for our app. A virtual host is an Apache term. NGINX does not have Virtual hosts, it has “server blocks”.
The recommended approach is to have a separate server block configuration file for each of your apps. The NGINX server blocks are located in two folders:

  1. /etc/nginx/sites-available which contains all configurations
  2. /etc/nginx/sites-enabled which contains enabled configurations

Firstly we will create config file inside sites-available and then symlink it to the sites-enabled folder.

Go to /etc/nginx/sites-available and create a new config file:

vim example.conf

Now I’ll explain what are all directives in the basic configuration file, and then we will build upon it.

A simple NGINX server block configuration:

listen and server_name directives will be used by the NGINX to determine which of the server blocks will serve the request. listen defines the port, which is, in our case, port 80. By default, the HTTP goes to port 80 and the HTTPS goes to port 443. server_name defines the host name or IP that it should match.
If you open your URL in the browser e.g. http://example.com, the NGINX will match port 80 since it’s an HTTP, and it will check for “Host” request header, which should be example.com. Based on that the NGINX will assume that this server block configuration should handle the request.

The root directive specifies the root directory that will be used to search for a file. To obtain the path of a requested file, the NGINX appends the request URI (path or the portion of the URL after the domain) to the path specified by the root directive. The directive can be placed on any level within the http, server, or locationcontexts. In the example above, the root directive is defined for a server context. It applies to all locationblocks where the root directive is not included.

loacation blocks will be checked against the request URI. The NGINX optional modifiers and Regular expressions or literal strings can be used to define location match. In the above case, the location will match all the requests. For now, I will use the most simple location match, a more complex example would be if you want to serve images from a folder that is not within the serverroot. In that case, you can write another location block with a regular expression that matches the image file extensions:

When you add s root directive under location, it will override the server root directive for all requests that match the regex. I’ll cover a few more location use cases later in the article.

The try_files within location basically tells the NGINX which file should be served after it matches the location. In our case, we first try to load the file as it is in requested URI, and if that file doesn't exist, we serve the index.html from the root directory.

The config below should be enough to get you started. Just remember to change it with your domain and root folder.

Now we need to symlink saved configuration to sites-enabled folder.
We do so with the command below:

ln -s /etc/nginx/sites-available/example.conf /etc/nginx/sites-enabled

This allows you to selectively disable/enable server block configurations by adding/removing the symlink. The NGINX will only load config files that are located within sites-enabled folder.

Now we can test the configuration with a simple command:

$ sudo nginx -t

The NGINX will tell you if something is not right. If you get “test successful message” then you need to reload the NGINX so it picks up the new config file:

$ sudo service nginx reload

Remember to always use the service nginx reload instead of service nginx restart . This is important because reloading will keep the old processes running if it encounters the syntax error in the configuration. Restart, on the other hand, will kill all NGINX processes and your server blocks will stop working until you fix all of the errors, so all of your apps that are running on that instance of the NGINX will go offline. Restart is necessary for some rare cases e.g. when you change the listen directive, so keep that in mind as well.

If you did everything right, you should see your website when you type your URL in the browser: http://example.com
Remember to replace example.com with your domain name, and also check that your domain is pointing to the correct server. The NGINX will look for the index.html file within the root folder, so make sure you have one. Now let’s jump into the more interesting part.

2. SSL Certificate & HTTPS

One of the things that every website needs these days is an SSL certificate and an HTTPS. Today the SSL certificates are free, thanks to Let’s Encrypt, a nonprofit Certificate Authority providing TLS certificates to 200 million websites at this moment. They simplify the process by providing a Certbot software client that automates most of the required steps for the NGINX.

So let’s firstly install Certbot for the NGINX and Linux:

$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx

After installing the Certbot, just run the command below for a domain that is pointing to your server.

$ sudo certbot --nginx -d example.com

Follow the instructions and, when asked, “Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.”, select 2: Redirect.
After that, the Certbot will alter your server block config and redirect all traffic to HTTPS. Open your config file now and you should see this:

You can see that your initial server block is changed and that new server block is created. Your initial server block now listens to 443 port, which is an HTTPS, and the newly created block only redirects the HTTP traffic to the HTTPS.

The Certbot also takes care of renewing certificates. By default, they are valid only for 90 days and the Certbot will renew it when it’s within thirty days of expiration.
We can verify if renewal works with the next command:

$ sudo certbot renew --dry-run

If you see no errors, automatic renewal should work.

Having an SSL certificate and an HTTPS is also important for the next section, because the HTTP/2 doesn’t work on an insecure HTTP.

3. HTTP/2

HTTP/2 is currently supported by nearly 96% of all web browsers in use. If you don’t have an older project that was fully optimized to get the best out of HTTP/1, and you don’t need to support older versions of Internet Explorer and Opera Mini, there is no reason not to use HTTP/2.

Few HTTP/1 optimizations are considered anti-patterns for the HTTP/2 environments. Most of these are related to concatenation. Concatenation is the process of combining files in order to reduce the number of HTTP requests which do not benefit the HTTP/2. Concatenation examples are sprite images, CSS/JavaScript bundling and inlining. Sprite images and inlining can also reduce the benefits of client caching — if you end up updating just one of your images, the whole sprite has to be requested again, instead of requesting only the updated image and keeping others in the cache.

Enabling basic HTTP/2 functionalities in the NGINX is pretty straight forward, you just need to modify listen directive to:

listen 443 ssl http2;

I tried a few performance tests with many images/static resources and the HTTP/2 was 2x faster on 4G and up to 3x on 3G network. Just by adding the http2 label to your listen directive, you can get an amazing performance upgrade, but be careful and test every change you do, since it all depends on your website or application.

4. HTTP/2 Push

The HTTP/2 Server Push is another useful feature that is a bit harder to implement. The server push allows a server to pre-load static resources by anticipating the user requests. In the NGINX we can do this again with location block:

location = /index.html {
http2_push /style.css;
http2_push /image.jpg;
}

This instructs the NGINX to send style.css and image.jpg whenever it gets a request for index.html. The http2_push directive takes one parameter, the full URI path of the file, to push to the client.
The HTTP2 Push is not really useful if you need to have these settings and files hardcoded on the server. It’s also not something I would recommend. There are two workarounds for this. The first one is to set Link headers within your Node.js or other application. The NGINX will automatically push resources to clients if proxied applications include an HTTP response header Link. This header will instruct the NGINX to preload the specified resources. To enable this feature, add http2_push_preload on; to the NGINX configuration:

Another approach is to have your NGINX configuration inside the GIT and Docker. I may cover that part in the next blog article.

5. Client-Side Caching

With the NGINX we can set response headers to tell the client (browser) that it can cache specific content for a specific amount of time. This can improve performance a lot, since the browser won’t make requests to the server for files that are cached. This is how we do it in the NGINX:

location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control “public”;
}

In this example, we have location regex that should match all images, JavaScript, and CSS files. The expires directive tells the client that their cached resource will no longer be valid after 30 days. The add_header directive adds the HTTP header Cache-Control to the response, with a value of public, which allows any caching server along the way to cache the resource. If we set it to private, only the browser is allowed to cache the value.

6. Compression

Compression is a simple, effective way to save the bandwidth and speed up your site. Compressing responses often significantly reduces the size of the transmitted data. However, since the compression happens at runtime, it can also add considerable processing overhead, which can negatively affect performance. The NGINX performs compression before sending responses to the clients, but does not “double compress” responses that are already compressed (for example, by a proxied server).

I’ll show you how to enable the two most popular compression algorithms — Gzip and Brotli. Gzip comes as a default module for the NGINX, and Brotli doesn’t. You can follow install instructions on the official ngx_brotli module Github. If you don’t want to bother, just remove that part from the configuration.

Brotli has impressive gains of up to 25% over Gzip compression. It’s developed by Google and most of the browsers these days support it. It’s also good practice to enable both (GZIP and Brotli), so that older browsers still get compressed content. This is a configuration with Gzip and Brotli enabled:

brotli_types& gzip_types directives enable on-the-fly compression of responses for the specified MIME types, in addition to text/html , which are always compressed. It’s not recommended to enable compression for image files, nor is for videos, PDFs and other binary formats. Using compression on them won’t provide any additional benefit and can actually make them larger. In the next section, I’ll show you how to compress and optimize images by using the Pagespeed module.

brotli_comp_level & gzip_comp_level directives are used to specify the level of compression. They simply determine how compressed the data is on a scale from 1–9 for GZIP, and from 1–12 for Brotli, where 9 & 12 are the most compressed. The recommended level is from 4 to 6, but it all depends on your website.

gzip_vary: on basically just adds header Vary: Accept-Encoding. It’s there to prevent client cache mix-ups between compressed and uncompressed files. If for some reason the client has an uncompressed version of the file in its cache, it will know not to request a compressed version of it again and instead to just use the uncompressed file from the cache. In the end, you are also preventing serving uncompressed version to a user that supports gzip, and vice versa.

7. PageSpeed Module

It is an open-source NGINX module that optimizes your site automatically and not many people know about it. It’s made by Google and it implements most of the Google page speed recommendations. It can boost performance and SEO dramatically. I’ve abused this module many times, and I will keep doing so! When you need to quickly improve the performances of a website, just set it up in a few hours and you should get great improvements! It’s hard to believe how easy it can be to turn super slow websites into super performant just by using this module.

The setup process is not that simple, but it’s worth it when you see how much value it adds. You can follow these instructions on how to install it, and I’ll give you a few suggestions on how to configure it.

This is a server block config with some PageSpeed configuration options:

It has a lot of configuration options and you should tune these for each website separately. Your frontend stack may already be doing some of these optimizations in place and it can be counterintuitive if you don’t sync it all together. If you are using the HTTP2, you should turn off all options for concatenation, or at least break files into smaller chunks. You should do that in the NGINX and, for example, Webpack.
I’ll try to explain a few PageSpeed directives I used above, but you should really check official documentation and see which options will be best for your website.

pagespeed RewriteLevel PassThrough directive disables RewriteLevel CoreFilters . What it means is that you have to manually enable each filter that you want to enable. It offers three rewrite levels to simplify configuration: PassThrough, CoreFilters, and OptimizeForBandwidth.
The CoreFilters set contains filters that the PageSpeed team believes are safe for most web sites. By using the CoreFilters set, as PageSpeed is updated with new filters, your site will get faster. The OptimizeForBandwidth setting provides a stronger guarantee of safety and is suitable as a default setting for use with sites that are not aware of PageSpeed. I personally prefer to manually set each filter and test it afterward. You can also try to use CoreFilters and OptimizeForBandwidth and see if it works better for you.

pagespeed EnableFilters recompress_images is a filter group consisting of convert_gif_to_png, convert_jpeg_to_progressive, convert_jpeg_to_webp, convert_png_to_jpeg, jpeg_subsampling, recompress_jpeg, recompress_png, recompress_webp, strip_image_color_profile, and strip_image_meta_data directives. You can check the documentation for each of these directives. Basically it will optimize all hosted images and serve the best possible format depending on browser support. For example, you can upload an unoptimized image in jpeg format, and if the client requests that image using Google Chrome, PageSpeed will serve it optimized in webp format since Chrome supports it. It will probably be 10x smaller than the original.

pagespeed FileCachePath tells Pagespeed which folder should be used for cache that it generates. Pagespeed will cache all of the static resources if you set it to do so.
The extend_cache filter rewrites the URL references in the HTML page to include a hash of the resource content (if rewrite_css is enabled, then image URLs in CSS will also be rewritten). Thus, if the site owners change the resource content, the URL for the rewritten resource will also change. The old content in the user's browser cache will not be referenced again because it will not match the new name. If the site owners change the logo, then PageSpeed will notice it within 5 minutes and begin serving a different URL to the users. But if the content does not change, then the hash will not change, and the copy in each user’s browser will still be valid and reachable.

It’s important to note that every time you change a Pagespeed setting, you have to reload your website a few times to see changes in the HTML code. Pagespeed needs some time to rewrite all resources and save them in the cache.

What I find most useful with Pagespeed are image compression and caching. I mainly use these filters, but this depends a lot on your app and other NGINX configuration settings.

You can also enable NGINX statistics and logging admin UI. Just add directives below and you should be able to access it via URI you set in GlobalAdminPath
Just be careful and disable this in production, since you don’t want people or bots to access this Admin panel. That is why I didn’t include it in the configuration above.

## ADMIN
pagespeed Statistics on;
pagespeed StatisticsLogging on;
pagespeed GlobalAdminPath /pagespeed_admin;

Here is the official documentation where you can see all the other filters & configuration options. At the bottom of the documentation, for each option there is a Risk section and it can be low, moderate, or high. Check this for every config and avoid all high-risk options. Try and test all configuration options to see which one works well with your website. I can’t stress enough how important it is to test every change you do inside the Pagespeed module configuration. Not every setting will work well for every app and environment, so be careful and test, test, test.

9. NJS scripting language

This part isn’t really well known, even within the DevOps community, and it will be most interesting to JavaScript developers. The NGINX team decided to add additional scripting options for their configuration files. They did so by writing their own interpreter in JavaScript that provides objects, methods and properties for extending the NGINX functionality.

Some of the reasons why they have chosen JavaScript instead of other languages:

  • JavaScript has a C-like syntax, which is very similar to the NGINX config files
  • JavaScript uses curly braces for different blocks, which is also the case with the NGINX.
  • JavaScript is event-driven, same as NGINX

Some of the use cases:

  • Complex access control and security checks in the NJS before a request reaches an upstream server
  • Manipulating response headers
  • Writing flexible asynchronous content handlers and filters

Simple Hello World example with the NJS:

# nginx.conf exampleevents {}http {
js_import http.js;
js_content http.hello;
}
# http.js examplefunction hello(r) {
r.return(200, “Hello world!”);
}
export default {hello};

You can find more examples and use cases on this link. One use case that I’m planning to work on is for the Dynamic HTTP2 Server Push. I’ll write another blog article just on that, when I properly test the solution.

It’s important to note that it doesn’t support most of the ES6 specifications and JavaScript modules/3rd-party code. The NGINX team says that this is mainly because of the performance e.g. ECMAScript specs require UTF-16 encoding and the NJS uses UTF-8 which needs 2x fewer bytes for any data chunk. That shows how much they actually care about the performance. If you ever notice that you lack some of the NGINX functionality, it’s worth checking the NJS.

To sum up

The goal of this article was to show an alternative approach to website optimization that’s not used as widely. The key with the NGINX is that most of the suggested optimizations can be implemented without changing a single line of website code. That can be really useful sometimes, if you need a centralized solution, or if you don’t have access to the app code. Some people may prefer some other tools and technologies for implementing the same optimizations and that’s perfectly OK.

I’m suggesting you read the official documentation for all terms that you don’t understand perfectly, and, one more time, it’ really important to test every change you make. Below you can see useful resources and documentation for covered topics:

Thank you for reading. Please comment, clap, or share it with your friends! Also, big thanks to Ana Šeler for the design!

--

--