How fast do your WordPress pages load?

Posted on May 21, 2011 by Jimmy K. in Articles, Tutorials.

Banner

This page was loaded in {CurrentLoadTime} seconds! Go ahead, refresh this page and the load time value you just read will change. This is possible because I wrote a WordPress plugin that calculates the time it takes to load a page from start to finish. This is the first plugin I have ever created for WordPress, and I would like to share my journey with you in this article.

Research, research, research is key.

I have done a great deal of searching online for guides and tutorials for complete plugin noobs like myself and the results were not very fruitful. One very helpful article for setting up a basic plugin was Your First WordPress Plugin: Simple Optimization by Nettuts+. When creating a settings page, I did stumble across an article called WordPress Settings API Tutorial by Otto from Nothing to See Here. Otto’s article, however, was written in a very abstract manner and, while I believe it was written that way on purpose, I found it difficult to follow. Don’t get me wrong – his article was very, very helpful in the learning process – but I found myself consulting the WordPress Codex more than a few times to wrap my head around all of the functions. (And even the WordPress Codex isn’t very straightforward with this!)

Setting up the plugin.

I basically hijacked the standard plugin header from the WordPress Codex article, Writing a Plugin. This block of code contains all the information WordPress needs to accurately display information about your plugin. This information includes your plugin name, web page, description, version, author information and license.

/*
 * Plugin Name: [E7] Execute Time
 * Plugin URI: http://www.endseven.net
 * Description: Records the current and average amount of time it takes for pages to load.
 * Version: 1.0
 * Author: Jimmy K.
 * Author URI: http://www.endseven.net
 * License: GPL2
 *
 */

Om, nom, nom. Variables are delicious.

In this plugin, I have three customizable options: Timezone Offset, Enable IP Filtering and IP Filtering. There are two ways (that I know of) to store settings for your plugin in the database. Regardless of which method you choose, your values will be stored in the wp_options table, using two fields called “option_name” and “option_value”. I found it to be a pretty good idea to keep phpMyAdmin open in another tab while I was writing the settings portion of this plugin to make sure I didn’t mess anything up.

$aOptions = get_option("endseven_exec_time"); // get the values from the database..

$aVars = array(); // holds the variables for this plugin..
$aVars['Table'] = "wp_pageload_z"; // the mysql table to store page load information..
$aVars['IPFilter'] = array(); // holds the ip addresses to ignore..

$aIPFilter = explode(",", $aOptions['ip_filter']);

foreach ($aIPFilter as $oKey => $oValue) {
	$aVars['IPFilter'][] = trim($oValue); // add this ip address to the filter..
}

Line 1 retrieves any options that have been saved to the database using this plugin’s settings page. Since our settings are saved in the database as an array, the built-in function get_option() returns those values as an array so we can use them in our plugin.

Lines 3 – 5 hold any run-time variables that will be used by the various functions in this plugin. I decided not to hard code the MySQL table name in case anyone wanted to change the name of the table in the future. Line 5 initializes an array that will be used to hold any IP addresses that we don’t want to record activity for. This comes in handy if you look at your own blog often, or if a robot or search engine is constantly pinging your website and you don’t want to waste MySQL space.

Lines 7 – 11 just explodes a comma-delimited list of IP addresses and adds them to the IPFilter array.

Hooking the header and footer.

Now the question is raised: “How do we calculate the time it takes for a page to load?” Well, that’s easy! We hook the header and footer using a built-in function called add_action(), of course. This function let’s us specify our own callback functions to execute in the wp_head and wp_footer sections of our WordPress template.

Pages load from top to bottom, so the logic for this is simple. We’ll use the microtime() function to calculate a starting and ending number, then calculate the difference. We generate the starting number in the header because that’s the top of the page, and we generate the ending number in the footer because that’s the bottom. Then, we subtract the ending number from the starting number and viola, we have the number of seconds it took for our page to load.


/* herp, aderp. */
function jHeaderAction() {

	global $aVars;
	$aVars['Start'] = microtime(true); // record the start time..

	$sQuery = "CREATE TABLE IF NOT EXISTS `" . $aVars['Table'] . "` (`Stamp` DATETIME NOT NULL, `IPAddress` VARCHAR(15) NOT NULL, `URL` VARCHAR(255) NOT NULL, `LoadTime` FLOAT NOT NULL) ENGINE = MYISAM;";
	mysql_query($sQuery) or die("Unable to create table! " . mysql_error());

	ob_start(); // start the output buffer..

}

/* herp, aderp. */
function jFooterAction() {

	$sMySQLSafeURL = htmlentities(mysql_real_escape_string($_SERVER['REQUEST_URI']));
	$sMySQLSafeIPAddress = mysql_real_escape_string($_SERVER['REMOTE_ADDR']);

	global $aVars, $aOptions;

	$aVars['Finish'] = microtime(true); // record the finish time..
	$aVars['Difference'] = $aVars['Finish'] - $aVars['Start']; // calculate the difference..

	if (!in_array($_SERVER['REMOTE_ADDR'], $aVars['IPFilter']) || $aOptions['ip_filter_enabled'] != "on") {

		$sTimezoneOffset = "DATE_ADD(NOW(), INTERVAL " . number_format($aOptions['timezone_offset'], 0, "", "") . " HOUR)";

		// insert a record for this page load..
		$sQuery = "INSERT INTO `" . $aVars['Table'] . "` VALUES (" . $sTimezoneOffset . ", '" . $sMySQLSafeIPAddress . "', '" . $sMySQLSafeURL . "', '" . $aVars['Difference'] . "');";
		mysql_query($sQuery) or die("Unable to insert page load! " . mysql_error());

	}

	// select the average load times for this page..
	$sQuery = "SELECT AVG(`LoadTime`) AS `AverageLoadTime` FROM `" . $aVars['Table'] . "` WHERE `URL` = '" . $sMySQLSafeURL . "';";
	$oQuery = mysql_query($sQuery) or die("Unable to select average page load! " . mysql_error());
	$oResults = mysql_fetch_assoc($oQuery);

	$sBuffer = ob_get_clean(); // get the output buffer contents..
	$sBuffer = str_replace("[CurrentLoadTime]", number_format($aVars['Difference'], 4), $sBuffer); // output the current load time..
	$sBuffer = str_replace("[AverageLoadTime]", number_format($oResults['AverageLoadTime'], 4) . "", $sBuffer); // output the average load time..
	echo $sBuffer;

}

add_action("wp_head", "jHeaderAction"); // hook the header..
add_action("wp_footer", "jFooterAction"); // hook the footer..

Three things happen in the jHeaderAction() function. 1) We generate our start time, 2) We perform a MySQL query to make sure that the table exists, and 3) We start the output buffer. We want to make sure that the table exists because if it doesn’t, we can’t record anything and that would make calculating the average load time for each page quite difficult. We also want to start an output buffer because we’re going to be doing a little search-and-replace to display the load time to users.

The rest of the calculation goodness happens in the jFooterAction() function. 1) We generate our ending time and calculate the difference, 2) We record this request and the time it took for this page to load, 3) We select the average load time for this page, and 4) We replace the tokens “[CurrentLoadTime]” and “[AverageLoadTime]” with their respective values. For anyone not familiar with the output buffer, I suggest reading PHP: Output Control. What we’re doing here is dumping the contents of the output buffer that we started in our jHeaderAction() function into a variable, performing a str_replace() on it to replace our tokens with our dynamic load time values, and then outputting the altered buffer contents.

The built-in function, add_action(), should be self-explanatory. If you’re familiar with creating templates for WordPress, the “wp_head” and “wp_footer” values in the function parameters should jump out at you. These two lines add a sort of “hook” into those functions so when WordPress is putting your website together, our plugin functions get called too.

Onward, to the settings page!

When I initially created this plugin, all I was concerned with was getting it to work. But once it was working, I realized how awesome it would be to create a settings page for it in the WordPress admin section. I had absolutely *no* clue how to even approach the settings page, and this is where I consulted Otto’s blog, Nothing to See Here. I also used the settings page for SyntaxHighlighter Evolved by Viper007 as a visual design reference.

/* add the options. */
function jAddOptionsPage() {
	add_options_page("[E7] Execute Time", "[E7] Execute Time", "manage_options", "endseven_exec_time", jEchoOptionsPage); /* page_title, menu_title, captability, menu_slug, function */
}

/* initialize the options. */
function jInitializeOptionsPage() {

	add_settings_section("main_settings", "Main Settings", jEchoDescription, "endseven_exec_time"); /* id, title, callback, page */
	register_setting("endseven_exec_time", "endseven_exec_time", jValidateSettings); /* group, name, callback */

	add_settings_field("timezone_offset", "Timezone Offset", jEchoTimezoneOffsetField, "endseven_exec_time", "main_settings"); /* id, title, callback, page, section */
	add_settings_field("ip_filter_enabled", "Enable IP Filter", jEchoEnableIPFilterField, "endseven_exec_time", "main_settings"); /* id, title, callback, page, section */
	add_settings_field("ip_filter", "IP Filter", jEchoIPFilterField, "endseven_exec_time", "main_settings"); /* id, title, callback, page, section */

}

/* output the options. */
function jEchoOptionsPage() {

	echo "<div class=\"wrap\">\n";
	echo "<div id=\"icon-options-general\" class=\"icon32\"></div>\n";
	echo "<h2>[E7] Execute Time Settings</h2>\n";

	echo "<form action=\"options.php\" method=\"post\">\n";

	settings_fields("endseven_exec_time");
	do_settings_sections("endseven_exec_time");

	echo "<input name=\"submit\" type=\"submit\" value=\"Save Changes\" class=\"button-primary\" style=\"margin-top: 30px;\" />\n";
	echo "</form>\n";

	echo "</div>\n";

}

/* herp aderp. */
function jEchoDescription() {
	echo "<p>This is my really awesome plugin. This plugin calculates the amount of time it takes a page to load from start to finish. This plugin can also output the average load time for each individual page. To output the current load time for a page, type <strong>[CurrentLoadTime]</strong> anywhere on the page. Similarly, to output the average load time for a page, type <strong>[AverageLoadTime]</strong>. It is recommended that these values be placed in a template file at the code-level. <em>Example: This page was loaded in [CurrentLoadTime] seconds. Average load time for this page is [AverageLoadTime] seconds.</em></p>";
}

/* validate the settings. */
function jValidateSettings($aValues) {

	$aValues['timezone_offset'] = trim(preg_replace("/[^0-9+-\s]/", "", $aValues['timezone_offset']));
	$aValues['ip_filter'] = trim(preg_replace("/[^0-9., ]/", "", $aValues['ip_filter']));
	$aValues['ip_filter_enabled'] = strtolower($aValues['ip_filter_enabled']);

	return $aValues;

}

/* set the timezone offset. */
function jEchoTimezoneOffsetField() {

	global $aOptions;

	echo "<input type=\"text\" name=\"endseven_exec_time[timezone_offset]\" id=\"timezone_offset\" style=\"width: 300px;\" value=\"" . $aOptions['timezone_offset'] . "\" />\n";
	echo "<div style=\"margin-top: 5px;\">Some MySQL databases have timezones that are not the same as the one you live in. For example, if you live in Chicago and your MySQL database is located in California, you would set this value to <strong>+2</strong> because California is <strong>two</strong> hours behind Chicago.</div>\n";

}

/* set the ip filter checkbox. */
function jEchoEnableIPFilterField() {

	global $aOptions;

	echo "<input type=\"checkbox\" name=\"endseven_exec_time[ip_filter_enabled]\" id=\"ip_filter_enabled\" style=\"margin-top: 6px;\" " . ($aOptions['ip_filter_enabled'] == "on" ? "checked" : "") . " />";\

}

/* set the ip filter. */
function jEchoIPFilterField() {

	global $aOptions;

	echo "<textarea name=\"endseven_exec_time[ip_filter]\" id=\"ip_filter\" style=\"width: 300px; height: 100px;\">" . $aOptions['ip_filter'] . "</textarea>\n";
	echo "<div style=\"margin-top: 5px;\">To ignore requests from certain IP addresses, simply type them into the box above, separating each individual address with a comma.</div>\n";

}

add_action("admin_menu", "jAddOptionsPage"); // add an options page..
add_action("admin_init", "jInitializeOptionsPage");

All of the settings for this plugin are stored in an option called “endseven_exec_time”. You will see this value passed through a lot of the built-in WordPress functions. This value is very, very important and should remain consistent throughout the plugin or the settings will not save correctly.

jAddOptionsPage() adds our settings page to the WordPress admin area. The first argument is the title of our settings page. The second argument is what will appear in the sidebar of the WordPress admin area. The third argument is the capability of our settings page. (I’m not entirely sure what it does, but “manage_options” is the value you want to use.) The fourth argument is the “slug” for our settings page and should be unique to any other installed plugins. The fifth argument is a callback function that will be used to output the content of our settings page.

jInitializeOptionsPage() registers all of our settings, where they go, how we validate them, etc. First, we add a settings section using the built-in add_settings_section() function. This function accepts an ID, a title, a callback function and a page name. Then, we register our setting using the built-in register_setting() function. (Clever!) This function accepts an option group, an option name and a callback function used for validation. Finally, we have three calls to a built-in function called add_settings_field(). For each option that we want to add, we will need to call this function to add it to our section. This function accepts an ID, a title, a callback function for displaying the field, a page name and the section to add this setting to.

jEchoOptionsPage() outputs the HTML for our settings page. Every settings page should include a form object, calls to the built-in settings_fields() and do_settings_actions() functions, and a submit button. The rest of the HTML in this function is fluff, but it makes our settings page not look like vomit so we’ll keep it.

jEchoDescription() outputs a description of our settings page. This function is required by add_settings_section(), but content output is not. This is useful for when you want to create a new settings section, but you don’t have anything to say.

jValidateSettings() accepts our options array and performs basic validation. I used the preg_replace() function to strip any unacceptable characters from the Timezone Offset and IP Filter. This will ensure that our plugin doesn’t get broken by bad user input.

jEchoTimezoneOffsetField(), jEchoEnableIPFilterField() and jEchoIPFilterField() output the HTML for our options. WordPress doesn’t handle the HTML output of our settings page or the elements on it, so we still need to output those ourselves using these callback functions.

Wrapping it all up.

This is a lot of information to absorb at once and while I’m glad I figured it out, it was a very bumpy road with a lot of outbursts along the lines of, “WTF is this thing doing?!” and “I hate WordPress!” (Don’t worry. I don’t really hate you, WordPress.) I was constantly flipping between WordPress Settings API Tutorial by Otto, Your First WordPress Plugin: Simple Optimization by Nettuts+, and the WordPress Codex. In total, this plugin (and this article) took roughly ten hours to complete, but it was toadally worth it.

I hope that this article gives you a better understanding of how to create a WordPress plugin and settings page. If I have left anything out, or you need help understanding any of the code, please feel free to drop me a line in the comments. Thanks for stopping by!

Click here to download the source files.

Tags: , , , ,

 

Jimmy K. is a Chicago-based web developer who actively posts tutorials, articles and insights on his web development blog to help other programmers and developers.

You can find Jimmy on Google+ and Twitter.

 
 
 
 
 

If you like this, please leave a comment.

Name (required)
Email Address (required)
Website
Comments: