I've been spending a lot of time optimising the page load performance of my website on both mobile and desktop devices. The bulk of my effort has been guided by the documentation listed by Google's PageSpeed Insights tool. I've documented some of the things that I've learned in the process.
Server
For the Docker container image, my biggest concern were largely to do with CPU usage and memory footprint. I am making use of a all-encompassing CMS framework called SilverStripe which (fortunately) ships with some additional features for caching to either a local filesystem, or to a distributed caching system such as Redis or Memcached. However, it does come with some added bloat, and often requires me to install a lot of additional extensions for PHP in order for it to work correctly.
NGINX
There are a few simple ways that you can overall improve page performance of your website by tweaking a few configuration options that are defined by default in NGINX.
Use GZip Compression Rules
By default NGINX only serves text/html
as with gzip
compression. You'll note that if you navigate to a file of any other type than a simple .html
document, that the response headers won't specify the Accept-Encoding header with gzip
as the corresponding value. This can be tweaked within the scope of a location configuration block.
For instance, within the location block for my SilverStripe installation's NGINX configuration, I've defined a wider range of assets that can be supported for compression.
Refer to the NGINX location block configuration below:
location ~* /assets/.+\.(?<extension>js|css|xml|json)$
{
gzip on;
gzip_static on;
gunzip on;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_min_length 1100;
gzip_buffers 4 8k;
gzip_proxied any;
gzip_types
text/plain
text/xml
text/css
text/comma-separated-values
image/png
image/x-icon
image/webp
image/svg+xml
image/jpeg
image/gif
text/javascript
application/x-javascript
application/javascript
}
The rule does two things:
- Matches the applicable file extensions based on the incoming request, by using the regular expression pattern specified (
/assets/.+\.(?<extension>js|css|xml|json)
). - Depending MIME type recognised, it will then serve the request with the applicable gzip headers, ensuring that the static asset will be served as such.
Use Reverse Proxy Caching
If you are using NGINX as a reverse proxy - which is often the case for PHP-FPM servers - then you might want to consider adding rules for caching content to a local path on your NGINX instance.
Refer to the location configuration block below.
http {
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:10m
inactive=24h max_size=1g;
server {
location / {
proxy_pass http://1.2.3.4;
proxy_set_header Host $host;
proxy_buffering on;
proxy_cache STATIC;
proxy_cache_valid 200 1d;
proxy_cache_use_stale error timeout invalid_header updating
http_500 http_502 http_503 http_504;
}
}
}
Apply Cache Control Headers
I'm working on this section!
Automatically Serving WebP Optimized Image Assets
Depending on how your project is developed, it's possible to write a configuration for NGINX so that it automatically attempts to send the .webp
version of an image to the requesting user, instead of the original image asset itself. The benefit of this approach is that if your server-sided software automatically converts your image assets to .webp
once they are uploaded, or rendered by the server, the optimized version of the asset will be used by NGINX, and the original asset will be used as a fallback should there not be one.
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
location ~* /assets/.+\.(?<extension>jpe?g|png|gif|webp)$ {
...
# more_set_headers 'Content-Type: image/webp';
add_header Vary Accept;
sendfile on;
try_files "${request_uri}${webp_suffix}" $uri =404;
...
}
Which translates to logic that can be otherwise described by this flowchart diagram.
A simple diagram illustrating the logic flow of requesting and retrieving a static image asset with SilverStripe using the NGINX configuration.
The above code snippet is an example of a NGINX "location block" that this website uses for serving publicly visible image assets to the requesting user. It's doing several things here, but most importantly the try_files
statement at the bottom of the location block is attempting to do one task. If the .webp
counterpart for the image that is being requested exists, then serve that back to the requesting user of the website. If it doesn't exist, then simply use the original version of the asset as a fallback.
Providing that you have integrated the NGINX location block correctly with your website, HTTP responses for image assets should be appearing in your developer tools as such.
A screenshot from Google Chrome's Developer Tools displaying a response from a HTTP request for an image asset on the website.
You'll notice that the Content-Type
header is now resolving image/webp
as opposed to the image assets actual MIME type which would typically be image/jpeg
or image/png
.
You might also find this project interesting: SilverStripe Automatic WebP Image Conversion
Website Performance
Improving the performance of your website itself is a significantly more complex subject, as it is of course largely contingent on which tools you use for developing your website with in the first place.
Minify Text-based Static Assets
Most modern Single Page Application frameworks such as Angular already minify and "bundle" JavaScript and CSS static assets, but if you're like me and not using a SPA framework such as Angular, then you will need to seek other tooling. "Minification" quite simply is the process of reducing the amount of unnecessary characters found in most JavaScript and CSS assets. Certain strategies in this approach include, but not limited to...
-
Reducing symbol length - reducing the length or size of syntactically recognized keywords such as variable names, numerical values, and operators (e.g. inline
function
statements converted to lambda declarations). - Removing white-space characters, and condensing all interpreted text - Applicable in both JavaScript or Cascading Style Sheet assets; this ensures there are fewer characters (and therefore bytes) saved in the file, and served to a user's browser.
- Removing duplicates - Removing any potentially duplicate code that might be imported or loaded by libraries with shared dependencies.
- Bundling - combining multiple JavaScript files into a single one so that it reduces the amount of HTTP requests to the server for individual JavaScript assets.
- "Tree-shaking" - Removing "loose" or unused code that is otherwise found in third-party libraries. Typically this is only done in production environments.
Fortunately there's already an abundance of tooling available, and you don't need to waste your spare time on the weekend reinventing the wheel.
Tooling - NodeJS and JavaScript
Much to what I was alluding to previously about SPA frameworks, there's already a significant amount of JavaScript tooling available for minifying and bundling JavaScript and CSS assets. I personally recommend the following libraries, as they have been suitable for my projects...
Tooling - PHP
If you're like me, and you're using reluctantly using PHP because your website's backend is powered by it (ahem, SilverStripe), you will probably be seeking alternatives to what has been mentioned above.
Optimising Image Assets with WebP
As described on the official website, "WebP is a modern image format that provides superior lossless and lossy compression for images on the web". It's an low-memory image format that is widely compatible with most modern browsers and has a significantly reduced on-disk memory footprint with minimal reduction in overall image quality.
Depending on how your website is developed will ultimately determine how best to implement the usage of this image format, but the tooling available is widely compatible with most (popular) operating systems including Linux, OS X, and Windows. Furthermore, the tooling is largely compatible with other existing image formats such as JPEG, PNG, and GIF.
In general, if your website makes use of some form of backend for rendering pages (i.e. Server-Sided Rendering or SSR for short) or for serving an API for data retrieval, you should be attempting to automatically rasterize or convert image assets to WebP image file format where possible.
You can read more about WebP image format here.
SilverStripe and Automatic WebP Image Conversion
SilverStripe doesn't have any official support for the WebP image format, but there are community members that have developed extensions for automatically generating .webp
assets for popular formats including JPEG and PNG when SilverStripe attempts to render images at a reduced size. This largely depends on how you have developed the templates for your SilverStripe website, but arbitrary template statements used for resizing images such as $Image.ResizedImage(200, 300)
and $Image.Fit(300,300)
will trigger SilverStripe to rasterize or resize the image. This behaviour or functionality can be overridden, so that the resulting resized image can be saved or compressed to a WebP image at the same time that it is being rasterized or resized.
Find below a screenshot of how the resulting image asset appears in the filesystem once it has been resized on disk.
A screenshot of how the newly resized or rasterized images appear on disk once they are converted to the .webp image file format.
If you are using the SilverStripe framework for your website, then I strongly recommend that you use a plugin or add-on such as the one that was originally developed by a community member by "nomidi". It's a SilverStripe add-on that seamlessly integrates the ability to automatically generate .webp images if the WebP PHP extension is loaded and available for usage on the server.
If you are using PHP (or SilverStripe), you can make use of this simple PHP code snippet for testing whether the WebP GD image library extension is loaded and available to use.
<?php
if (function_exists('imagewebp'))
{
echo 'webp support available';
}
else
{
echo 'function not found';
}
echo phpinfo();
As it may be possible to infer from the code, it simply checks whether or not the function "imagewebp
" is loaded and available globally.
I made a fork of this add-on that only does one change, and that's to save newly created *.webp
images as *.jpeg.webp
instead of *_webp.jpeg
. The reasoning behind this modification is simply so that it's easier to produce an NGINX configuration that can construct a path to the .webp
version of the image instead of the original. This constructed file path is then of course used for loading and serving the .webp
image asset to the user (if it exists on disk).
You can find my fork for this add-on here.
I've since added support for
- GIF Images
- Silverstripe 4.10
WebP Image Format Compatibility with PHP
In the event that imagewebp
functions are not available for usage with your PHP Docker container, can make use of the following snippet as part of your Dockerfile for building PHP FPM containers. These snippets assume that you are making use of the Alpine Linux variant of the PHP FPM container.
The first snippet ensures the system-wide dependencies for creating .webp
images are available, and that the supporting PHP extension can create .webp
images. The second snippet makes the function listed above available for usage within PHP, so that it can actually access the system-wide tooling for creating and converting assets to .webp
.
Install packages and dependencies of WebP. This is _ only _ applicable if you are using both Docker, and the Alpine Linux image variant of PHP FPM.
RUN apk add --no-cache libwebp-dev libwebp
Next, the GD library has to be configured so that it points to the relevant paths for the PNG, JPEG, FreeType, and WebP system libraries.
RUN docker-php-ext-configure gd \
--with-freetype-dir=/usr/include/ \
--with-jpeg-dir=/usr/include/ \
--with-png-dir=/usr/include/ --with-webp-dir=/usr/include/
Lastly, you will of course want to run your container with the aforementioned modifications, and test that the required modules are loaded and are accessible.
Improving "First Contentful Paint" Loading Speed
In summary, "First Contentful Paint" is the amount of time it takes for any DOM element to be rendered to the screen. This doesn't necessarily mean the entire page must be loaded, rather it simply means that a single element has begun rendering, and that the browser has acquired the all required assets to render the page with. In order for the browser to begin rendering, it must first download all required assets. These are typically defined in the <head>
element of the page as <script>
and <link>
statements. Unless defined otherwise, these statements or HTML elements are considered to be "blocking", therefore meaning that the rest of the page cannot be loaded, or rendered to the screen until the assets defined in those tags have been downloaded by the browser. As you might imagine, this can potentially significantly impact the "First Contentful Paint" (FCP for short) speed.
Structured Usage of Async and Defer Statements
In some instances it could be the case that not all script or CSS assets need to be loaded in order for the page to be rendered to the user. Instead, they could be deferred or done asynchronously while the remainder of the website or page is being rendered or displayed to the user.
Lazy Load Blocking Resources
Static assets such as images, cascading style sheets and JavaScript files are considered to be blocking resources when they are loaded by default. Where possible, it's important to ensure that these resources are loaded after text content and other DOM elements have. There are a number of methods that you can adopt for reducing the time it takes for the "First Contentful" Paint (FCP) to complete, depending on the type of static assets you are optimizing. The idea of "lazy loading" is simple. Only load static, blocking assets when they are required such as when the window has focus, or if the DOM element is made visible to the user.
Images
Most images on a page can be "lazy loaded", particularly ones that are being rendered as part of a home page carousel (i.e. revolving gallery of images). For instance, not every image in a carousel is displayed to the user at once (this is by design), so therefore they don't need to be loaded immediately. However, by default, a web-browser will automatically load all images regardless of whether they are considered visible (i.e. display: none;
or display: block;
)
Fortunately there is a convenient way of achieving this kind of asynchronous loading behaviour for images, by using a third-party JavaScript library named "lazysizes".
It can be loaded as part of your website by simply declaring it as part of your header as such.
<head>
<script src="lazysizes.min.js" async="true"></script>
</head>
Integrating it with existing templates and images is as simple as doing the following.
<!-- responsive example with automatic sizes calculation: -->
<img
data-sizes="auto"
data-src="image2.jpg"
data-srcset="image1.jpg 300w,
image2.jpg 600w,
image3.jpg 900w" class="lazyload" />
The original image asset might look like this.
<!-- responsive example with automatic sizes calculation: -->
<img src="image2.jpg" class="lazyload" />
Fonts, Stylesheets, and JavaScript
No additional third-party library is required for loading static assets that are fonts, CSS stylesheets or JS assets. Instead, depending on the kind of behaviour you are seeking, you can use one of the following specifiers as part of <link/>
or <script/>
statements when loading static assets.
Google's Third-party Libraries
Lazily loading third-party libraries such as the ones that are used by Google Analytics engine, and Google AdSense are a little bit more tricky. It should be noted that in doing this, it can incur edge-case behaviour should you continue to use and rely on the data that is recorded in Google Analytics.
Google AdSense
Observe the following code snippet.
<script type="text/javascript">
window.addEventListener('load', function() {
var is_adsense_load = 0
window.addEventListener('scroll', function() {
if (is_adsense_load == 0) {
is_adsense_load = 1;
var ele = document.createElement('script');
ele.async = true;
ele.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'
var sc = document.getElementsByTagName('script')[0]
sc.parentNode.insertBefore(ele, sc);
//google_ad_client: "ca-pub-xxxxxxxxx",
(adsbygoogle = window.adsbygoogle || []).push({
google_ad_client: "$AdSenseID",
enable_page_level_ads: true
});
}
})
})
</script>
Typically such a code snippet would be placed in the
section of your page. If you look closely enough, you'll note that this script is in fact just a revised version of the one that Google asks you to integrate as part of your pages, but it ensures that the script is only loaded after the JavaScript event call back is fired when the window has loaded.It constructs the <script>
element manually, and appends it to the page's Document Object Model (DOM). This code will only get invoked once the page has loaded (and therefore invokes the callback).
This works, but the caveat with this is that it may affect the accuracy of your statistics, as it will likely discount users who are only visiting your page for less than a couple of seconds. However, you may also not consider this to be of importance, so this would be a significant improvement if this is the case.
Tools for Testing Performance
Once you have made the necessary modifications for optimizing your website for improved load times, you can make use of some of the following websites for testing page load times, and for identifying any other errors.
Miscellaneous Links
You may also find these other links useful.
Top comments (0)