The other day, I took on the task of extending a CDN plugin to allow multiple origin domains.  Unfortunately, the CDN plugin we were working with only allowed 1 domain.

If you've ever worked with resource-intensive website, you've likely learned that parallelizing asset downloads leads to increased performance.  As a result, it's a good idea to use multiple static domains to server your content to allow the browser to pull in as much content as is can quickly.

PHP and Constants

The convenient thing about the original plugin was its used of a configuration constant for specifying the CDN domain.  Drop the plugin in [cci]/mu-plugins[/cci], set up a constant in your [cci]wp-config.php[/cci], and you're ready to go.

Sadly, this doesn't allow for parallel asset downloads.

Sadder still, you can't define an array as a constant in PHP.  I had to look for another alternative.

The first draft utilized a Singleton-style object factory to create the CDN object.  You then added as many domains as you want programatically.  This worked pretty well, but to make it run on production you now need to define two must-use plugins instead of one.  The alternative was to somehow force a constant to work as an array.

Defining your multiple CDN domains as a comma-delimited list looks messy, but it works just fine.  The constructor of the CDN object reads in the constant, [cci]explode()[/cci]s the list, [cci]trim()[/cci]s off any accidental whitespace, and moves merrily along.

Spreading the Load

Once we had multiple CDN domains available, we needed a way to route assets somewhat evenly amongst them.

We also wanted to make sure that individual assets always mapped to the same CDN domain so that browsers would cache them appropriately - this meant randomly mapping images to [cci]cdn1.domain.com[/cci] versus [cci]cdn2.domain.com[/cci] wouldn't be a safe idea as images would switch, at random, from one CDN to the other on repeated page loads.

Also, running a round-robin between the available domains would only be stable so long as new images were never added to the page.  A new image at the top would force everything to swap to a different domain, thus busting the browser cache.

My solution - a rarely used PHP function called [cci]crc32()[/cci].

Checksums

Using PHP's built-in [cci]crc32()[/cci] function, I can reliably reduce any asset url to an integer.  Every image url will always map down to the same checksum, meaning it's independent of load time, processing time, or visitor.

Further, I can calculate the modulus of that checksum against the number of available CDN urls, generating an array key I can then use to select a specific CDN url.  This all boils down to one magic little function:

/**
* Get a CDN path for a given file, using a reduced checksum to automatically select from an array of available domains.
*
* @param string $file_path
*
* @return string
*/
public function cdn_domain( $file_path ) {
// First, get a checksum for the file path to give us the index we'll use from the CDN domain array.
$index = abs( crc32( $file_path ) ) % count( $this->cdn_domains );

// Return the correct CDN path to the file
return apply_filters( 'dynamic_cdn_domain_for_file', $this->cdn_domains[ $index ], $file_path );
}

The result of the above - every image on the site maps to a single, reliable CDN url every time.

Open Source

My new CDN plugin is freely available on GitHub for all to enjoy (or critique).

It borrows heavily from the CDN component of Mark Jaquith's WP Stack toolkit, which is also available on GitHub.

The general gist is to scan page content before returning it to the browser and dynamically replace locally-hosted assets (images, scripts, styles, etc) with their CDN-hosted equivalents. If you use a service like Photon, rewriting your image tags to reference Photon-hosted versions of the same assets will drastically improve your site's performance.

If you use another CDN system, minor changes might be required to get things to work right. Luckily, the plugin exposed a wide variety of filters for just that purpose. If you give it a try and find more filters (or other changes) are needed, I'll be glad to review any pull requests.