Why Your WordPress Plugin Should Have Almost Nothing In The Main Folder
As we continue to roll out our Store Locator Plus SaaS service built on top of WordPress as our application foundation we continually refine our plugin, theme, and API architecture. One of the issues I noticed while testing performance and stability is how WordPress Core handles plugins. Though WordPress caches plugin file headers there are a lot of cases where it re-reads the plugin directories.
What do I mean by “read the plugin directories”?
WordPress has a function named get_plugin_data(). Its purpose is simple. Read the metadata for a plugin and return it in an array. This is where things like a plugin name, version, an author come from when you look at the plugins page.
However that “simple” function does some notable things when it comes to file I/O. For those of you that are not into mid-level computer operations, file I/O is one of the most time consuming operations you can perform in a server based application. It tends to have minimal caching and is slow even on an SSD drive. On old-school rotating disks the performance impact can be notable.
So what are those notable things?
It is best described by outlining the process it goes through when called from the get_plugins() function in WordPress Core.
- Find the WordPress plugins directory (easy and fast)
- Get the meta for every single file in that directory using PHP readdir and then…
- skip over any hidden files
- skip over any files that do not end with .php
- store every single file name in that directory in an array
- Now take that list of every single file and do this…
- if it is not readable, skip it (most will be on most servers so no saving time here)
- call the WP Core get_plugin_data() method above and store the “answers” in an array , to do THAT, we need to do THIS for all of those files
- call WP Core get_file_data() which does this..
- OPEN the file with PHP fopen
- Read the first 8192 characters
- CLOSE the file
- Translate all newline and carriage returns
- Run WordPress Core apply_filters()
- Do some array manipulation
- Do a bunch of regex stuff to match the strings WordPress likes to see in headers like “Plugin Name:” or “Version:” and store the matching strings in an array.
- Return that array which is the “answers” (plugin metadata) that WordPress is interested in.
- call WP Core get_file_data() which does this..
- take that array and store it in the global $wp_plugins variable with the plugin base name as the key to the named array.
In other words it incurs a LOT of overhead for every file that exists in your plugin root directory.
Cache or No Cache
Thankfully viewing a plugin page tends to fetch that data from a cache. The cache is a string stored in the WP database so a single data fetch and a quick parsing of what is likely a JSON string and you get your plugins page listing fairly quickly. However caches do expire.
More important to this discussion is the fact that there are a LOT functions in the WordPress admin panel and cron jobs that explicitly skip the cache and update the plugin data. This runs the entire routine noted above to do that.
Designing Better Plugins
If you care about the performance impact of your plugins on the entire WordPress environment in which it lives, and you SHOULD, then you may want to consider a “minimalist top directory approach” to designing your plugins.
Best Practices on the Plugin Developer Handbook mentions “Folder Structure” and shows an example of having something like this as your plugin file setup:
/plugin-name plugin-name.php uninstall.php /languages /includes /admin /js /css /images /public /js /css /images
However they don’t get into the performance details of WHY you should have an includes directory and what goes in there.
In my opinion, EVERYTHING that is not the main plugin-name.php or uninstall.php file should go in the ./includes directory. Preferably in class files named after the class, but that is a discussion for another blog post.
If possible you may even want to try making plugin-name.php as minimalist as possible with almost no code. Even though the fread in WordPress Core get_file_data() only grabs the first 8192 characters, most of that content is “garbage” that it will not process because it is not part of the /* … */ commentary it is interested in. If you can get your main plugin-name.php file to be something like 4K because it only includes the header plus a require_once( ‘./includes/main-code-loader.php’); or something similar the memory consumption, regular expression parser and other elements used by get_file_data() will have less work to do.
No matter what your code design, it is going to have some performance impact on WordPress. My guess is it will be especially notable on sites that have 3,987 plugins installed and are running an “inline” WordPress update. Ever wonder why that latest version of your premium (not hosted in the WordPress Plugin Directory) plugins don’t show up? It could be because WordPress spent all the time granted to a single PHP process reading the first 8K of 39,870 files because all those plugins had a dozen-or-so files in the root directory.
Help yourself and help others. Put the bulk of your plugin code in the includes folder. The WordPress community will thank you.
Update to this post:
Reading WordPress Core 4.7.3 get_plugins() a little more closely, the performance problem is even deeper than first thought.
WP Core read everything in the top level plugin directory AND ONE LEVEL DEEP in subdirectories.
Any hidden file (files starting with ‘.’ – not sure how that impacts Windows installs) is skipped immediately.
Any file with a ‘.php’ extension is then added to the ‘scan for headers’ list.
The bottom line is that any non-php files in the top level of one-level deep subdir is OK where it is. However you need to bury your .php files at least TWO levels deep. To improve WordPress plugin processing overhead.
Next up… investigate how often get_plugins() is called in WP Core.