Immutable assets with unique URLs in WordPress for enqueued JS and CSS files

If you’re utilizing the browser cache correctly, you’ll gain huge performance benefits for your users, as well as save bandwidth and server capacity which equals to saving money. To do this right, you must create unique URLs for all versions of your resources, and tell them to never ask for the content again by telling the browsers that the assets are immutable resources.

The next time I hear an arrogant or ignorant frontend developer telling a client to “clear their cache to see the changes” I’m going to smack them over the head with a fax machine (they won’t know what hit them). You can’t ever expect a returning visitor to clear their cache to see the changes you’ve done in the *static* assets.

Immutable assets and resources

A bit simplified, your HTML pages are made up of the text you put in your editor, the templates from the theme you are using, sprinkled with some additional output from plugins. The theme and some of your plugins will most likely have links to static assets, including JS and CSS files. To avoid our users having to download these assets over and over again, they should have a really long expiration time – so long they’re to be considered “forever” (that’s like a year or even more in computer time).

These assets are called long-lived resources, and the clients don’t expect them to ever change.

Because the server tells the browser to cache the resources “forever”, you should consider them immutable. Changing the content at those resources is done in vain, and will probably just break the site for your visitors. The clients, or any caching proxies, won’t get the changes you put in them.

Create unique URLs with version strings in the URL

To tell the clients that you have a “new” resource with updated content, you should create a “new” URL that’s unique for that content. The best way to do so, is to add the version number of the resource directly in the URL.

Doesn’t WordPress already add version strings to the URLs?

Yes, it kind of does. It does so in a manner that is very cautious and fail-safe. WordPress adds the version to a query var, like styles.css?ver=1.2.3.

If you’ve tried a web page speed test service, you’ve probably got a message that you should “Remove query strings from static resources”.  because

Resources with a “?” in the URL are not cached by some proxy caching servers.

Oh. We want something more aggressive. Something that puts the version string directly in the “filename” part of the URL, like styles-1.2.3.css. That will be easier for us to deal with in our web server configs if we want to do special things, and caching will work across all weird proxies and caching engines that are around. But we really don’t want to create new files every time we update a resource. Actually, we want those with the old URL to use the updated content as well, if that edge case scenario ever occurs.

What we can do, is to create symlinks with the new version string in their filename. But that will fill up your filesystem with a lot of entries, as you will have to keep the old URLs for the clients that have cached the URLs to those resources, but not the content.

Instead, we can modify our web server configuration, so that all URLs matching a resource URL with a version string in it, are aliased to the real resource file. Just add the config once, and it will continue to function without any more work on our side. Then we’ll just have to filter the enqueued asset URLs in WordPress so that the version strings are moved from a query argument to the filename part of the URL.

Configure your web server to handle the new URLs

It is quite important that you do this in the correct order. You should configure your web server to accept the new URLs as aliases to the real files, before you modify the URLs going out from WordPress. The web server should work fine with both kind of URLs: both the real ones and the aliases. But once you change the URLs going out from WordPress the web config must work or your website will break.

In addition to handling the new URLs, we want our web server to send the Cache-Control: immutable header. This is also known as the browser tweak that saved 60% of requests to Facebook:

In testing this change the Chrome team was able to run an A/B test that found for mobile users with a 3G connection across all websites the 90th percentile reload was 1.6 seconds faster with this change.

~ Facebook after implementing immutable assets

The header will tell the browser that they never have to check again if the resource has changed.

Alias for JS and CSS in Nginx

If your web server is Nginx, this is the configuration you should put in your server context. You might already have a section that deals with static resources. If so, you have to see for yourself if you need to make any adaptations.

location ~* (.+)\.(?:[a-zA-Z0-9]+)\.(js|css)$ {
    try_files $uri $1.$2 =404;
    add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
    add_header Cache-Control "max-age=315360000, public, immutable";

If the browser is requesting an URL that fits our new pattern, Nginx will first see if there’s an actual file in the system with that name, before trying to fetch one without our injected version string in it.

Alias for JS and CSS in Apache

If you are using Apache 2.4, you should add this to your VirtualHost configuration block – or your .htaccess file.

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule (.+)\.(?:[a-zA-Z0-9]+)\.(js|css)$ $1.$2 [L]
<IfModule mod_headers.c>
    <FilesMatch "\.(js|css)$">
        Header set Expires "Thu, 31 Dec 2037 23:55:55 GMT"
        Header set Cache-Control "max-age=315360000, public, immutable"

This requires that you have the modules rewrite and headers enabled. You can enable them by running:

$ sudo a2enmod rewrite headers && sudo service apache2 reload

Test your configuration

Go to your browser, open the web inspector/developer tools and check that all resources load as they should. They should all have a 200 or 304 status code, none of them should be 404.

Now, if one of your asset URLs are like this:


Try requesting an URL like this:


And it should work just fine too.

Instead of NC43LjM you can use any bogus string you like, but it is actually the version string 4.7.3 base64 encoded. Below, I’ll show you the code to inject the version strings into the enqueued resource URLs. If needed, we are normalizing the version strings by base64 encoding them, so that the alias we defined in Nginx or Apache will work. It would be much harder to setup the web server config in a fail-safe manner without the normalization.

Modifying the enqueued resources in WordPress

When your web server is configured correctly (do that first!) you can use filters to modify script and style URLs. You can simply drop this file in your mu-plugins directory and it should be working. It only modifies URLs that we are sure points back to your own server, and it handles URLs that are protocol agnostic or without hostnames.

Use with caution

Consider both the web server configuration and the WordPress code to be in beta state. I’ve only had the chance to test it on one other site that my own (thank you, Marius!). If you use it in production, use it with caution. As usual: There are no warranties.