egregius.be

Urban Exploration, PHP and others…

Optimizing web performance

Optimizing web performance for WordPress and general websites.

Apache2 vs Nginx

Some time ago a question was asked on the Domoticz forum why I used Apache2 instead of Nginx. The answer was quite simple, just because I always used Apache2 and therefor didn’t know Nginx. Time for some investigation…

I first started at my home server, the one that servers my Pass2PHP and floorplan for Domoticz. The setup and installation was straight forward and everything went really smooth. Together with the replacement of curl by wget in the LUA script I saw a improvement of 30%. Response times where now at 10 msec where they were before 15msec.

After that first positive impression of Nginx I also changed the webserver on my VPS. That’s the one that hosts this site and some others like minja.be, y013.be and urbexforum.org.
Again, the installation went smooth and easy. Of course I was still running a basic Nginx setup but I already noticed speed improvements.

After that second good impression I couldn’t resist on changing the webserver at work also. That one hosts, at this moment, about 70 websites. Used by approx. 400000 people. This one would benefit the most of the change.

Because the change in performance between the two is not that easy to measure I will not even try to measure it and say anything about it. It comes most down to the ability of Nginx to handle much more with the same hardware.

Anyway, time for the second part of the optimization: WordPress.

WordPress plugins

Before the optimization, the frontpage of this site needed 56 requests to open. That’s because the Pen theme uses quite some js and css files. There were 2,3MB resources need resulting in a finish at 1;24 sec.

Bore optimizing

After the optimizing, the same page only needed 15 requests and 1,5MB resources. That’s 35% less resources but that finish time, waw, 65% faster, Amazing! That’s a huge performance gain.

How did I achieve that?
Well, I first installed 2 plugins in WordPress: WP Super Cache and Autoptimize.

WP Super Cache

WP Super Cache creates a great cache and has just enough options without being to difficult.
These are my settings:
The biggest change is the cache location. I don’t want it in my www folder to avoid it being in the backup.

WP Super Cache

WP Super Cache also supports CDN. For this I did a small trick.
I already had the cdn.egregius.be subdomain of a previous project, so why not use that. The trick was to create a symlink in the cdn home folder to the WordPress root. Biggest advantage of using a CDN is that the browser can use more concurrent connections. A browser is limited to a small amount of connections. If a page need more requests than this limit the requests are queued. With a CDN you double the possible concurrent connections and therefor half the queue.

I excluded css files because they give errors in the Pen theme.

Autoptimize

Autoptimize is even more simple to set up. Just install the plugin, activate the options to optimize javascript code, aggregate js file, optimize css code, aggregate css files and optimize html code. I don’t use any of the other options.
This plugin limits the number of request needed by aggregating the different small files in bigger ones. Of course, things like this can break your site. Therefor always test enough to be sure. For example the minimatica theme at minja.be stopped working until I activated the Add try-catch wrapping option.

Webp images

Another great enhancement I did was converting all images to WebP images. WebP becomes the web default. It’s a lot newer format than JPG and PNG and compresses images a lot better without quality loss.
I first tried several WordPress plugins but none of them worked as I expected. Time to search the www for another solution.

Converting images to WebP

On Matt Gadient’s site I found a lot of interesting information for this. In short it’s about a script to convert JPG and PNG files to webp, storing them in the same location and just adding .webp to the extension.
The original script of Matt had some issues on my server. Sometimes an empty file was created, or a WebP image that was larger than the original. I tweaked the script to avoid these issues. Very small files are skipped. The touch -r to set the timestamp the same is only executed if the file is actually created and good. In case the converted image is bigger than the original it’s removed again.

This script will convert all JPG and PNG files in any folder under /var/www. The new files are named file.jpg.webp and the originals are kept in place. The originals need to stay to be server to old browsers that can’t read WebP images or the WebP image isn’t available yet. See more on that later.

#!/bin/bash
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
process() {
	DEBUG=1
	SLEEP_DELAY=0.1
        FILE="$1"
	STARTTIME=$(date +%s)
	CREATED=0
        if [ -f "$FILE".webp ]
        then
                FILE_ORIG=$(stat -c %Y "$FILE")
                FILE_GZIP=$(stat -c %Y "$FILE".webp)
                ORIG_SIZE=$(stat -c %s "$FILE")
                GZIP_SIZE=$(stat -c %s "$FILE".webp)
                if [ $ORIG_SIZE -gt 5000 ]
                then
			if [ $FILE_ORIG -gt $FILE_GZIP ]
			then
				rm "$FILE".webp
				EXTENSION=$(echo "$FILE" | awk -F . '{print $NF}' | awk '{print tolower($0)}')
				if [ "$EXTENSION" == "png" ]
				then
					cwebp -preset drawing -q 80 -m 6 -mt -short -sharp_yuv "$FILE" -o "$FILE".webp
					if [ $? -eq 0 ]
					then
						CREATED=1
						if [ "$DEBUG" == 1 ]
						then
							echo "Replaced old PNG WebP with: $FILE.webp"
						fi
					fi
				elif [ "$EXTENSION" == "jpg" ]
				then
					cwebp -preset photo -q 80 -m 6 -mt -short -sharp_yuv "$FILE" -o "$FILE".webp
					if [ $? -eq 0 ]
					then
						CREATED=1
						if [ "$DEBUG" == 1 ]
						then
							echo "Replaced old JPG WebP with: $FILE.webp"
						fi
					fi
				fi
				if [ $CREATED -eq 1 ]
				then
					GZIP_SIZE=$(stat -c %s "$FILE".webp)
					if [  $GZIP_SIZE -gt $ORIG_SIZE ]
					then
						echo "Removing larger WebP: $FILE.webp"
						rm "$FILE".webp
					elif [  $GZIP_SIZE -eq 0 ]
					then
						echo "Removing empty WebP: $FILE.webp"
						rm "$FILE".webp
					else
						touch "$FILE".webp -r "$FILE"
					fi
					ENDTIME=$(date +%s)
					WAITTIME=$((($ENDTIME-$STARTTIME)*1))
					if [ $WAITTIME -gt 1 ]
					then
						echo "Sleeping $WAITTIME..."
						sleep $WAITTIME
					fi
				fi
			elif [  $GZIP_SIZE -gt $ORIG_SIZE ]
			then
				echo "Removing larger WebP: $FILE.webp"
				rm "$FILE".webp
			elif [  $GZIP_SIZE -eq 0 ]
			then
				echo "Removing empty WebP: $FILE.webp"
				rm "$FILE".webp
			fi
		
			sleep $SLEEP_DELAY
		fi
	else
		ORIG_SIZE=$(stat -c %s "$FILE")
		if [ $ORIG_SIZE -gt 5000 ]
                then
			EXTENSION=$(echo "$FILE" | awk -F . '{print $NF}' | awk '{print tolower($0)}')
			if [ "$EXTENSION" == "png" ]
			then
				cwebp -preset drawing -q 80 -m 6 -mt -short -sharp_yuv "$FILE" -o "$FILE".webp
				if [ $? -eq 0 ]
				then
					CREATED=1
					if [ "$DEBUG" == 1 ]
					then
						echo "Created new PNG WebP at: $FILE.webp"
					fi
				fi
			elif [ "$EXTENSION" == "jpg" ]
			then
				cwebp -preset photo -q 80 -m 6 -mt -short -sharp_yuv "$FILE" -o "$FILE".webp
				if [ $? -eq 0 ]
				then
					CREATED=1
					if [ "$DEBUG" == 1 ]
					then
						echo "Created new JPG WebP at: $FILE.webp"
					fi
				fi
			fi
			if [ $CREATED -eq 1 ]
			then
				GZIP_SIZE=$(stat -c %s "$FILE".webp)
				if [  $GZIP_SIZE -gt $ORIG_SIZE ]
				then
					echo "Removing larger WebP: $FILE.webp"
					rm "$FILE".webp
				elif [  $GZIP_SIZE -eq 0 ]
				then
					echo "Removing empty WebP: $FILE.webp"
					rm "$FILE".webp
				else
					touch "$FILE".webp -r "$FILE"
				fi
				ENDTIME=$(date +%s)
				WAITTIME=$((($ENDTIME-$STARTTIME)*1))
				if [ $WAITTIME -gt 1 ]
				then
					echo "Sleeping $WAITTIME..."
					sleep $WAITTIME
				fi
				sleep $SLEEP_DELAY
			fi
		fi
	fi
}
export -f process

find /var/www -type f -regextype posix-extended -regex '.*\.('"jpg|JPG|png|PNG"')' -exec /bin/bash -c 'process "{}"' \;

The script is executed weekly by cron. Weekly is more than enough. After all, for new posts and images the JPG’s are loaded until the WebP images are created. Not so bad.

0 0 * * 6 /scripts/webpresize.sh > /dev/null 2>&1

Nginx serve WebP if exists and supported

In the http section of nginx.conf add a map to set a variable when the browser supports WebP images.

	map $http_accept $webp_suffix {
		default “”;
		"~image\/webp" ".webp";
	}

And then in the server section, where you control the cache of images:
The try_files directive will first try to load the .jpg.webp file, if it doesn’t exist the original .jpg will be loaded.

location ~ \.(jpg|png|JPG|PNG)$ {
	access_log        /dev/null;
	log_not_found     off;
	add_header Vary Accept;
	try_files $uri$webp_suffix $uri =404;
	expires           1y;
	add_header Cache-Control "public";
}
location ~ \.(txt|jpeg|gif|swf|css|js|xml|woff2|flv|ico|pdf|avi|mov|ppt|doc|mp3|wmv|wav|mp4|m4v|ogg|webm|aac|eot|ttf|otf|woff|svg|JPEG|GIF|CSS|JS|ICO|XML)$ {
	access_log        /dev/null;
	log_not_found     off;
	add_header Vary Accept;
	expires           1y;
	add_header Cache-Control "public";
}
location ~ \.(webp)$ {
	access_log        /dev/null;
	log_not_found     off;
	add_header Vary Accept;
	expires           1y;
	add_header Cache-Control "public";
}

That’s it. A lot of improvements with these one time adjustments.