CSS & JS Concatenation in WordPress

At WordPress.com VIP one of the features we have on our platform is automated concatenation of Javascript and CSS files when registered through the core WordPress wp_enqueue__*() functions.

We do this using the nginx-http-concat plugin:

This plugin was written to work with nginx, but the server running derrick.blog is Apache.  I’ve worked around this and have nginx-http-concat running fully in WordPress, with added caching.

The bulk of the plugin is this file, which does all of the work of caching and calling the nignx-http-concat plugin:

<?php
// phpcs:disable WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.ValidatedSanitizedInput, WordPress.VIP.FileSystemWritesDisallow, WordPress.VIP.RestrictedFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, WordPress.WP.AlternativeFunctions.json_encode_json_encode
if ( isset( $_SERVER['REQUEST_URI'] ) && '/_static/' === substr( $_SERVER['REQUEST_URI'], 0, 9 ) ) {
	$cache_file      = WP_HTTP_CONCAT_CACHE . '/' . md5( $_SERVER['REQUEST_URI'] );
	$cache_file_meta = WP_HTTP_CONCAT_CACHE . '/meta-' . md5( $_SERVER['REQUEST_URI'] );

	if ( file_exists( $cache_file ) ) {
		if ( time() - filemtime( $cache_file ) > 2 * 3600 ) {
			// file older than 2 hours, delete cache.
			unlink( $cache_file );
			unlink( $cache_file_meta );
		} else {
			// file younger than 2 hours, return cache.
			if ( file_exists( $cache_file_meta ) ) {
				$meta = json_decode( file_get_contents( $cache_file_meta ) );
				if ( null !== $meta && isset( $meta->headers ) ) {
					foreach ( $meta->headers as $header ) {
						header( $header );
					}
				}
			}
			echo file_get_contents( $cache_file ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
			die();
		}
	}
	ob_start();
	require_once 'nginx-http-concat/ngx-http-concat.php';

	$output = ob_get_clean();
	$meta   = array(
		'headers' => headers_list(),
	);

	file_put_contents( $cache_file, $output );
	file_put_contents( $cache_file_meta, json_encode( $meta ) );
	echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
	die();
}

This little bit of code in wp-config.php is what calls the above file, before WordPress even initializes, to make this as speedy as possible:

define( 'WP_HTTP_CONCAT_CACHE', dirname(__FILE__) . '/wp-content/cache/http-concat-cache' );
require_once dirname(__FILE__) . '/wp-content/mu-plugins/emrikol-defaults/config-nginx-http-concat.php';

Finally, in an mu-plugin these lines enable the nginx-http-concat plugin:

if ( ! is_admin() ) {
	require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/nginx-http-concat/cssconcat.php' );
	require_once( plugin_dir_path( __FILE__ ) . 'emrikol-defaults/nginx-http-concat/jsconcat.php' );
}

You’ll notice the is_admin() check, because at this time, the nginx-http-concat plugin doesn’t play nice with Gutenberg.  It’s probably going to get fixed soon, but in the meantime, I’ll just leave it disabled for wp-admin

All of this could definitely be packed into a legit plugin, and even leave room for other features, such as:

  • An admin UI for enabling/disabling under certain condition
  • A “clear cache” button
  • A cron event to regularly delete expired cache items

As it is now though, I’m just leaving it be to see how well it works.  Wish me luck 🙂

Leave a Reply