WP Smith

Creating WordPress & Genesis Websites Since 2010

  • Home
  • About
  • Services
  • Blog
  • Contact

About Travis Smith

As a WordPress enthusiast, developer, and speaker, Travis writes about what he learns in WordPress trying to help other WordPress travelers, beginners and enthusiasts with tutorials, explanations, & demonstrations.

Sep 16 2020

Solving WordPress 5XX Server Errors on SiteGround

500s on SiteGround

Lately, I have helped a lot of clients who have 5XX errors on SiteGround, especially eCommerce sites. If you are experiencing any 499, 500, 502, 503, and 504 errors on SiteGround, please know that they are very common. I know there is nothing more frustrating than having one's site down, especially if you do not know why! If your site is down, you are truly suffering:

  1. You lose those precious leads
  2. You lose revenue and real money whether via clients, ads, or purchases from your store
  3. You lose trust from your potential customers
  4. Your SEO rankings will drop because a site with poor uptime will less likely be available when the search needs it from a search results page

Look no further! In this article, I will explain what the error codes mean, some of the terms and relevant architecture, and outline my 15 steps of what I do to address these issues.

  1. Understanding 500 Errors
  2. Solving 500 Errors
  3. Understanding 499, 502, 503, & 504 Errors
  4. Understanding the Web Hosting Architecture
  5. What to Expect from Managed Hosting Support
  6. 15 Ways of Solving 499, 502, 503, & 504 Errors
  7. Summary
Hire Me!
Hire Me on Codeable

Why is it so difficult to fix the 5xx errors on SiteGround?

On SiteGround, the problems with even identifying the 5XXs are: 

  1. No visibility provided by SiteGround on the number of 5XXs.
  2. Inability to access the logs in real time via SSH, which are 15-30min behind.
  3. No alerts to you or from the system of any 5XXs.

Essentially, you have absolutely zero ability to identify the 5XXs without SiteGround support which they have consistently answered, "Scale the hosting plan" (which obviously benefits them). SiteGround is not really motivated to suggest anything else, so besides suggesting external experts and recommending you scale your site, I doubt you will ever get anything else from their Support team.

As a side note, SiteGround is not 100% designed and optimized for WordPress like WP Engine (which solely and only hosts WordPress). Many managed hosts that may have originally been designed for WordPress have expanded to support other types of applications like Drupal and Joomla, which also uses the same technological stack. While it makes sense to do this, fine tuning for WordPress v. Drupal v. Joomla is still different albeit very small (but these nuances matter at scale). Only when a hosting company scales their hosting do these nuances begin to show themselves. Those managed hosting companies like Flywheel that use containers (probably Kubernetes) can still claim they maintain optimization for WordPress though they've expanded into other platforms.

Can You Just Solve This For Me!?

If you do not care about how to solve this, and just want someone to do this, you are looking at between $500–$3,000. With others, it may even cost you more as they may not have a strategic approach as I do. This sort of work is considered a troubleshooting task where you are paying based on the number of projected hours worked (and must pay hourly until solved with no guarantees). The strategy laid out below is a strategic plan that can be done as a service that will give you a level of satisfaction. If you are still interested, you can hire me immediately by contacting me directly or via Codeable.

Disclaimer

Please feel free to read this article and attempt to fix your site yourself at your own risk! Please note that I cannot be held liable for what you attempt to do on your own. If you attempt to do what is outlined here and mess up your site even further, it will most likely cost you even more monies to rectify and help you. So please, please create backups of everything before moving forward with attempting to fix things yourself or with hired help.

Hire Me!
Hire Me on Codeable

Understanding 500 Errors

A 500 is an Internal Server Error. The server has encountered a situation that it doesn't know how to handle, and usually a 500 error is more persistent.

All hosts handle this the same way and this issue is not really germane to just SiteGround. This is also known as the WordPress White Screen of Death (though that also occurred for other reasons too). The reason for these 500 errors can be limitless, but generally the primary cause is a plugin, a missing plugin/files, or some bad code somewhere.

Now what is problematic is that SiteGround calls all their errors 500 to the end user. You only see the actual error code though the access logs. All users get the error page pictured above that says 500 - Internal Error. So the actual error code could be any one of these: 499, 500, 502, 503, or 504; all of which mean something different to a developer.

So what do you do to solve your 500 issue(s)?

If you are on a managed host (e.g., SiteGround's Cloud Hosting, WP Engine, etc.), then simply revert to a previous installation/backup. This will be the easiest by far and will not require you to hire a developer. If you are more technical or daring...then you can try some of the recommendations below. Alternatively, you can hire me or another expert or contact me directly.

If this happened right after upgrading or activating a plugin, then you simply want to deactivate the plugin either via WordPress admin or WP CLI. To do this:

  • via SFTP/FTP, simply go to the wp-content/plugins folder and rename the plugin folder (e.g., from my-plugin to my-plugin.bak) and log into the WordPress admin plugins page (wp-admin/plugins.php). Going to the plugins page will automatically deactivate the plugin (because it will be considered missing).
  • via WP CLI, at the root of the site (same place where wp-config.php is located), run wp plugin deactivate my-plugin-slug. For more information, see WP CLI plugin command.

If this happened right after upgrading your theme, then you want to do something similar. To do this:

  • via SFTP/FTP, simply go to the wp-content/themes folder and rename the plugin folder (e.g., from my-theme to my-theme.bak) and log into the WordPress admin plugins page (wp-admin/themes.php). Going to the themes page will automatically activate the default theme giving you a notice that the active theme is broken.
  • via WP CLI, at the root of the site (same place where wp-config.php is located), run wp theme install my-theme-slug --activate. For more information, see WP CLI theme command.

If you have access to the WordPress admin (meaning you can log into the site, access the plugins (wp-admin/plugins.php) and/or themes page (wp-admin/themes.php) and can install plugins.

  1. Go to the plugins page.
  2. Install the Health Check plugin.
  3. Begin troubleshooting mode via the Advanced.
  4. Enable your theme and check the site.
  5. Enable plugins one by one and check the site after every activation.

These methods will help you temporarily fix the issue and you may need to hire a developer or myself to completely fix this for you.

Hire Me!
Hire Me on Codeable

Understanding 499, 502, 503, & 504 Errors

There are four primary 5XX error codes that your server will be giving you: 499 Client Closed Request, 502 Bad Gateway, 503 Service Unavailable, and 504 Gateway Timeout.

  • A 499 is Client Closed Request. This was originally a NGINX Error code to highlight that the client closed the request before the server could respond.
  • A 502 is Bad Gateway. The server was acting as a gateway and received an invalid response from the upstream server.
  • A 503 is Service Unavailable. The server is not ready to handle the request, generally as a result of being overloaded or down for maintenance.
  • A 504 is Gateway Timeout. The server was acting as a gateway and did not receive a timely response from the upstream server.

There are several reasons that this is happening. First, you could be under a DDOS Attack of some kind. If this is the case, you need to implement Cloudflare or Sucuri ASAP. Second, you could have a surge in traffic that you were not expecting. Third, your resources (CPU/RAM) was not configured properly. Fourth and onward, too complicated to explain in this short-ish post.

Skip Architecture

Understanding Web Hosting Architecture

So to prevent the 5XXs, you could scale out (or expand) your resources (which costs money always) or remove load off your server by offloading traffic. To understand what I mean by offloading traffic, you need to understand the general architecture of WordPress on managed hosting platforms. On SiteGround (and many other installations), you are generally dealing with these layers:

  1. Web Server layer (e.g., Apache) with an ingress (e.g., NGINX)
  2. (Optional) Caching layer
  3. Web Application layer (e.g., WordPress)
  4. Object Cache layer (e.g., memcached and/or in-memory)
  5. MySQL Database layer
  6. Filesystem layer (see also Help Desk Geek's article on Inodes in Linux)

While I don't know SiteGround's architecture explicitly, I am fairly certain that SiteGround is using NGINX, Apache, memcached, and a database (MySQL or PostgreSQL) all on the same set of resources. There is no resource separation or isolation for preventing interference with one another (though I could be convinced that NGINX is separate from the resources as it appears to be a global thing). At scale, you would have each of these items isolated (and at super-scale a sharded MySQL, but you can ignore this as most WordPress sites don't need this level of scale).

So far, I have derived the following sequence on SiteGround:

  1. Traffic enters the ecosystem via NGINX (called an ingress). If you have SG Optimizer installed and enabled/configured via the SiteTools Dashboard, then NGINX will respond at that layer for all static resources (e.g., images, JS, CSS, etc.). NGINX is much, much more performant for handling these requests than Apache.
  2. Traffic is then handed off to the site specific stack fronted by Apache as directed by .htaccess file. This will catch any and all static assets that missed (for whatever reason) the NGINX ingress.
  3. If a caching plugin like SG Optimizer or WP Rocket is installed, then the cache may intercept the traffic and respond.
  4. If there is no cache or cache is bypassed (called a cache miss), traffic is then handed off to WordPress to process.
  5. WordPress will load all its core files, plugins and theme files depending on object cache and/or memcached (if configured properly on SiteGround either via SG Optimizer or other caching plugin) fetching from the MySQL DB as needed (autoloading all [or mostly all] options).
  6. The Filesystem returns all the files and data to be read by WordPress.

Here's a sequence diagram that may help...hopefully.

sequenceDiagram participant Client participant NGINX participant Apache participant WordPress participant Memcached participant MySQL participant Filesystem Client->>NGINX: A Request NGINX->>Filesystem: Checks cache alt NGINX Miss note over NGINX,Apache: NGINX checks the cache.<br>If not cached, continue. Filesystem-->>NGINX: (Empty) NGINX->>Apache: Cache MISS note over Apache, WordPress: Apache checks .htaccess Apache->>Filesystem: Checks cache alt Apache Miss Filesystem-->>Apache: (Empty) Apache->>WordPress: Cache Miss\nBegin Processing Request WordPress->>Filesystem: Load WP PHP Files WordPress->>Memcached: WP checks cache Memcached->>Filesystem: Get Data alt Not Cached note over WordPress,MySQL: If Object Cache does not have it, get from DB Filesystem-->>Memcached: (Empty) Memcached-->WordPress: (Empty) WordPress->MySQL: Query DB MySQL->Filesystem: Get data Filesystem->MySQL: Read data MySQL->WordPress: Fres Data else DB Cached Memcached->WordPress: Cached Data end WordPress->WordPress: Process query opt Populate DB Cache WordPress->Memcached: Populate Cache Memcached->Filesystem: Write Data end opt Plugin Populates File Cache WordPress->Filesystem: Cache HTML, Write Data end WordPress->Apache: Response else Apache Cached note over Apache, WordPress: Apache gets cached file Filesystem->Apache: Cached Response end else NGINX Cache note over NGINX, Apache: If cached, get the cached file. Filesystem->NGINX: Cached Response end NGINX->Client: The Response

By default, SiteGround configures http2_idle_timeout and http2_recv_timeout to 600 seconds, and these are not changeable. Also by default, the max_execution_time of PHP is 120s, which is configurable via user.ini at the root of your site (same location as wp-config.php). So if you increase the max_execution_time to 600 or greater then you will eventually see 499s as opposed to 5XXs since NGINX will timeout before the PHP times out.

Next, you need to understand the most vulnerable parts of WordPress. Outside the known issues with load-scripts and load-styles, the most vulnerable parts of WordPress are:

  • XMLRPC
  • Login Page
  • Admin Area
  • Any theme/plugin index.php files

If you look at your access logs, you will find that bots, people, crawlers, scrapers, etc. will be hitting those URLs trying to gain access to your site...just because they can. So you want to mitigate those as soon as you are able.

What to Expect from Managed Hosting Technical Support

If you engage the SiteGround technical support, more than likely you will be referred to external WordPress experts, like myself. At best, they will tell you to scale your site with more RAM or CPU, which obviously helps them (and really doesn't solve the problem). Running a WooCommerce WordPress site on SiteGround should be able to run well with 3-4 CPUs and 6-8GB RAMs assuming you have the appropriate plugins, architecture and caching established.

Alternatively, technical support may tell you something like the site has high IOPS (input/output operations). Simply, IOPS are read/writes of the hard disk. IOPS is the reading of the various PHP files (plugins/themes and template files) as well as the writing of cache files and MySQL transactions in order to render the site. Higher IOPS also increases memory usage and in some occasions increases CPUs (but rarely with WordPress sites). In my experience, high IOPS is characteristic of many eCommerce sites and that’s one of the few problems and is a symptom of many other potential root causes. This data and debug information is useful if you have access to monitor these processes, which on a managed host like SiteGround, you do not have.

So what do you do to solve your 499, 502, & 503 issues?

Depending on your setup, the plan I would create may differ, but generally speaking you would need to take the following actions. I attempted to organize these actions in a somewhat logical order, but depending on the situation, I would do some tasks before other tasks. Also, while these items will work almost on any host, all examples will focus almost solely on SiteGround. These actions are:

  1. Analyze the Logs for 499s, 500s, 502s, 503s, & 504s
  2. Check PHP Version
  3. Run WP CLI’s Doctor Command
  4. Properly Configure a CDN
  5. Disable XMLRPC
  6. Control Bots
  7. Check Memory & Compute Utilization
  8. Configure Proper Cron Jobs
  9. Use Optimized Plugins
  10. Optimize the Database
  11. Offload All Emails
  12. Limit AJAX Usage
  13. Offload Images/Assets
  14. Analyze WP Actions & Filters
  15. Determine Cache Strategy
1. Analyze the Logs for 499s, 500s, 502s, 503s, & 504s

First and foremost, you want to check all the logs to see what is impeding or bogging down the server. This exercise is entirely exploratory and will only reveal further issues that need to be addressed. Based on work I have done so far, most of the time these will reveal issues that will be addressed in subsequent items (e.g., XML-RPC, bots, etc.). Sometimes, however, they reveal gems.

Access logs are in a gzipped file, so you need to inspect them using zcat. SSH into the site and check the logs. You can find them at www/domain.com/logs/. You can find all the errors by using one of these commands:

# Search the gzipped file for 499s and all 5XXs.
zcat domain.com-YYYY-MM-DD.gz |
grep -P "\"\s((499)|(5\d+))\s"
# Search for all 499s.
zcat domain.com-YYYY-MM-DD.gz | grep "\" 499 "
# Search for all 503s.
zcat domain.com-YYYY-MM-DD.gz | grep "\" 500 "
# Search for all 503s.
zcat domain.com-YYYY-MM-DD.gz | grep "\" 503 "
# Search for all 5XXs.
zcat domain.com-YYYY-MM-DD.gz |
grep -P "\"\s5\d+\s"

As you work through the access logs, you will find a variety of pages that are the targets of various crawlers/bots and users. More than likely, the bots are causing most of the damage. If you have a WooCommerce site, you want to check the various Woo pages as well as their AJAX calls (via wc-ajax). Also, check your archive pages, your /feed/ pages, and the WP API (/wp-json/) URLs.

The format of a log entry at SiteGround is a bit different. SiteGround uses a Custom Log Format combining logs from Apache and NGINX. So, in the techy world it looks like this (if you're not techy, just skip this):

In human words, it is this:

%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" | TLS |
$upstream_header_time $upstream_response_time $request_time
$upstream_cache_status $nocache NC:$log_nocache BP:$log_cache_bypass
(IP Address) (identd of client or -) (userid of client or -) [ISO
timestamp] "(HTTP METHOD) (Request URI) (HTTP Version)" (Status Code)
(Object Size in bytes) "(Referer)" "(User Agent)" | (TLS Version) |
(Upstream Header Time) (Upstream Response Time) (Request Time) (Cache
Status = HIT|MISS|BYPASS) NC:(SG Specific Codes) (Binary 0/1 Cache Hit)

For an explanation of the latter third see NGINX's Embedded Variables.

Here's a real example (with IP and domain obfuscated):

9.999.999.99 domain.com - [11/Aug/2020:14:29:37 +0000] "GET /path-to-
my-awesome-page/ HTTP/1.1" 503 2676 "https://domain.com/my-awesome-
blog/" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:79.0)
Gecko/20100101 Firefox/79.0" | TLSv1.3 | 0.037 0.041 0.041 MISS
W NC:000000 UP:1

Error Logs are currently only available via the SiteGround dashboard. According to the SiteGround Dashboard:

The error log displays the 300 most recent errors that have occurred when accessing your website over HTTP or HTTPS. Reviewing the error log can be useful for troubleshooting broken links, files and other errors.

However, this is not true at all. At the time of writing this post, they only show the 50 most recent errors. I did file a ticket with SiteGround and they have escalated the issue to their DevOps team but no timeline has been given as to when it will be fixed.

PHP Error Logs & WP Logs are only available via SSH or FTP. For PHP Error Logs, you will find a file like php_errorlog and potentially error_log in the folder where the PHP error occurred, which is somewhat annoying. So you want to find this via find . -name "php_errorlog" on the SSH command line or use the find function in your favorite FTP client.

For WP Logs, check wp-content/debug.log. If the file does not exist (and really should not on a production site), you want to temporarily enable WP_DEBUG in wp-config.php. This can even be done dynamically via:

// Uncomment to enable WP_DEBUG. Of course, you want to change this cookie value to something else.
//define( 'WP_DEVELOPER_COOKIE', 'V1AgRGV2ZWxvcGVyIGZvciBkb21haW4uY29tIGRlYnVnZ2luZyBwdXJwb3Nlcy4=' );
// Enable WP DEBUG if debug query parameter exists OR the "wp_developer" cookie exists with a specific value (WP_DEVELOPER_COOKIE).
$wp_debug = (
isset( $_GET['debug'] ) ||
( isset( $_COOKIE['wp_developer'] ) && defined( 'WP_DEVELOPER_COOKIE' ) && $_COOKIE['wp_developer'] == WP_DEVELOPER_COOKIE )
);
define( 'WP_DEBUG', $wp_debug );
define( 'WP_DEBUG_LOG', $wp_debug );
define( 'SCRIPT_DEBUG', $wp_debug );
// Enable WP DEBUG_DISPLAY if debug query parameter equals "display".
define('WP_DEBUG_DISPLAY', ( $wp_debug && isset( $_GET['debug'] ) && ( 'display' === $_GET['debug'] ) ));

The way this work is probably self-evident but it is a method that I use on the majority of the sites I do any development. It's a great and secure method to never have to edit wp-config.php continually.

If you are thinking, "Ok, I realize this is too much for me," you can hire a developer or myself to help you through this.

Hire Me!
Hire Me on Codeable
2. Check PHP Version

If you are on SiteGround, more than likely you are on the Managed PHP version and are on the latest version of PHP, but it's worth a check. PHP 7.x provides more performance and security than PHP 5.x. You can check your PHP version in SiteTools > Devs > PHP Manager (https://tools.siteground.com/php-settings).

3. Run WP CLI's Doctor Command

By default, WP CLI is installed on the server, but the doctor command is not. So first you will need to install the WP CLI doctor command via wp package install [email protected]:wp-cli/doctor-command.git and then run wp doctor check --all at the root of the WordPress site (same location as wp-config.php) and examine those issues to fix. I use a custom yaml configuration for my magic fixes and to automate some of the items listed in this longer-than-expected blog post.

wp package install [email protected]:wp-cli/doctor-command.git
wp doctor check --all
Running checks 100% [=================================================================================] 4:19 / 0:05
+----------------------------+---------+--------------------------------------------------------------------+
| name | status | message |
+----------------------------+---------+--------------------------------------------------------------------+
| core-verify-checksums | success | WordPress verifies against its checksums. |
| file-eval | success | All 'php' files passed check for 'eval\(.*base64_decode\(.*'. |
| cache-flush | warning | Use of wp_cache_flush() detected. |
| autoload-options-size | success | Autoloaded options size (531.1kb) is less than threshold (900kb). |
| constant-savequeries-falsy | success | Constant 'SAVEQUERIES' is undefined. |
| constant-wp-debug-falsy | success | Constant 'WP_DEBUG' is defined falsy. |
| core-update | success | WordPress is at the latest version. |
| cron-count | success | Total number of cron jobs is within normal operating expectations. |
| cron-duplicates | success | All cron job counts are within normal operating expectations. |
| option-blog-public | success | Site is public as expected. |
| plugin-active-count | success | Number of active plugins (13) is less than threshold (80). |
| plugin-deactivated | success | Less than 40 percent of plugins are deactivated. |
| plugin-update | success | Plugins are up to date. |
| theme-update | success | Themes are up to date. |
| php-in-upload | success | No PHP files found in the Uploads folder. |
| language-update | success | Languages are up to date. |
+----------------------------+---------+--------------------------------------------------------------------+
4. Properly Configure a CDN

In order to prevent load from reaching your server, you must establish a solid caching strategy including browser cache and an edge cache via configuring a CDN. It is the easiest and fastest way to prevent useless load on your server which reduces 499, 503 and 504 errors. I always recommend that clients either use Cloudflare or Fastly (if you're OpenSource software, Fastly will front your site for free).

Implement the free version of Cloudflare (personally, I would do this at Cloudflare and not via SiteGround). Within Cloudflare, enhance security around /wp-login.php* and /wp-admin/*, geofence /wp-admin, and set the browser cache of general assets to a few months or even a year and the edge cache to a month or two.

I always recommend paying for Cloudflare Pro minimally. This gives you image optimization out-of-the-box, plus more rules. Alternatively, you can use something like Optimole.

5. Disable XML-RPC

What is XML-RPC?

XML-RPC on WordPress is an interface that allows apps and other services the ability to talk to your WordPress site to do any of the following:

  • Publish, Edit, or Delete a Post
  • Upload a new file (e.g. an image for a post)
  • List or Edit Comments

Because of these capabilities, it is a potential security vulnerability and open for an attack. Most websites today do not use the capabilities offered via XML-RPC especially since the WP API has been released.

I always recommend simply disabling XML-RPC and block access to xmlrpc.php via .htaccess and/or Cloudflare. Blocking at Cloudflare will reduce load on the server altogether and blocking at .htaccess will reduce load on the processing of the web application (i.e., WordPress).

# BEGIN Custom
# Block XMLRPC - Security.
<Files xmlrpc.php>
order deny,allow
deny from all
</Files>
# END Custom
6. Control the Bots

While resistance may be futile when it comes to bots, you do want to attempt to control them in some way that benefits your site and your audience. If the bots are hitting your site/server so hard that you cannot even serve your true customers, what benefit is for you to have these bots index your site? None because they will penalize you for being down. So preventing bots from hitting your sites primarily addresses 499s and some of the 500s errors as some of these errors are not appropriately identified on SiteGround.

Good Bots: If you are running an eCommerce site, you most likely will want to redirect all bots away from adding items to their cart as identified by ?add-to-cart in the access logs; even good bots like bingbot, googlebot, ahrefs, semrush, and others do this. On SiteGround, to find out what these are simply go to the logs and execute:

zcat domain.com-YYYY-MM-DD.gz | grep "bot" | grep "?add-to-cart"

Please note, that these good bots may also cause a server load on your site. Generally, I would recommend delaying the bots that are not important to you. If googlebot is causing you issues, you can set its delay in the Google Search Console.

User-agent: mj12bot
crawl-delay: 10
User-agent: ahrefs
crawl-delay: 10

Bad Bots: Block all bad bots via .htaccess or via Cloudflare Pro. You can access this Bad Bots list to see all the bad bot user agents.

7. Check the Memory (RAM) and Compute (CPU) Utilization

There are two things you want to enable with your memory and CPU: one via WordPress and the other via SiteGround Dashboard.

  1. Increase WordPress PHP Memory Limit via a define statement in wp-config.php.
    define( 'WP_MEMORY_LIMIT', '1024M' );
  2. Enable CPU and RAM Autoscaling on SiteGround ($$). You can do this in your site's account (URL will be something like https://my.siteground.com/services/hosting/{SOME-CRAZY-ID}/autoscale). In order to prevent all 5XX errors, this must be enabled. Any hosting company that offers auto-scaling, no matter how good or bad it is, a site owner should always elect to use it.
8. Configure Proper Cron Jobs

By default the WordPress cron is run by the wp_cron() function, which is hooked to run on the init hook, which runs on every page load. Disable WP Cron via a define statement in wp-config.php:
define( 'DISABLE_WP_CRON', true );
Then, setup a "true" cron job via the SiteGround SiteTools Dashboard, Pingdom, or Uptime Robot.

9. Use Optimized Plugins

Plugins are great, after all, "There's a plugin for that!" However, plugins can cause massive problems if they are not designed well or use too much compute from the server. So let's take a look at a few plugin categories: Caching Plugins, Security Plugins, BackupPlugins, and Related PostsPlugins.

Caching Plugins: Properly configure either SG Optimizer. If you want something with more control and settings, install and configure WP Rocket. Using WP Rocket renders SG Optimizer useless (because WP Rocket has logic inside the plugin to mitigate SG Optimizer), so you will need to implement memcached yourself, and you will need to be sure to install the No Cache for Administrators Add-on.

Security Plugins: Some security plugins are resource hogs and Wordfence is one of those. If you are using Wordfence, uninstall it, install Limited Login Attempts Reloaded, and use Cloudflare Pro and/or Sucuri as a web application firewall (WAF); both Cloudflare and Sucuri are $20/mo.

Backup Plugins: SiteGround backs-up your files and DB daily which appear to run around 8am UTC time. To verify, go to your Backups in the SiteGround Dashboard via SiteTools > Security > Backups (https://tools.siteground.com/backup-restore) and note the time, which will be in your timezone. So, you really do not need a backup plugin.

Server Plugins: Broken Link Checker is a notoriously awful plugin to run on your server. So I always recommend only using it when you have to use it and then uninstall it. Also for emails, use a plugin like WP SMTP where you can offload your server emails to another service. Not only does this remove server load, it removes server responsibility and ensures your IP doesn't get blacklisted by Google's servers (which are also connected with Gmail analyzing bad email actors).

Related Posts Plugins: Most of these plugins are horrible and hog resources. Simply, do not use and uninstall SEO Auto Links & Related Posts, Yet Another Related Posts Plugin, Similar Posts, & Contextual Related Posts. Instead you want to use a related posts plugin that does all its compute offsite (e.g., Bibblio Related Posts, Jetpack Related Posts, Related Posts for WordPress, Outbrain, Contextly).

For a list of plugins, you should never use, see WP Engine's Disallowed Plugin List. If you are not hosted on WP Engine, then you can ignore Duplicate Behavior Plugins section.

Finally, always remove plugins that you are not using, even if you use them periodically.

10. Optimize the Database

Before you do anything with the database, always create a backup! Always!

wp doctor will check your database (DB) for the size of your auto-loaded options and recommends that it be below 900kb (by default). You can also use wp db optimize to further optimize the DB. To reduce the database, I use WordPress Advanced Database Cleaner to do find orphaned options, etc.

Generally speaking, if the DB is good according to the doctor, then I do nothing. If it is not, then I will take action. However, the WordPress DB can almost always be optimized further. If this is interesting to you, I'd advise you to hire a MySQL DB administrator to help you think this through.

11. Offload All Emails

Many hosting companies offer email hosting with their plans, and SiteGround is one of those companies. SiteGround's email hosting leverages RoundCube for its webmail client. This also creates load on your server resources, and IMHO, always, always offload email hosting to another third party like Google Suite, Microsoft Office 365, ZohoMail, Greatmail or even hey.com.

Also, WordPress sends out emails (e.g., user registration, forgot password, reset password, etc.). WordPress should be configured via WP Mail SMTP to send emails using a third party service like SendGrid, Google Suite, Sendinblue, or Mailgun.

Offloading email from your server will free compute (because monitoring, logging, etc. on those processes are not required) and ensure that your servers are not blacklisted or blocked as a result of some sort of email hack or mass email blasts.

12. Limit AJAX Usage

AJAX creates an awesome user experience, and if improperly implemented can also create a detrimental user experience. When considering AJAX, the tradeoff is simple: UX v. Server Costs/Uptime. In order to determine this, you need some good marketing analytics and probably some A/B testing with server correlations (which is fairly difficult to do correctly).

Themes/Plugins

Many themes and plugins use AJAX to improve the user experience (UX). The problem with WordPress's AJAX is that it is not cached by default because it is accessing wp-admin/admin-ajax.php. Most caching plugins ignore /wp-admin/ in its caching, and generally speaking AJAX functions IMHO should only be for custom, personalized functionality; for all else, use the WP REST API (which is usually cached by default).

WooCommerce

WooCommerce uses AJAX a ton; or rather they can and do by default. WooCommerce has also implemented their own AJAX endpoints instead of the standard admin-ajax.php (good, bad or indifferent). The Add to Cart AJAX (via /?wc-ajax=add_to_cart) isn't so bad, but the Cart Fragments (via /?wc-ajax=get_refreshed_fragments) can be quite detrimental to your server (and even possibly Google PageSpeed Insights; though I am not convinced that it is blocking).

WooCommerce's Cart Fragments is extremely hard on the server as it was built without caching in mind. This is because they hash every cart and every cart item as well as using nonces to remove items (for security purposes most likely). It would be easier for Woo/Automattic to re-create and develop cart fragments with statelessness in mind (than doing it yourself in Woo's software pattern). But I doubt they will fix this. Additionally, most eCommerce sites have the cart on every page, including the About page of a site.

Here is an example after adding one item to the cart:

<div class="widget_shopping_cart_content">
<ul class="woocommerce-mini-cart cart_list product_list_widget">
<li class="woocommerce-mini-cart-item mini_cart_item">
<a aria-label="Remove this item" class="remove remove_from_cart_button" data-cart_item_key="abcdef12345some00hash67890uvwxyz" data-product_id="9999" data-product_sku="GRA000010" href="https://example.com?remove_item=abcdef12345some00hash67890uvwxyz&#038;_wpnonce=f5e62f5167">&times;</a> <a href="https://example.com/product/some-product"><img alt="" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" height="300" sizes="(max-width: 300px) 100vw, 300px" src="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg" srcset="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg 300w, https://assets.example.com/shop/YYYY/MM/####/some-image-150x150.jpg 150w, https://assets.example.com/shop/YYYY/MM/####/some-image-180x180.jpg 180w, https://assets.example.com/shop/YYYY/MM/####/some-image-600x600.jpg 600w, https://assets.example.com/shop/YYYY/MM/####/some-image-100x100.jpg 100w" width="300">Some Product</a> <span class="quantity">1 &times; <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></span>
</li>
</ul>
<p class="woocommerce-mini-cart__total total"><strong>Subtotal:</strong> <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></p>
<p class="woocommerce-mini-cart__buttons buttons"><a class="button wc-forward" href="https://example.com">View cart</a><a class="button checkout wc-forward" href="https://example.com">Checkout</a></p>
</div>
<div class="widget_shopping_cart_content">
<ul class="woocommerce-mini-cart cart_list product_list_widget">
<li class="woocommerce-mini-cart-item mini_cart_item">
<a aria-label="Remove this item" class="remove remove_from_cart_button" data-cart_item_key="abcdef12345some00hash67890uvwxyz" data-product_id="9999" data-product_sku="GRA000010" href="https://example.com?remove_item=abcdef12345some00hash67890uvwxyz&#038;_wpnonce=f5e62f5167">&times;</a> <a href="https://example.com/product/some-product"><img alt="" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" height="300" sizes="(max-width: 300px) 100vw, 300px" src="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg" srcset="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg 300w, https://assets.example.com/shop/YYYY/MM/####/some-image-150x150.jpg 150w, https://assets.example.com/shop/YYYY/MM/####/some-image-180x180.jpg 180w, https://assets.example.com/shop/YYYY/MM/####/some-image-600x600.jpg 600w, https://assets.example.com/shop/YYYY/MM/####/some-image-100x100.jpg 100w" width="300">Some Product</a> <span class="quantity">1 &times; <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></span>
</li>
<li class="woocommerce-mini-cart-item mini_cart_item">
<a aria-label="Remove this item" class="remove remove_from_cart_button" data-cart_item_key="uvwxyz12345some00hash67890abcdef" data-product_id="9998" data-product_sku="GRA000011" href="https://example.com?remove_item=uvwxyz12345some00hash67890abcdef&#038;_wpnonce=f5e62f5167">&times;</a> <a href="https://example.com/product/some-other-product"><img alt="" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" height="300" sizes="(max-width: 300px) 100vw, 300px" src="https://assets.example.com/shop/YYYY/MM/####/some-other-image-300x300.jpg" srcset="https://assets.example.com/shop/YYYY/MM/####/some-other-image-300x300.jpg 300w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-150x150.jpg 150w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-180x180.jpg 180w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-600x600.jpg 600w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-100x100.jpg 100w" width="300">Some Other Product</a> <span class="quantity">1 &times; <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></span>
</li>
</ul>
<p class="woocommerce-mini-cart__total total"><strong>Subtotal:</strong> <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>20.00</span></p>
<p class="woocommerce-mini-cart__buttons buttons"><a class="button wc-forward" href="https://example.com">View cart</a><a class="button checkout wc-forward" href="https://example.com">Checkout</a></p>
</div>

Here is an example after adding a second item to the cart:

<div class="widget_shopping_cart_content">
<ul class="woocommerce-mini-cart cart_list product_list_widget">
<li class="woocommerce-mini-cart-item mini_cart_item">
<a aria-label="Remove this item" class="remove remove_from_cart_button" data-cart_item_key="abcdef12345some00hash67890uvwxyz" data-product_id="9999" data-product_sku="GRA000010" href="https://example.com?remove_item=abcdef12345some00hash67890uvwxyz&#038;_wpnonce=f5e62f5167">&times;</a> <a href="https://example.com/product/some-product"><img alt="" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" height="300" sizes="(max-width: 300px) 100vw, 300px" src="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg" srcset="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg 300w, https://assets.example.com/shop/YYYY/MM/####/some-image-150x150.jpg 150w, https://assets.example.com/shop/YYYY/MM/####/some-image-180x180.jpg 180w, https://assets.example.com/shop/YYYY/MM/####/some-image-600x600.jpg 600w, https://assets.example.com/shop/YYYY/MM/####/some-image-100x100.jpg 100w" width="300">Some Product</a> <span class="quantity">1 &times; <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></span>
</li>
</ul>
<p class="woocommerce-mini-cart__total total"><strong>Subtotal:</strong> <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></p>
<p class="woocommerce-mini-cart__buttons buttons"><a class="button wc-forward" href="https://example.com">View cart</a><a class="button checkout wc-forward" href="https://example.com">Checkout</a></p>
</div>
<div class="widget_shopping_cart_content">
<ul class="woocommerce-mini-cart cart_list product_list_widget">
<li class="woocommerce-mini-cart-item mini_cart_item">
<a aria-label="Remove this item" class="remove remove_from_cart_button" data-cart_item_key="abcdef12345some00hash67890uvwxyz" data-product_id="9999" data-product_sku="GRA000010" href="https://example.com?remove_item=abcdef12345some00hash67890uvwxyz&#038;_wpnonce=f5e62f5167">&times;</a> <a href="https://example.com/product/some-product"><img alt="" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" height="300" sizes="(max-width: 300px) 100vw, 300px" src="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg" srcset="https://assets.example.com/shop/YYYY/MM/####/some-image-300x300.jpg 300w, https://assets.example.com/shop/YYYY/MM/####/some-image-150x150.jpg 150w, https://assets.example.com/shop/YYYY/MM/####/some-image-180x180.jpg 180w, https://assets.example.com/shop/YYYY/MM/####/some-image-600x600.jpg 600w, https://assets.example.com/shop/YYYY/MM/####/some-image-100x100.jpg 100w" width="300">Some Product</a> <span class="quantity">1 &times; <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></span>
</li>
<li class="woocommerce-mini-cart-item mini_cart_item">
<a aria-label="Remove this item" class="remove remove_from_cart_button" data-cart_item_key="uvwxyz12345some00hash67890abcdef" data-product_id="9998" data-product_sku="GRA000011" href="https://example.com?remove_item=uvwxyz12345some00hash67890abcdef&#038;_wpnonce=f5e62f5167">&times;</a> <a href="https://example.com/product/some-other-product"><img alt="" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" height="300" sizes="(max-width: 300px) 100vw, 300px" src="https://assets.example.com/shop/YYYY/MM/####/some-other-image-300x300.jpg" srcset="https://assets.example.com/shop/YYYY/MM/####/some-other-image-300x300.jpg 300w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-150x150.jpg 150w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-180x180.jpg 180w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-600x600.jpg 600w, https://assets.example.com/shop/YYYY/MM/####/some-other-image-100x100.jpg 100w" width="300">Some Other Product</a> <span class="quantity">1 &times; <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>10.00</span></span>
</li>
</ul>
<p class="woocommerce-mini-cart__total total"><strong>Subtotal:</strong> <span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">&pound;</span>20.00</span></p>
<p class="woocommerce-mini-cart__buttons buttons"><a class="button wc-forward" href="https://example.com">View cart</a><a class="button checkout wc-forward" href="https://example.com">Checkout</a></p>
</div>

As you can see get_refreshed_fragments refreshes the entire cart. This makes it nearly impossible to cache well. If the response was broken down to the smaller items (e.g., mini_cart_item) then it can be cached. Otherwise, you cannot cache this due to the hashes and well, you don't want everyone buying the same thing...unless you do (well I guess you may be able to pull this off if you only have one item with no variations, etc.).

So what do you do? If you are struggling with server load and want something simple and fast, just do these 4 things:

  1. Disable AJAX add to cart buttons on archives,
  2. Redirect customers to the cart upon adding items,
  3. Remove cart fragments, and
  4. Remove all cart widgets
To disable AJAX add to cart buttons on archives,

within WooCommerce > Products, beside Add to cart behaviour uncheck Enable AJAX add to cart buttons on archives.

To redirect customers to the cart upon adding items,

within WooCommerce > Products, beside Add to cart behaviour check Redirect to the cart page after successful addition.

To remove cart fragments,

you can use a plugin or custom code within your theme's functions.php (or even better as a basic custom plugin). If you do disable cart fragments, be sure to enable Add to cart behavior's Redirect to the cart page after successful addition.

Within functions.php, you can add one of these snippets:

Disable cart fragments on the front page only:

add_action( 'wp_enqueue_scripts', 'prefix_dequeue_woocommerce_cart_fragments_front_page', 11 );
/**
* Disable WooCommerce Cart Fragments from front page.
*/
function prefix_dequeue_woocommerce_cart_fragments_front_page() {
if ( is_front_page() ) {
wp_dequeue_script( 'wc-cart-fragments' );
}
}

Disable cart fragments everywhere except WooCommerce shop pages:

add_action( 'wp_enqueue_scripts', 'prefix_dequeue_all_woocommerce_styles_scripts_non_shop_pages', PHP_INT_MAX );
/**
* Disable all WooCommerce styles and scripts everywhere except WooCommerce pages.
*/
function prefix_dequeue_all_woocommerce_styles_scripts_non_shop_pages() {
if ( function_exists( 'is_woocommerce' ) ) {
if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() ) {
// Styles
wp_dequeue_style( 'woocommerce-general' );
wp_dequeue_style( 'woocommerce-layout' );
wp_dequeue_style( 'woocommerce-smallscreen' );
wp_dequeue_style( 'woocommerce_frontend_styles' );
wp_dequeue_style( 'woocommerce_fancybox_styles' );
wp_dequeue_style( 'woocommerce_chosen_styles' );
wp_dequeue_style( 'woocommerce_prettyPhoto_css' );
// Scripts
wp_dequeue_script( 'wc_price_slider' );
wp_dequeue_script( 'wc-single-product' );
wp_dequeue_script( 'wc-add-to-cart' );
wp_dequeue_script( 'wc-cart-fragments' );
wp_dequeue_script( 'wc-checkout' );
wp_dequeue_script( 'wc-add-to-cart-variation' );
wp_dequeue_script( 'wc-single-product' );
wp_dequeue_script( 'wc-cart' );
wp_dequeue_script( 'wc-chosen' );
wp_dequeue_script( 'woocommerce' );
wp_dequeue_script( 'prettyPhoto' );
wp_dequeue_script( 'prettyPhoto-init' );
wp_dequeue_script( 'jquery-blockui' );
wp_dequeue_script( 'jquery-placeholder' );
wp_dequeue_script( 'fancybox' );
wp_dequeue_script( 'jqueryui' );
}
}
}

Using a plugin, you can disable Cart Fragments with the Disable Cart Fragments plugin, with TrimPress (which also does many other things), or Perfmatter's Premium Plugin. Some have had success with using WP Menu Cart as an alternative to the default Woo Cart.

The great thing about the Disable Cart Fragments is that you can selectively disable cart fragments.

// Disable via comma-separated page/post IDs.
define('DISABLE_CART_FRAGMENTS', '123,456,789');
Cart Fragments Reimagined Plugin Sponsorship

If one wants to sponsor the creation of a custom plugin with a custom cart fragments implementation in order to cacheability, improve performance and server load, I would be excited for the opportunity. The cost to do this would be the equivalent of having a custom plugin created. I would love to be funded by someone to create an extension for WooCommerce that does this properly and correctly.

WooCommerce Reports

One of the hidden costs of WooCommerce is the async (or on-demand) reporting computations for the WooCommerce MailChimp integration and WooCommerce reports. This can place a massive load on your server. To fix this you need to disable the amount of the user's spend and/or move get_total_spend() post meta data to its own custom DB table. To do both, you can use my plugin or to do just disable the amount of a user's spend:

add_action( 'init', 'stop_heartbeat', 1 );
/**
* Stop the heartbeat altogether.
*/
function stop_heartbeat() {
wp_deregister_script( 'heartbeat' );
}

To limit the heartbeat, install WP Heartbeat Control. You can then set different intervals for each page type or even disable it for some pages.

13. Offload Images/Assets ($$)

Offloading your images from your server does three things:

  1. Reduces load (CPU) on the server.
  2. Reduces bandwidth from the server.
  3. Reduces storage space used on the server.

Space on the server is a hidden cause of 5XX errors. It prevents new cache files from being written, prevents new server processes from being started creating a bottleneck, and potentially increases IOPs and thus death for some requests.

Since hosting companies charge based on bandwidth, CPU/RAM, and storage space, any reductions in these items will save you money and will allow you downgrade your hosting plan (though on SiteGround to reduce SSD space, you actually have to purchase a new server, migrate and cancel the old one which is a lot of fun).

The cost of AWS S3 is quite minimal even beyond the free tier. For example, if you have 100GB of storage and had 1 million requests for those assets (e.g., images, CSS, JS files), it would only cost you about $5 USD/month. AWS S3 is cheaper than any hosting plan's SSD space. If you have a lot of images, then you may want to offload them via a script, which I am happy to do for you!

I highly recommend you do offload your images from your server via WP Offload Media to AWS S3 (or DigitalOcean Spaces or Google Cloud Storage) connected with Cloudflare to a custom domain (e.g., assets.domain.com). It also has an add-on that will pull other site assets to be delivered via S3.

For sites with a lot of media assets, the pricing structure of the WP Offload Media plugin is very, very disappointing, but there are ways around this. If you are hosted on SiteGround, more than likely you are going to need some help to properly offload those images.

14. Analyze the WordPress Actions & Filters

When troubleshooting a WordPress site, I immediately install a set of plugins:

  • Debug Bar + some add-ons.
  • Query Monitor
  • WP Crontrol
  • Transients Manager

You can install these plugins via the WP CLI.

wp install --skip-themes --skip-plugins \
debug-bar \
debug-bar-actions-and-filters-addon \
debug-bar-super-globals \
debug-bar-slow-actions \
health-check \
query-monitor \
wp-crontrol \
transients-manager

These plugins will provide a vast amount information about the site. Of particular focus will be the number of queries, duplicate queries, and query times.

As a side note: Generally, when I am debugging a site, I have almost all the Debug Bar plugins installed. Depending on the issue I install all of or a subset of the following plugins: Debug Bar, Debug Bar Actions & Filters Addon, Debug Bar Console, Debug Bar Constants, Debug Bar Cron, Debug Bar Custom Info, Debug Bar Extender, Debug Bar Hook Log, Debug Bar List Script & Style Dependencies, Debug Bar Localization, Debug Bar Plugin Activation, Debug Bar Post Meta, Debug Bar Post Types, Debug Bar Query Count Alert, Debug Bar Query Tracer, Debug Bar Remote Requests, Debug Bar Rewrite Rules, Debug Bar Roles & Capabilities, Debug Bar Screen Info, Debug Bar Shortcodes, Debug Bar - Sidebars & Widgets, Debug Bar Slow Actions, Debug Bar Super Globals, Debug Bar Taxonomies, Debug Bar Tracer, Debug Bar Transients, and Debug Bar Widgets. Besides these, I also usually install WP Crontrol, Transients Manager, and Query Monitor. If the issue is URL routing, I also like to use Rewrite Rules Inspector.

15. Determine Your Caching Strategy

Finally and most importantly, you want to determine your caching strategy. My philosophy is simply cacheAllTheThings();. To really understand this section, you should have read the architecture section. If you didn't please scroll back up to read it.

First, why cache?

Simply, you should always cache. Ok, but why? As we mentioned above, one primary reason is to offload work from the server. Another reason is performance. If a request is cached at the browser, that experience is super awesome and fast. If a request has to go all the way back to origin, that will always be much longer.

SiteGround uses Google Cloud, so your origins will be in one of these locations. Let's take a concrete example. Let's say your origin is in N. Virginia (Ashburn most likely), and you live in Tyler TX (location of the client). When you make a request, more than likely, it will hit the CDN POP (Point-of-Presence; basically where one of the Cloudflare servers are located) in Dallas or Houston (based on the networks and/or routing enhanced by Cloudflare's Argo) before it goes back to its origin. Getting a response is always faster from Dallas than Virginia, but in all honesty, it will be negligible. Now, let's say that you live in Liverpool UK. The request will be going to N. Virginia via the London Cloudflare POP. So the response is a lot faster from London than it is from Virginia, and that will be noticeable.

So distributed cache via a CDN always improves your site's speed. If it doesn't, then your rules and/or strategy may not be correct.

What is a cache strategy?

A caching strategy has two components, relationship & access:

  1. The relationship between the data, one's origin, the end user, and the caching systems, and
  2. How your data is accessed.

While we could discuss the various technical strategies (technically) like the look-aside, reading-through, writing-through, writing-around, writing-back, let's leave that for more enterprise and high-scale, high-volume sites that usually have teams or agencies building and maintaining their sites on enterprise-grade clouds (e.g., Google, AWS, Azure or IBM), and let's focus on the simple toolset that is offered via SiteGround.

With every request, there are three basic levels of cache (thinking linearly):

  1. Client Caching (AKA Browser Caching)
  2. Intermediate Caching (AKA CDN or Edge Caching)
  3. Origin Caching (AKA Backend Caching)

So it looks something like this:

sequenceDiagram participant Client participant Intermediate participant Origin Client->>Client: Check Cache alt Client IS Cached note right of Client: Got Cache<br>Do nothing else No Client Cache Exists Client->>Intermediate: Cache Miss<br>Send Request Intermediate->>Intermediate: Check Cache alt Intermediate IS Cached Intermediate->>Client: Cached Response else No Intermediate Cache Exists Intermediate->>Origin: Cache Miss<br>To Origin Origin->>Origin: Check Cache alt Origin IS Cached Origin->>Intermediate: Cached Response else No Origin Cache Exists Origin->>Origin: Process Request Origin->>Intermediate: The Response end end Intermediate->>Client: Response end

Client Caching includes browser cache, local storage, application storage, service workers, web workers, etc. If something is cached here, there is very low latency and nothing leaves the client (e.g., browser, device).

Intermediate Caching includes any cache between the client and origin (think WordPress). In our case, this includes Cloudflare CDN. CDNs are generally read/write through caching that can also have look aside caches (which are usually only used at scale). The great thing about intermediate caching, especially CDNs, is that we can always override and ignore origin caching requests.

Backend/Origin Caching includes any cache that the backend uses for its services. In our case, this would include memcached, in-memory cache, and MySQL DB cache.

In this flow, the client would be a Chrome Browser on a desktop device or Safari Browser on a mobile device. The intermediate would be Cloudflare CDN and/or Sucuri/Cloudflare WAF. The origin would be NGINX, WordPress, memcached, and MySQL.

With caching, there are decisions made at each point:

  1. Do we want to cache at this location?
  2. What do we want to cache?
  3. How long should it be cached?

Caching can be dictated at every stage. At every stage, one can choose to ignore the caching recommendation or request made by the client request or origin response.

Consider this conceptual example: The browser could make a request saying, "Please don't send me anything cached," and the other client caches, intermediate cache, and backend cache would all decide whether to honor that request or not. Likewise, origin could respond saying, "Don't cache this," and the intermediate cache and client caches would decide whether to honor that request or not.

Now for a concrete example: By default, WordPress disables all cache for users who are logged into the site. In other words, every response sent out by WordPress for any user that is logged into the site, origin is saying, "Don't cache this." At Cloudflare, we can choose to ignore this and cache stuff regardless or we can do nothing and let WordPress have its way (even to its own detriment).

So in determining one's caching strategy, you have to determine where, what, and for how long you want to cache.

Where do we want to cache?

Where is the best place to cache stuff? Is it at origin? at the network edge (CDN)? or on the client? This somewhat depends on the what we are caching and why we are caching it. Generally speaking, we want to cache at every level.

You have the greatest control over your origin cache as that is your server. As you move closer to the end user, you have less control. CDNs control the shortest amount of time you are able to expire your cache based on your subscription level. Shortest Cache Times (also known as TTL - Time to Live) available by Cloudflare plans are:

  • Free Plan: 2 hours
  • Pro: 1 hour
  • Business: 30 minutes
  • Enterprise: 30 seconds

While you are able to set the browser cache, browsers have settings that allow users to ignore that cache, and users can clear their cache at any time. So the client cache is the least dependable cache there is.

What do we want to cache?

There are 2 basic types of assets: static and dynamic. Dynamic content can then be broken into three different types: general, segmented, and personalized.

  • Static Assets (e.g., images, JS, CSS, HTML, JSON; think logo, jQuery, theme styles)
  • Dynamic Assets (e.g., HTML, JSON)
    • General Assets (e.g., HTML, JSON; think feeds, home page)
    • Segmented Assets (e.g., HTML; think pages by role, WP Admin)
    • Personalized Assets (e.g., HTML; think store cart, checkout page, user profile)

Now let's say you have an eCommerce site. By default those users who are logged-in purchasing or even just browsing your site are all bypassing your cache. So if you set to cache your entire site, you are now caching personalized content (e.g., the cart, checkout, invoice, account, etc.). So you have to exempt some pages from the cache, e.g., /cart/, /checkout/, /my-account/*, and /wp-admin/* pages.

To cache the WordPress admin or pages when users are logged into the site or their account, we can use (a.) custom code via nocache_headers and rest_send_nocache_headers, (b.) a setting with WP Rocket (see Caching Plugins above), or (c.) configuration in Cloudflare to cache the admin.

For how long do we want to cache?

The length of time of cache depends on the location of the cache and the location of the asset. In my approach of cacheAllTheThings();, even a short cache time (e.g., 30s or 1min) can help you out tremendously. Normally, you want to cache child items longer than parent items. For example, JS on a HTML page is cached longer than the HTML page itself as the JS asset is a child of the HTML asset.

Let's use a couple way-too-simple examples. As previously explained, we have three levels of cache: origin, edge, and browser.

Increasing Example:
  1. Origin Cache: 20min
  2. Edge Cache: 10min
  3. Browser Cache: 5min

When the browser cache expires (after 5min), it will get the same asset from the edge. When the browser cache expires again (after 10min), it will get the same asset from the origin setting the edge cache. When the browser cache expires again (after 15min), it will get the same asset from the edge, again. When the browser cache expires again (after 20min), it will get a new asset from origin.

sequenceDiagram participant Client participant Edge participant Origin note over Client, Origin: Client: 5min Cache, Edge: 10min Cache, Origin: 20min Cache # 1st Request note over Client: 1st Request @ 7:00 #User->>Client: Request @ 7:00 note over Client: Cache MISS Client->>Edge: Request @ 7:00 note over Edge: MISS Edge->>Origin: Request @ 7:00 note over Origin: MISS Origin->>Client: Origin Fresh Response @ 7:00<br>Asset Cached @ Client, Edge, & Origin # 2nd Request note over Client: 2nd Request @ 7:02 #User->>Client: Request @ 7:02 note over Client: HIT Client->>Client: Client Cached Response @ 7:02 # 3rd Request note over Client: 3rd Request @ 7:05 #User->>Client: Request @ 7:05 note over Client: MISS Client->>Edge: Request @ 7:05 note over Edge: HIT Edge->>Client: Edge Cached Response @ 7:05<br>Asset Cached @ Client # 4th Request note over Client: 4th Request @ 7:10 #User->>Client: Request @ 7:10 note over Client: MISS Client->>Edge: Request @ 7:10 note over Edge: MISS Edge->>Origin: Request @ 7:10 note over Origin: HIT Origin->>Client: Origin Cached Response @ 7:10<br>Asset Cached @ Client & Edge # 6th Request note over Client: 6th Request @ 7:15 #User->>Client: Request @ 7:15 note over Client: MISS Client->>Edge: Request @ 7:15 note over Edge: HIT Edge->>Client: Edge Cached Response @ 7:15<br>Asset Cached @ Client # 7th Request note over Client: 7th Request @ 7:20 Client->>Edge: Request @ 7:20 note over Edge: MISS Edge->>Origin: Request @ 7:20 note over Origin: MISS Origin->>Client: Origin Fresh Response @ 7:20<br>Asset Cached @ Client, Edge, & Origin
Decreasing Example:
  1. Origin Cache: 5min
  2. Edge Cache: 10min
  3. Browser Cache: 15min

If we have decreasing length of time as we move to the origin, when the browser cache expires (after 15min), it will get a new asset from the origin immediately bypassing edge cache the first time the edge is hit. If the edge cache is warmed (say by another client), then when the browser cache expires, it will get a new asset from the edge cache.

sequenceDiagram participant Client participant Edge participant Origin note over Client, Origin: Client: 15min Cache, Edge: 10min Cache, Origin: 5min Cache # 1st Request note over Client: 1st Request @ 7:00 note over Client: Cache MISS Client->>Edge: Request @ 7:00 note over Edge: MISS Edge->>Origin: Request @ 7:00 note over Origin: MISS Origin->>Client: Origin Fresh Response @ 7:00<br>Asset Cached @ Client, Edge, & Origin # 2nd Request note over Client: 2nd Request @ 7:10 #User->>Client: Request @ 7:10 note over Client: HIT Client->>Client: Client Cached Response @ 7:10 # 3rd Request note over Client: 3rd Request @ 7:15 Client->>Edge: Request @ 7:15 note over Edge: MISS Edge->>Origin: Request @ 7:15 note over Origin: MISS Origin->>Client: Origin Fresh Response @ 7:15<br>Asset Cached @ Client, Edge, & Origin
Pyramid Example
  1. Origin Cache: 5min
  2. Edge Cache: 10min
  3. Browser Cache: 5min

If we have a pyramid length of time, when the browser cache expires (after 5min), it will get the same, cached asset from the edge. When the browser cache expires again (after 10min), it will get a fresh asset from origin immediately.

sequenceDiagram participant Client participant Edge participant Origin note over Client, Origin: Client: 5min Cache, Edge: 10min Cache, Origin: 5min Cache # 1st Request note over Client: 1st Request @ 7:00 note over Client: Cache MISS Client->>Edge: Request @ 7:00 note over Edge: MISS Edge->>Origin: Request @ 7:00 note over Origin: MISS Origin->>Client: Origin Fresh Response @ 7:00<br>Asset Cached @ Client, Edge, & Origin # 2nd Request note over Client: 2nd Request @ 7:02 #User->>Client: Request @ 7:02 note over Client: HIT Client->>Client: Client Cached Response @ 7:02 # 3rd Request note over Client: 3rd Request @ 7:05 #User->>Client: Request @ 7:05 note over Client: MISS Client->>Edge: Request @ 7:05 note over Edge: HIT Edge->>Client: Edge Cached Response @ 7:05<br>Asset Cached @ Client # 4th Request note over Client: 4th Request @ 7:10 #User->>Client: Request @ 7:10 note over Client: MISS Client->>Edge: Request @ 7:10 note over Edge: MISS Edge->>Origin: Request @ 7:10 note over Origin: MISS Origin->>Client: Origin Fresh Response @ 7:10<br>Asset Cached @ Client & Edge
Help Determining Your Caching Strategy

Now that was way too much information! Did I confuse you more? Hopefully not. I am more than happy to consult with you on your caching strategy.

Hire Me!
Hire Me on Codeable

Finally, one key implementation is converting your site to a Progressive Web App (PWA). PWAs are websites the deliver an app-like experience, so they must be fast, reliable, engaging and responsive. Why spend the time investing converting your site into a PWA?

  1. PWA's run in the mobile browser.
  2. Not subject to app store reviews and approval processes.
  3. Can be launched from the home screen of mobile devices.
  4. Accessible across all platforms.
  5. Work offline.
  6. Increase your visits by 2-4x.
  7. Increase your conversions by 80%.
  8. Improve performance and decrease load time by 10-50%.
  9. Reduce your bounce rate.

All of these and more can be seen by reading the various Google Case Studies; namely, BookMyShow, ele.me, Jumia, Mynet, & Washington Post.

The best implementations are always a custom implementation, but you can get mostly there through one of these plugins:

  • PWA
  • PWA for WP & AMP
  • PWA4WP
  • Progressive WordPress
  • Super Progressive Web Apps
  • Add to Homescreen + Offline Shell + Offline Content + Web Push
  • From CodeCanyon:
    • Instantify
    • WordPress Mobile Soft PWA
    • PWACommerce
    • Progressive Web App (PWA) & Push Notifications for WordPress & WooCommerce
    • Simple PWA
    • PWA for WooCommerce
    • Progressive Web Apps For WordPress

Summary

Sounds like a lot, but by doing all of these things, you now have a server that is truly serving your end customers. More than likely you won't want to do every little thing I recommend, but doing even a subset of these will improve your server's performance. There is no guarantee that doing only a subset of these will remove all 5XX errors from your site, but doing all of them will eradicate nearly 99% of those errors leaving only the errors that are truly a result of insufficient scale.

If you read this entire article, kudos! I really commend you as even writing this, I didn't want to read this entire article.

Hire Me!
Hire Me on Codeable

For your convenience, here is the outline of the entire article:

  1. Understanding 500 Errors
  2. Solving 500 Errors
  3. Understanding 499, 502, 503, & 504 Errors
  4. Understanding the Web Hosting Architecture
  5. What to Expect from Managed Hosting Support
  6. 15 Ways of Solving 499, 502, 503, & 504 Errors
    1. Analyze the Logs for 499s, 500s, 502s, 503s, & 504s
    2. Check PHP Version
    3. Run WP CLI’s Doctor Command
    4. Properly Configure a CDN
    5. Disable XMLRPC
    6. Control Bots
    7. Check Memory & Compute Utilization
    8. Configure Proper Cron Jobs
    9. Use Optimized Plugins
    10. Optimize the Database
    11. Offload All Emails
    12. Limit AJAX Usage
    13. Offload Images/Assets
    14. Analyze WP Actions & Filters
    15. Determine Cache Strategy
  7. Summary

Written by Travis Smith · Categorized: WordPress

Jan 09 2019

Hiding an User in the WordPress Admin

Sometimes it is good to hide a user from other users so that user won't be deleted or modified accidentally by another administrator. This especially good for hiding the hosting user or any machine/automation user.

Setup

So let's setup a plugin main file. Within wp-content/mu-plugins, add a file, hide-user.php.

<?php
/**
* Plugin Name: WPS User
* Plugin URI: https://wpsmith.net
* Description: User management.
* Author: Travis Smith <[email protected]>
* Author URI: https://wpsmith.net
* Text Domain: wps
* Domain Path: /languages
* Version: 0.1.0
*/
/**
* Plugin main file.
*
* @package WPS\Plugins\HideUser
* @author Travis Smith <[email protected]>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\HideUser;
view raw hide-user.php hosted with ❤ by GitHub

Now that we are setup, we can hide the user one of two ways:

  1. Using Composer in a Single File
  2. Putting All Code in a Single File

Mu-Plugin Using Composer

Create a composer.json file where we can require my user package (wpsmith/user) via composer.

{
"name": "wpsmith/hide-user",
"description": "Hides user in WordPress Admin.",
"type": "project",
"license": "GPLv2+",
"authors": [
{
"name": "Travis Smith",
"email": "[email protected]"
}
],
"minimum-stability": "dev",
"require": {
"wpsmith/user": "dev-master"
}
}
view raw composer.json hosted with ❤ by GitHub

Once we have this file, we can do a composer install which will install our packages into a folder called vendor automagically.

In the plugin file (hide-user.php), we need to require the composer autoloader.

namespace WPS\Plugins\HideUser;
// Require the composer autoloader.
require 'vendor/autoload.php';
view raw hide-user-composer.php hosted with ❤ by GitHub

Finally, we add the simple code to hide the user(s):

// Use the User Package & hide hidden_user1 & hidden_user2.
\WPS\User\HideUser::get_instance( array(
'hidden_user1',
'hidden_user2',
) );
view raw hide-user-composer.php hosted with ❤ by GitHub

Mu-Plugin with All Code

In the plugin file (hide-user.php), we need to add a hook into the pre_user_query.

namespace WPS\Plugins\HideUser;
add_action( 'pre_user_query', 'WPS\Plugins\HideUser\pre_user_query' );
/**
* Remove user from all user queries.
* @global \wpdb $wpdb WordPress database abstraction object.
*
* @param \WP_User_Query $user_search The current WP_User_Query instance,
* passed by reference.
*/
function pre_user_query( $user_search ) {
/**
* @var \WP_User $current_user \WP_User object for the current user.
*/
$current_user = wp_get_current_user();
if ( ! $current_user->exists() ) {
return;
}
// If the current user is not hidden_user1, let's remove hidden_user1.
if ( 'hidden_user1' !== $current_user->user_login ) {
global $wpdb;
// Now remove our hidden_user1 from the user query.
$user_search->query_where = str_replace(
'WHERE 1=1',
"WHERE 1=1 AND {$wpdb->users}.user_login != 'hidden_user1'",
$user_search->query_where
);
}
}
view raw hide-user-single-file.php hosted with ❤ by GitHub

 

Credits: Image From Kristina Alexanderson.

Written by Travis Smith · Categorized: Snippets

Jan 05 2019

Custom Rewrite Rules for Custom Post Types and Taxonomies

Getting custom URL structures that differ from standard WordPress convention can be tough, even confusing.

When you register a custom post type, you get these URLs:

  • Singular Page: https://domain.com/cpt-slug/post-name-slug/
  • Paginated Singular Pages: https://domain.com/cpt-slug/post-name-slug/2/
  • Archive Page: https://domain.com/cpt-archive-slug/
  • Paginated Archive Pages: https://domain.com/cpt-slug/post-name-slug/page/2/
  • Trackback Page: https://domain.com/cpt-slug/post-name-slug/trackback/
  • Feed Pages: https://domain.com/cpt-slug/post-name-slug/feed/rss/, https://domain.com/cpt-slug/post-name-slug/rss/
  • Comment Page: https://domain.com/cpt-slug/post-name-slug/comment-page/
  • Embed Page: https://domain.com/cpt-slug/post-name-slug/embed/
  • Attachment:
    • Attachment Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/
    • Attachment Trackback Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/trackback/
    • Attachment Feed Pages: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/feed/rss/ , https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/rss/
    • Attachment Comment Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/comment-page/
    • Attachment Embed Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/embed/

When you register a custom taxonomy, you get these URLs:

  • Term Archive Page: https://domain.com/custom-taxonomy-slug/term-slug/
  • Paginated Archive Pages: https://domain.com/custom-taxonomy-slug/term-slug/page/2/
  • Embed Page: https://domain.com/custom-taxonomy-slug/term-name-slug/embed/
  • Feed Pages: https://domain.com/custom-taxonomy-slug/term-name-slug/feed/rss/, https://domain.com/tax-slug/term-name-slug/rss/

So, if you want anything else, you need to do something custom. For example, if you want date archive pages with your custom post type or if you want to introduce a taxonomy term in the URL, you must add some custom code.

Setup

So let's setup a plugin main file. First create a folder called post-type-taxonomy-rewrite.

<?php
/**
* Plugin Name: WPS Post Type Taxonomy Rewrite
* Plugin URI: https://wpsmith.net
* Description: Rewrite for {taxonomy}/{postname} rewrites.
* Author: Travis Smith <[email protected]>
* Author URI: https://wpsmith.net
* Text Domain: wps-rewrite
* Domain Path: /languages
* Version: 0.1.0
*/
/**
* Plugin main file.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <[email protected]>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
view raw post-type-taxonomy-rewrite-resources.php hosted with ❤ by GitHub

Create a composer.json file where we can require my rewrite package (wpsmith/rewrite) via composer.

{
"name": "wpsmith/post-type-taxonomy-rewrite",
"description": "Rewrite for {taxonomy}/{postname} rewrites.",
"type": "project",
"license": "GPLv2+",
"authors": [
{
"name": "Travis Smith",
"email": "[email protected]"
}
],
"minimum-stability": "dev",
"require": {
"wpsmith/rewrite": "dev-master"
}
}
view raw composer.json hosted with ❤ by GitHub

Once we have this file, we can do a composer install which will install our packages into a folder called vendor.

Now in the plugin file (post-type-taxonomy-rewrite.php), we need to require the composer autoloader.

// Add autoloader.
require 'vendor/autoload.php';
view raw post-type-taxonomy-rewrite-videos.php hosted with ❤ by GitHub

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- vendor/

Custom Post Type / Taxonomy Example

For example, let's say we want this pattern(domain.com/{post-type-slug}/{term}/{postname}):

  • Single Custom Post Type Post: https://domain.com/resource/tutorials/setting-up-something
  • Custom Taxonomy Archive: https://domain.com/resource/tutorials/
  • Custom Post Type Archive: https://domain.com/resource/

Now, I have written a class that you can use to make this extremely easy!

Setup

Now, create a resources.php file to contain our post type and taxonomy registration code for resource and resource-type.

<?php
/**
* Post Type and Taxonomy Registration.
*
* Resource Post Type & Resource Type Taxonomy.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <[email protected]>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_tax_resource_type' );
/**
* Register the Resource Type taxonomy
*
*/
function register_tax_resource_type() {
$labels = array(
'name' => __( 'Resource Types', 'wps' ),
'singular_name' => __( 'Type', 'wps' ),
'search_items' => __( 'Search Types', 'wps' ),
'popular_items' => __( 'Popular Types', 'wps' ),
'all_items' => __( 'All Types', 'wps' ),
'parent_item' => __( 'Parent Type', 'wps' ),
'parent_item_colon' => __( 'Parent Type:', 'wps' ),
'edit_item' => __( 'Edit Type', 'wps' ),
'update_item' => __( 'Update Type', 'wps' ),
'add_new_item' => __( 'Add New Type', 'wps' ),
'new_item_name' => __( 'New Type', 'wps' ),
'separate_items_with_commas' => __( 'Separate Types with commas', 'wps' ),
'add_or_remove_items' => __( 'Add or remove Types', 'wps' ),
'choose_from_most_used' => __( 'Choose from most used Types', 'wps' ),
'menu_name' => __( 'Types', 'wps' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'show_in_nav_menus' => true,
'show_ui' => true,
'show_tagcloud' => false,
'hierarchical' => true,
'rewrite' => array( 'slug' => 'resource-type', 'with_front' => false ),
'query_var' => true,
'show_admin_column' => true,
);
register_taxonomy( 'resource_type', array( 'resource' ), $args );
}
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_cpt_resource' );
/**
* Register the custom post type
*
* @since 1.2.0
*/
function register_cpt_resource() {
$labels = array(
'name' => __( 'Resources', 'wps' ),
'singular_name' => __( 'Resource', 'wps' ),
'add_new' => __( 'Add New', 'wps' ),
'add_new_item' => __( 'Add New Resource', 'wps' ),
'edit_item' => __( 'Edit Resource', 'wps' ),
'new_item' => __( 'New Resource', 'wps' ),
'view_item' => __( 'View Resource', 'wps' ),
'search_items' => __( 'Search Resources', 'wps' ),
'not_found' => __( 'No Resources found', 'wps' ),
'not_found_in_trash' => __( 'No Resources found in Trash', 'wps' ),
'parent_item_colon' => __( 'Parent Resource:', 'wps' ),
'menu_name' => __( 'Resources', 'wps' ),
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'revisions', 'author', 'comments', 'discussion', 'page-attributes' ),
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'has_archive' => true,
'query_var' => true,
'can_export' => true,
'rewrite' => array( 'slug' => 'resource', 'with_front' => false ),
'menu_icon' => 'dashicons-format-aside',
);
register_post_type( 'resource', $args );
}
view raw resources.php hosted with ❤ by GitHub

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- resources.php
|- vendor/

Doing the Rewrites

Then let's create a function:

/**
* Does the resources rewrites.
*/
function do_resources() {
// Require post types file.
require_once 'resources.php';
// Do the rewrite for resources.
try {
// Create the rewrite object connecting the post type and taxonomy.
$resource_resource_type = new \WPS\Rewrite\PostTypeByTaxonomy( array(
'post_type' => 'resource',
'taxonomy' => 'resource_type',
) );
// Set the order of the rewrite to `%post_type%/%term%`. Defaults to `%term%/%post_type%`.
$resource_resource_type->set_order( [
'%post_type%',
'%term%',
] );
// Add all the rewrites. This includes the main, embed, feed, pagination, and date URLs.
$resource_resource_type->add_all_rewrites();
} catch ( \Exception $e ) {
// do nothing right now.
// @todo Maybe do something.
}
}
// Do it!
do_resources();
view raw post-type-taxonomy-rewrite-resources.php hosted with ❤ by GitHub

That's it!

On Activation

Because this is a plugin, we need to flush the rewrite rules when the plugin is activated. So in the plugin file (post-type-taxonomy-rewrite.php), let's flush the rules.

register_activation_hook( __FILE__, '\WPS\Plugins\Rewrite\PostTypeTaxonomy\on_activation' );
/**
* Flush rules on activation.
*/
function on_activation() {
// Registering Resources.
register_tax_resource_type();
register_cpt_resource();
// Flush the rules.
flush_rewrite_rules();
}
view raw post-type-taxonomy-rewrite-resources.php hosted with ❤ by GitHub

Taxonomy / Custom Post Type Example

What if you have a hierarchical post type that you wanted to use with this pattern (domain.com/{term}/{post-type-slug}/{postname}):

  • Single Parent Custom Post Type Post: https://domain.com/email/landing-page/selling-something
  • Single Child Custom Post Type Post: https://domain.com/email/landing-page/selling-parent/selling-something
  • Custom Taxonomy Archive: https://domain.com/email/

Setup

Now, create a landing-pages.php file to contain our post type and taxonomy registration code for landing_page and campaign_type (this replaces resources.php).

<?php
/**
* Post Type and Taxonomy Registration.
*
* Landing Page Post Type & Campaign Type Taxonomy.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <[email protected]>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_cpt_landing_pages' );
/**
* Register the Landing Pages Post Type.
*/
function register_cpt_landing_pages() {
$labels = array(
'name' => __( 'Landing Pages', 'wps' ),
'singular_name' => __( 'Landing Page', 'wps' ),
);
$args = array(
'label' => __( 'Landing Pages', 'wps' ),
'labels' => $labels,
'description' => '',
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'delete_with_user' => false,
'show_in_rest' => true,
'rest_base' => '',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'has_archive' => false,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'exclude_from_search' => true,
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => array( 'slug' => 'landing-page', 'with_front' => true ),
'query_var' => true,
'menu_position' => 5,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'taxonomies' => array( 'campaign_type' ),
'menu_icon' => 'dashicons-analytics',
);
register_post_type( 'landing_page', $args );
}
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_tax_campaign_type' );
/**
* Register the Campaign Type Custom Taxonomy.
*/
function register_tax_campaign_type() {
$labels = array(
'name' => __( 'Campaign Types', 'wps' ),
'singular_name' => __( 'Campaign Type', 'wps' ),
);
$args = array(
'label' => __( 'Campaign Types', 'wps' ),
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'hierarchical' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'campaign_type', 'with_front' => true, ),
'show_admin_column' => false,
'show_in_rest' => true,
'rest_base' => 'campaign_type',
'rest_controller_class' => 'WP_REST_Terms_Controller',
'show_in_quick_edit' => false,
);
register_taxonomy( 'campaign_type', array( 'landing_page' ), $args );
// Make taxonomy single term only.
// 'type' can be 'radio' or 'select' (default: radio)
new \WPS\Taxonomies\SingleTermTaxonomy( 'campaign_type', array( 'landing_page' ), 'radio' );
}
view raw landing-pages.php hosted with ❤ by GitHub

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- landing-pages.php
|- vendor/

With Landing Pages, I added an additional class SingleTermTaxonomy to ensure that the taxonomy, campaign_type would always have only one term selected. In order to use this additional class, composer.json needs to be updated.

{
"name": "wpsmith/post-type-taxonomy-rewrite",
"description": "Rewrite for {taxonomy}/{postname} rewrites.",
"type": "project",
"license": "GPLv2+",
"authors": [
{
"name": "Travis Smith",
"email": "[email protected]"
}
],
"minimum-stability": "dev",
"require": {
"wpsmith/single-term-taxonomy": "dev-master",
"wpsmith/rewrite": "dev-master"
}
}
view raw composer-landing-pages.json hosted with ❤ by GitHub

SingleTermTaxonomy is not required, and if you do not wish to use this class, then simply delete lines 88-89.

new \WPS\Taxonomies\SingleTermTaxonomy( 'campaign_type', array( 'landing_page' ), 'radio' );
}
view raw landing-pages.php hosted with ❤ by GitHub

Doing the Rewrites

Then let's create a function:

/**
* Does the landing-pages rewrites.
*/
function do_landing_pages() {
// Require post types file.
require_once 'landing-pages.php';
// Do the rewrite for landing pages.
try {
// Create the rewrite object connecting the post type and taxonomy.
$landing_page_campaign_type = new \WPS\Rewrite\PostTypeByTaxonomy( array(
'post_type' => 'landing_page',
'taxonomy' => 'campaign_type',
) );
// Set the order of the rewrite to `%post_type%/%term%`. Defaults to `%term%/%post_type%`.
$landing_page_campaign_type->set_order( [
'%term%',
] );
// Add the feed/embed rewrite URLs.
$landing_page_campaign_type->add_embed_rewrites();
$landing_page_campaign_type->add_feed_rewrites();
} catch ( \Exception $e ) {
// do nothing right now.
// @todo Maybe do something.
}
}
// Do it!
do_landing_pages();
view raw post-type-taxonomy-rewrite-landing-pages.php hosted with ❤ by GitHub

That's it!

On Activation

Because this is a plugin, we need to flush the rewrite rules when the plugin is activated. So in the plugin file (post-type-taxonomy-rewrite.php), let's flush the rules.

register_activation_hook( __FILE__, '\WPS\Plugins\Rewrite\PostTypeTaxonomy\on_activation' );
/**
* Flush rules on activation.
*/
function on_activation() {
// Register Landing Pages.
register_cpt_landing_pages();
register_tax_campaign_type();
// Flush the rules.
flush_rewrite_rules();
}
view raw post-type-taxonomy-rewrite-landing-pages.php hosted with ❤ by GitHub

Prefix / Custom Post Type Example

What if you have a post type that you wanted to add a prefix slug, the date archives, and so use with this pattern (domain.com/{prefix}/{post-type-slug}/{postname}):

  • Single Custom Post Type Post: https://domain.com/video/some-music-video
  • Date Archives: https://domain.com/video/2019, https://domain.com/video/2019/01, and https://domain.com/video/2019/01/05
  • Custom Post Type Archive: https://domain.com/videos/

Setup

Now, create a videos.php file to contain our post type and taxonomy registration code for video (this replaces resources.php or landing-pages.php).

<?php
/**
* Post Type and Taxonomy Registration.
*
* Video Post Type.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <[email protected]>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_cpt_videos' );
/**
* Register the Video Post Type.
*/
function register_cpt_videos() {
$labels = array(
'name' => __( 'Videos', 'wps' ),
'singular_name' => __( 'Video', 'wps' ),
);
$args = array(
'label' => __( 'Videos', 'wps' ),
'labels' => $labels,
'description' => '',
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'delete_with_user' => false,
'show_in_rest' => true,
'rest_base' => '',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'has_archive' => 'videos',
'show_in_menu' => true,
'show_in_nav_menus' => true,
'exclude_from_search' => true,
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => array( 'slug' => 'video', 'with_front' => true ),
'query_var' => true,
'menu_position' => 5,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'menu_icon' => 'dashicons-video-alt3',
);
register_post_type( 'video', $args );
}
view raw videos.php hosted with ❤ by GitHub

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- videos.php
|- vendor/

Doing the Rewrites

Then let's create a function:

/**
* Does the videos rewrites.
*/
function do_videos() {
// Require post types file.
require_once 'videos.php';
// Do the rewrite for landing pages.
try {
// Create the rewrite object connecting the post type and taxonomy.
$videos = new \WPS\Rewrite\PostTypeRewrite( array(
'post_type' => 'video',
) );
// Add all the rewrites. This includes the main, embed, feed, pagination, and date URLs.
$videos->add_all_rewrites();
} catch ( \Exception $e ) {
// do nothing right now.
// @todo Maybe do something.
}
}
// Do it!
do_videos();
view raw post-type-taxonomy-rewrite-videos.php hosted with ❤ by GitHub

That's it!

On Activation

Because this is a plugin, we need to flush the rewrite rules when the plugin is activated. So in the plugin file (post-type-taxonomy-rewrite.php), let's flush the rules.

register_activation_hook( __FILE__, '\WPS\Plugins\Rewrite\PostTypeTaxonomy\on_activation' );
/**
* Flush rules on activation.
*/
function on_activation() {
// Register Videos.
register_cpt_videos();
// Flush the rules.
flush_rewrite_rules();
}
view raw post-type-taxonomy-rewrite-videos.php hosted with ❤ by GitHub

Wrap-Up

If this was helpful, you can find all the code either in the gist or the Github repo (https://github.com/wpsmith/post-type-taxonomy-rewrite). Please feel free to let me know if there is anything I missed!

Written by Travis Smith · Categorized: Tutorials

Jun 21 2018

WordPress JavaScript Manager Native Functions

WordPress has native PHP JavaScript management functions. Using these functions are better than manually adding <script> tags in the <head> or footer of the page. In this article, we will discuss:

  1. PHP Constants for Debugging JavaScript
  2. Registering and Deregistering Scripts
  3. Enqueueing and Dequeueing Scripts
  4. Localizing Scripts
  5. Initializing Scripts
  6. Modifying Script Tags on Print

JavaScript in WordPress

PHP Constants for Debugging JavaScript

Most WordPress developers are familiar with WP_DEBUG and know how to use that to debug WordPress. WP_DEBUG is designed as a global for turning debugging on and off, but WP_DEBUG has no affect on JavaScript files. Fewer developers know about SCRIPT_DEBUG. While WP_DEBUG helps debug the PHP code, SCRIPT_DEBUG helps with JavaScript. SCRIPT_DEBUG will output the normal, non-minified version of the JavaScript files. For example, with SCRIPT_DEBUG will output wp-embed.js while normally WordPress will output wp-embed.min.js. You can use this constant to output minified and non-minified your plugin/theme JavaScript files dynamically. For example:

<?php
namespace MyPrefix\MyPluginOrTheme;
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// JavaScript File
$filename = "myscript{$suffix}.js";
// CSS File
$filename = "myscript{$suffix}.css";
view raw wp-script-debug-js-css.php hosted with ❤ by GitHub

Alternatively, you could use a function that will do this for you:

<?php
namespace MyPrefix\MyPluginOrTheme;
/**
* Gets the proper JavaScript filename based on SCRIPT_DEBUG.
*
* @param string $filename Filename without the extension.
*
* @return string JavaScript filename.
*/
function get_js_filename( $filename ) {
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
return "{$filename}{$suffix}.js";
}
$filename = get_js_filename( 'myscript' );
view raw wp-script-debug-get_js_filename.php hosted with ❤ by GitHub

Or, you can use a constant or multiple constants:

<?php
namespace MyPrefix\MyPluginOrTheme;
// Define MULTIPLE Suffixes
define( 'JS_SUFFIX', ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.js' : '.min.js' ) );
define( 'CSS_SUFFIX', ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.css' : '.min.css' ) );
// JavaScript file
$filename = 'myscript' . JS_SUFFIX;
// CSS file
$filename = 'mystyle' . CSS_SUFFIX;
// Define a SINGLE Suffix
define( 'SCRIPT_SUFFIX', ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min' ) );
// JavaScript file
$filename = 'myscript' . SCRIPT_SUFFIX . '.js';
// CSS file
$filename = 'mystyle' . SCRIPT_SUFFIX . '.css';
view raw wp-script-debug-constants.php hosted with ❤ by GitHub
]

Or (just for fun) create a function for the suffix only:

<?php
namespace MyPrefix\MyPluginOrTheme;
/**
* Gets the proper JavaScript filename based on SCRIPT_DEBUG.
*
* @param string $filename Filename without the extension.
*
* @return string JavaScript filename.
*/
function get_suffix( $filename ) {
static $suffix;
if ( null !== $suffix ) {
return $suffix;
}
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
return $suffix;
}
$filename = 'myscript' . get_suffix() . 'js';
view raw wp-script-debug-get_suffix.php hosted with ❤ by GitHub

If SCRIPT_DEBUG is on, the filename will be my-js-file.js while if SCRIPT_DEBUG is false, the filename will be my-js-file.min.js. So if you have a Gulp/Grunt script or a PHPStorm File Watcher auto-minifying your JavaScript, WordPress will properly output the correct JavaScript file.

Registering and Deregistering Scripts

Why register your scripts?

  1. DRY: The script manager allows you to re-use and output the script in various places of your code.
  2. Integrations: The script manager allows for anyone to integrate, optimize, and build upon your script.
  3. Prevents Duplication: A script using the script manager will only be printed on the page once.
  4. Client Focused & Client Friendly: It allows your customers/clients to remove the scripts easily.

DRY

The first reason (and one which I agree) is perfect for coders who subscribe to DRY (Don't Repeat Yourself) coding, which most of us prefer. It optimizes your code and makes your code clean and readable. WordPress Core registers a ton of JavaScript files. Most notable of these are:

  • jquery (core and other parts—jquery-ui, jquery-effects)
  • thickbox
  • underscrore
  • backbone
  • media-element

If you are using any of these, please use the WordPress core version. It ensures the best experience.

Client Focused & Client Friendly

Many developers do not like the second reason. They want to control every aspect of their code because they are afraid that their client/customer or another developer will break their plugin. However, to me, this is not a good reason. If a customer/client breaks the plugin/site because they chose to copy/paste code and place it in their functions.php site, it is their fault, and they can pay you to fix it. But by providing the client/customer code that is changeable, it makes you look like a better developer, and you will have return customers/clients because they can trust that you have their best interests in mind.

Integrations & Prevents Duplication

Plugins like WP Rocket and other optimization plugins can detect and handled registered scripts much easier than functions that echo script elements at random places throughout the document. WordPress filters, like script_loader_src and script_loader_tag, can be used to amend how one or more registered scripts are loaded in the DOM by amending the tag attributes (see below for the advanced examples), and finally other scripts can use those scripts as dependencies themselves.

By using a script manager, WordPress tries to prevent duplication of scripts. For example, if one plugin outputs underscore via a script tag and the other developer uses WordPress's version, then multiple underscore scripts will be printed on the page, which obviously will lead to problems.

The Code: Registering/Deregistering Scripts

There is a function for registering a JavaScript file (wp_register_script) and one for deregistering a JavaScript file (wp_deregister_script). Now, many developers, and even those on developers.wordpress.org, recommend that you register scripts within the wp_enqueue_scripts hook. However, it is my opinion that you should always register scripts on init (for frontend scripts wrapped with ! is_admin() conditional) and admin_init (for admin scripts) hooks. For the purposes of this article, we will be focusing all of the examples to frontend JavaScript, so we won't be using the admin_init hook. This allows your customers/clients to manipulate the scripts however they need, if they need.

Registering JavaScript Files

wp_register_script($handle, $src, $deps, $ver, $in_footer) takes a couple parameters which are obvious: $handle (slug) $src (the script URL), $in_footer (whether to output in the head or footer). It is always recommended that $in_footer be true unless (1) there is a very specific reason for the script to be in the head (other than "my code doesn't work in the footer") or (2) you use defer. Two really good examples are Google Analytics and New Relic—both require to be run in the head at the top. IMHO Google Analytics should have the async attribute added to it. The two remaining parameters—dependencies ($deps) and version ($ver) are more fascinating.

Script Version ($ver)

Most people use a simple version number to identify the version (e.g., 0.0.1 or 2.1). In fact, most developers are either ignorant or lazy and only use the version of the theme or plugin via a constant or something hardcoded that never seems to change. So if you are doing development with an aggressively caching browser it becomes problematic and frustrating to ensure that your changes are being reflected in the DOM, even with browsersync or gulp-livereload. However, the best option is to do both the version of the plugin and the file version. If you are like me, changing the version of the file every time in the JavaScript file and the correlating PHP file is such a hassle. Being a stickler on this yet also lazy about versioning myself, I need something simple that would be cacheable and yet also cache bust when needed (on update/change). My goal is to load the JavaScript file via WordPress and forget about that part and work on the JavaScript file only. So, I use filemtime for file versioning within my PHP and maintain semantic versioning within the JavaScript file properly. filemtime helps me accomplish this. It returns the epoch time (e.g., 1529330834) of when the file was written/changed. So as long as the file does not change or update, the file will be cached.

So here is how I do versioning for my scripts:

<?php
namespace MyPrefix\MyPluginOrTheme;
define( 'PLUGIN_VERSION', '1.0.0' );
// Parent Theme Version
$ver = PLUGIN_VERSION . '-' . filemtime( get_template_directory() . "/myscript$suffix.js" );
// Child Theme Version
$ver = PLUGIN_VERSION . '-' . filemtime( get_stylesheet_directory() . "/myscript$suffix.js" );
// Plugin Version, assumes this PHP file is in the root of the plugin folder (e.g., ./wp-content/plugins/myplugin/script-dependencies.php)
$ver = PLUGIN_VERSION . '-' . filemtime( get_plugin_dir( __FILE__ ) . "/myscript$suffix.js" );
view raw wp-script-dependencies.php hosted with ❤ by GitHub

The resulting filename will be something like myscript.js?ver=1.0.0-1528229721. This will make it easy to identify the version of the plugin while also ensuring that the correct version of the script is on the frontend. Even WordPress.com does something similar outputting files with the version of the file and a date (e.g., https://s1.wp.com/wp-content/js/mustache.js?ver=6.1.1-201824).

Script Dependencies ($deps)

Finally, $deps are really cool because when I output the file, WordPress will also output all the dependencies without me having to do it...but more on that in the next section.

Registering Scripts in Themes

For themes, you register files like this (using the previous best practice with the dynamic suffix):

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the theme folder.
wp_register_script(
'myscript', // file slug
get_stylesheet_directory_uri() . "/js/myscript$suffix.js", // file URL
array(), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/myscript$suffix.js" ), // version
true // in footer or not
);
wp_register_script(
'jquery-myscript', // file slug
get_stylesheet_directory_uri() . "/js/jquery.myscript$suffix.js", // file URL
array( 'jquery' ), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/jquery.myscript$suffix.js" ), // version
true // in footer or not
);
}
view raw wp-register-theme-script.php hosted with ❤ by GitHub

Alternatively, you could use a theme script register helper:

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
/**
* Register my scripts.
*
* @param string $filename Filename.
* @param string $theme_relative_path Relative path to file within theme folder.
*/
function register_theme_script( $filename, $theme_relative_path = '/', $dependencies = array(), $footer = true ) {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
wp_register_script(
str_replace( '.', '-', $filename ), // file slug without js extension
get_stylesheet_directory_uri() . "{$theme_relative_path}{$filename}{$suffix}.js", // file URL
$dependencies, // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "{$theme_relative_path}{$filename}{$suffix}.js" ), // version
$footer // in footer or not
);
}
view raw wp-register-theme-function.php hosted with ❤ by GitHub

Registering Scripts in Plugins

For plugins, you register files like this (using the previous best practice with the dynamic suffix):

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'myscript',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/myscript{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/myscript{$suffix}.js" ), // version
true
);
wp_register_script(
'jquery-myscript',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/jquery.myscript{$suffix}.js", // file URL
array( 'jquery' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/jquery.myscript{$suffix}.js" ), // version
true
);
}
view raw wp-register-plugin-script.php hosted with ❤ by GitHub

Alternatively, you could use a plugin script register helper:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
// Should be placed at the root of the plugin folder
define( 'MY_PLUGIN_FILE', __FILE__ );
/**
* Register my scripts.
*
* @param string $filename Filename.
* @param string $theme_relative_path Relative path to file within theme folder.
*/
function register_plugin_script( $filename, $theme_relative_path = '/', $dependencies = array(), $footer = true ) {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
wp_register_script(
str_replace( '.', '-', $filename ), // file slug without js extension
get_plugin_dir( MY_PLUGIN_FILE ) . "{$theme_relative_path}{$filename}{$suffix}.js", // file URL
$dependencies, // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "{$theme_relative_path}{$filename}{$suffix}.js" ), // version
$footer // in footer or not
);
}
view raw wp-register-plugin-function.php hosted with ❤ by GitHub

Deregistering JavaScript Files

Deregistering is extremely straight forward as you can see here:

<?php
namespace MyPrefix\MyPluginOrTheme;
// Deregister script by handle
wp_deregister_script( 'myscript' );
view raw wp-deregister-script.php hosted with ❤ by GitHub

However, actually deregistering a script can be quite tricky. The main problem with deregistering scripts is deregistering AFTER it has been registered. For plugins, this is why registering a script and enqueueing (or just enqueueing) a script in the same function (without a do_action between them) is an unfriendly and bad idea.

For example, here are three bad ways to do it:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_enqueue_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
}
view raw wp-register-script-bad-0.php hosted with ❤ by GitHub

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
wp_enqueue_script( 'plugin-script' );
}
view raw wp-register-script-bad-1.php hosted with ❤ by GitHub

https://gist.github.com/6e676ff6c5516427bb04aac361cacf00

This last one looks that most right but the problem is that they both are hooked at the same priority and there is no way to hook in between the functions to do anything.

Here is a better way:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts', 99 );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script' );
}
view raw wp-register-script-better.php hosted with ❤ by GitHub

This way, I can hook in between the functions within wp_enqueue_scripts hook to make any changes necessary.

Here is the best way (IMHO):

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script' );
}
view raw wp-register-script-best.php hosted with ❤ by GitHub

So when do you deregister a script? You deregister a script for two primary reasons:

  1. If you want to re-register the script with different arguments (primarily $src URL).
  2. If you want to ensure the script never gets placed on the frontend period.

Warning: Whenever you deregister a script (e.g., myscript) without re-registering a new version of that script, any script dependent upon that script (e.g., myscript) will not output either.

So, consider this example. In wp-config.php, we have define( 'SCRIPT_DEBUG', true );. Now, let's say we have this in an active plugin:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script-base',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js", // file URL
array( 'underscore' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array( 'plugin-script-base' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script-ext',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js", // file URL
array( 'plugin-script' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts', 999 );
/**
* Outputs our plugin scripts
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script-ext' );
}
view raw wp-plugin-scripts.php hosted with ❤ by GitHub

With a single statement, our use of the active plugin outputs three files on the frontend in this order:

  • js/plugin-script-base.js
  • js/plugin-script.js
  • js/plugin-script-ext.js

Now in our theme, say we have this in the functions.php file:

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\deregister_scripts', 999 );
/**
* Register my scripts.
*/
function deregister_scripts() {
// Assumes the JS file is located in the js folder within the plugin folder.
wp_deregister_script( 'plugin-script-base' );
}
view raw wp-functions-theme-remove-scripts.php hosted with ❤ by GitHub

This function within functions.php will cause plugin-script and plugin-script-ext NOT to be placed on the frontend either because the WordPress script manager assumes that since plugin-script and plugin-script-ext are dependent upon plugin-script-base, they also won't work if plugin-script-base is missing. So WordPress will not output those files.

A Note

Many seasoned non-WordPress PHP developers will want to use their own version of jQuery. So they will deregister WordPress's jQuery and register their own. This may be a great idea for your own project ONLY that you will always control, but it is a terrible idea for any free/premium plugins and/or themes. Here's why:

  • Other themes/plugins depend on the same version of that script WordPress provides, so you may break the theme or plugins accidentally. So if you deregister it and register your own (even if the same version), when WordPress upgrades that script (i.e., jQuery), it could break everything. Alternatively, if you deregister jQuery (for example) and register another specific version (older or newer), then you may break current plugins.
  • WordPress may upgrade that script for security reasons and now you have opened a security hole. This will cause a massive amount of support requests if you sell a premium theme/plugin and cause harm to your brand/reputation. Or, you may not even be around to fix the problem for your client.

This is not client/customer friendly at all. As a developer, we need to get over our own opinions that using a CDN version or a specific version of a script is better and accept this part of WordPress and the WordPress ecosystem. Simply, play nice!

Enqueueing and Dequeueing Scripts

So far, we have only properly registered (or deregistered) the scripts within WordPress but that does not output the script tags on the frontend. To output the scripts on the frontend, you need to queue the script to be outputted by the WordPress script manager. To do this, you will need to use wp_enqueue_script.

wp_enqueue_script does two things:

  1. Registers the script if it has not been registered already.
  2. Outputs any files on the frontend during the wp_head hook via print_head_scripts (which calls wp_print_scripts hook) and wp_footer hook via _wp_footer_scripts which calls print_late_styles() and print_footer_scripts().

Sometimes, however, plugins and themes will just enqueue all their scripts even if the page doesn't use that particular script. They do this mostly because they provide their functionality through a shortcode and don't know how to properly test the page to determine whether that shortcode exists or not (blog post coming soon). In these cases, to increase the performance of your site, you may want to remove them. To do this, you will need to use wp_dequeue_script. Let's say that our script was "properly" registered on init and enqueued on wp_enqueue_scripts hooks. To dequeue the script, you just need to do this:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\remove_script' );
/**
* Register my scripts.
*/
function remove_script() {
// Always dequeue the script
wp_dequeue_script( 'plugin-script' );
// Conditionally dequeue the script on archive pages
if ( is_archive() ) {
wp_dequeue_script( 'plugin-script' );
}
}
view raw wp-dequeue-script.php hosted with ❤ by GitHub

Localizing Scripts

Sometimes, our scripts need some additional information or configuration for whatever reason (localizing strings, configuring a JavaScript object, etc). This is done via wp_localize_script. wp_localize_script takes three parameters: $handle (string), $object_name (string), and $l10n (array).

When you localize your script, the object name (the second parameter) is the name of your JavaScript object, so best practice is that this parameter string would be camelCase. There are no checks within WordPress, so if you pass something like 'my-object', your JavaScript will fail.

Whatever PHP array is passed to wp_localize_script will be placed ahead of your external script tag. You can organize the $l10n array however you'd like, but over time, I have learned and now prefer to do something like this:

$plugin_l10n = array(
'strings' => array(
'firstLabel' => __( 'Some string to translate', 'textdomain' ),
'secondLabel' => __( 'Some string to translate', 'textdomain' ),
),
'config' => array(
'something' => 'some-value',
'somethingElse' => 3,
),
'debug' => ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ( ! defined( 'SCRIPT_DEBUG' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) ),
);
view raw wp-localize-plugin-script.php hosted with ❤ by GitHub

Again, please note that the string properties are camelCased.

Your JavaScript object will be placed in the Global Namespace. In our example, the JavaScript object is named pluginScriptExt. Therefore, our object is available globally via window.pluginScriptExt, so you must always prefix your objects unless you overwrite another object (if you need more information, see Mozilla's Global Object definition).

For example, if we take our script plugin-script-base from above, it is dependent on underscore. Underscore has a global JavaScript object named _. If we were to do this, we would render Underscore useless and _.each would not be available; instead _.strings would be.

// Overwrites underscore
wp_localize_script( 'plugin-script-ext', '_', $plugin_script_base_l10n );
view raw wp-localize-plugin-script-bad.php hosted with ❤ by GitHub

Alternatively, calling wp_localize_script twice on the same script with the same object name will overwrite the object, not extend the object. For example,

// Create empty pluginScriptExt object -- won't work
wp_localize_script( 'plugin-script-ext', 'pluginScriptExt', array() );
// Add .config to pluginScriptExt object -- won't work
wp_localize_script( 'plugin-script-ext', 'pluginScriptExt', array(
'strings' => array(
'firstLabel' => __( 'Some string to translate', 'textdomain' ),
'secondLabel' => __( 'Some string to translate', 'textdomain' ),
),
) );
// Add .strings to pluginScriptExt object -- won't work
wp_localize_script( 'plugin-script-ext', 'pluginScriptExt', array(
'config' => array(
'something' => 'some-value',
'somethingElse' => 3,
),
) );
view raw wp-localize-plugin-script-bad.php hosted with ❤ by GitHub

With this example, you will have:

// Overwritten
var pluginScriptExt = {
config: {
something: 'some-value',
somethingElse: 3
}
};
view raw wpPluginScriptExt.js hosted with ❤ by GitHub

So, if you want to "expand" an object, it is better to create two objects (until WP_Scripts expands its capabilities).

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script-base',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js", // file URL
array( 'underscore' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array( 'plugin-script-base' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script-ext',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js", // file URL
array( 'plugin-script' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js" ), // version
true
);
// Create base object for pluginScriptExt object.
wp_localize_script( 'plugin-script-ext', 'pluginScriptExtStrings', array(
'firstLabel' => __( 'Some string to translate', 'textdomain' ),
'secondLabel' => __( 'Some string to translate', 'textdomain' ),
) );
// Create base object for pluginScriptExt object.
wp_localize_script( 'plugin-script-ext', 'pluginScriptExtConfig', array(
'something' => 'some-value',
'somethingElse' => 3,
) );
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts', 999 );
/**
* Outputs our plugin scripts
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script-ext' );
}
view raw wp-localize-plugin-script-2.php hosted with ❤ by GitHub

With this example, you will have:

// As 2 objects
var pluginScriptExtStrings = {
firstLabel: 'Some string to translate', // localized
secondLabel: 'Some string to translate', // localized
};
var pluginScriptExtConfig = {
something: 'some-value',
somethingElse: 3
};
view raw wpPluginScriptExt.js hosted with ❤ by GitHub

It is important to note that the script should either be registered (via wp_register_script) or enqueued (via wp_enqueue_script) before it can be localized.

Initializing Scripts

There are several scripts, like sliders, that require some sort of initialization. Take waypoints, for example. It requires initialization before it can be used on the page. For example:

var waypoint = new Waypoint({
element: document.getElementById('waypoint'),
handler: function(direction) {
console.log('Scrolled to waypoint!')
}
});
view raw wp-waypoint-init.js hosted with ❤ by GitHub

To do this properly within WordPress, you need to use wp_add_inline_script.

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my frontend scripts.
*/
function register_scripts() {
if ( is_admin() ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js" ), // version
true
);
wp_register_script(
'my-waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js", // file URL
array( 'waypoints' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'my-waypoints' );
wp_add_inline_script( 'my-waypoints', "var waypoint = new Waypoint({
element: document.getElementById('waypoint'),
handler: function(direction) {
console.log('Scrolled to waypoint!')
}
})" );
}
// Result
//<script type="text/javascript" src="https://domain.com/wp-content/plugins/myplugin/js/my-waypoints.js"></script>
//<script type="text/javascript">
// var waypoint = new Waypoint({element: document.getElementById('waypoint'),
// handler: function(direction) {
// console.log('Scrolled to waypoint!')
// }
// })
//</script>
view raw wp-add-inline-script-plugin.php hosted with ❤ by GitHub

wp_add_inline_script can also output JavaScript before a specific script tag. To do this:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my frontend scripts.
*/
function register_scripts() {
if ( is_admin() ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js" ), // version
true
);
wp_register_script(
'my-waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js", // file URL
array( 'waypoints' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'my-waypoints' );
wp_add_inline_script( 'my-waypoints', 'console.log("before my-waypoints file")', 'before' );
}
// Result
//<script type="text/javascript">
// console.log("before my-waypoints file")
//</script>
//<script type="text/javascript" src="https://domain.com/wp-content/plugins/myplugin/js/my-waypoints.js"></script>
view raw wp-add-inline-script-plugin-before.php hosted with ❤ by GitHub

If you use wp_localize_script and wp_add_inline_script, wp_localize_script will always output before wp_add_inline_script.

Using wp_add_inline_script is a great way to keep your code clean using native WordPress functions and the script manager avoiding yet another hook and manually outputting this yourself.

It is important to note that the script should either be registered (via wp_add_inline_script) or enqueued (via wp_add_inline_script) before it can be initialized.

Extending the previous Waypoints example, let's only do something if waypoints is not already registered. You do this by using wp_script_is.

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Do nothing if the script is registered
if ( wp_script_is( $handle, 'registered' ) ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js" ), // version
true
);
}
view raw wp-scripts-is.php hosted with ❤ by GitHub

With wp_script_is, you can check whether a script has been:

  • registered
  • enqueued/queued
  • to_do/to_print
  • done/printed

wp_script_is is quite powerful in debugging scripts and in trying to find when a script was registered, enqueued, and printed on the page.

For example, with wp_script_is( $handle, 'enqueued' ), you can know that you can either deregister (if false) or dequeue the script (if true) in order to prevent that script from being printed on the page.

Finally (though this is becoming much less important), you can make your scripts conditional by adding conditional data to the script. You can do this by using

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my frontend scripts.
*/
function register_scripts() {
if ( is_admin() ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
// IE only Script
wp_register_script(
'plugin-script-ie',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-ie{$suffix}.js", // file URL
array( 'plugin-script' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-ie{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script' );
wp_script_add_data( 'plugin-script', 'conditional', 'lt IE 9' );
}
view raw wp-conditional-scripts.php hosted with ❤ by GitHub

Modifying Script Tags on Print

Adding Additional Attributes to script tag—async and defer

WordPress does not automatically add async or defer to your script tags, nor is there an "easy" way to add it to the script manager (which is still being actively discussed in core (tickets 22249 and 12009).

First, what is the difference between defer and async? bitsofcode has two great diagrams that explains the difference between the two:
JavaScript Async Execution

JavaScript Defer Execution

Simply stated both defer and async download/fetch the script immediately and do not pause/prevent further HTML parsing. The difference comes at JavaScript execution. defer executes after the HTML parsing is completed and DOM is ready. Because of this adding defer to scripts in the footer makes little sense. async executes when the script comes ready.

To improve the performance of the page, you need to add these yourself. You can do this by using the script_loader_tag filter, which gives you the HTML of the script being printed on the page, including the conditional HTML and any added inline script.

To add async or defer to all, you can do this:

<?php
namespace MyPrefix\MyTheme;
add_filter( 'script_loader_tag', __NAMESPACE__ . '\script_loader_tag_add_async', 10 );
/**
* Filters the HTML script tag of an enqueued script.
*
* @param string $tag The `<script>` tag for the enqueued script.
* @param string $handle The script's registered handle.
* @param string $src The script's source URL.
*
* @return string Modified script tags including `async="async"`.
*/
function script_loader_tag_add_async( $tag ) {
if ( false === strpos( $tag, 'async' ) ) {
return str_replace( ' src', ' async="async" src', $tag );
}
}
add_filter( 'script_loader_tag', __NAMESPACE__ . '\script_loader_tag_add_defer', 10 );
/**
* Filters the HTML script tag of an enqueued script.
*
* @param string $tag The `<script>` tag for the enqueued script.
* @param string $handle The script's registered handle.
* @param string $src The script's source URL.
*
* @return string Modified script tags including `async="async"`.
*/
function script_loader_tag_add_defer( $tag ) {
if ( false === strpos( $tag, 'defer' ) ) {
return str_replace( ' src', ' defer="defer" src', $tag );
}
}
view raw wp-scripts-cache-async-all.php hosted with ❤ by GitHub

Alternatively, you can be more specific and do something like this:

<?php
namespace MyPrefix\MyTheme;
add_filter( 'script_loader_tag', __NAMESPACE__ . '\script_loader_tag', 10 );
/**
* Filters the HTML script tag of an enqueued script.
*
* @param string $tag The `<script>` tag for the enqueued script.
* @param string $handle The script's registered handle.
* @param string $src The script's source URL.
*
* @return string Modified script tags including `async="async"`.
*/
function script_loader_tag( $tag ) {
// Load AFTER a page has finished loading completely
$deferred = array( 'comment-reply', 'wp-emoji' );
foreach ( $deferred as $script ) {
if ( false !== strpos( $tag, $script ) && false === strpos( $tag, 'defer' ) ) {
return str_replace( ' src', ' defer="defer" src', $tag );
}
}
// Load asynchronously while page is loading
$async = array( 'wp-embed', 'myscript' );
foreach ( $async as $script ) {
if ( false !== strpos( $tag, $script ) && false === strpos( $tag, 'async' ) ) {
return str_replace( ' src', ' async="async" src', $tag );
}
}
return $tag;
}
view raw wp-scripts-cache-async-defer.php hosted with ❤ by GitHub

If you use the Genesis Framework, this is already built-in for you. All you need to do is register your script with either ?async=true or ?defer=true and Genesis will automagically add async or defer to the script tag for you!!

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the theme folder.
wp_register_script(
'myscript', // file slug
get_stylesheet_directory_uri() . "/js/myscript$suffix.js?defer=true", // file URL with ?async=true added
array(), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/myscript$suffix.js" ), // version
true // in footer or not
);
wp_register_script(
'jquery-myscript', // file slug
get_stylesheet_directory_uri() . "/js/jquery.myscript$suffix.js?async=true", // file URL with ?defer=true added
array( 'jquery' ), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/jquery.myscript$suffix.js" ), // version
true // in footer or not
);
}
view raw wp-register-genesis-theme-script.php hosted with ❤ by GitHub

Changing the Script Source—Cache Busting

You can use the script_loader_src filter to cache bust everything.
DO NOT USE THIS IN PRODUCTION.

<?php
namespace MyPrefix\MyTheme;
add_filter( 'script_loader_src', __NAMESPACE__ . '\script_loader_src', 10, 2 );
/**
* Cache bust all the scripts.
*
* @param string $src Script loader source path.
* @param string $handle Script handle.
*
* @return string
*/
function script_loader_src( $src, $handle ) {
if ( 'plugin-script' !== $handle ) {
return $src;
}
return add_query_arg( 'wps', time(), $src );
}
view raw wp-scripts-cache-bust-all.php hosted with ❤ by GitHub

Summary

With this overview of the native WordPress Script functions for script management, you should be able to use any type of JavaScript file in any plugin or theme extending parent theme or plugin JavaScript files.

  1. PHP Constants for Debugging JavaScript: SCRIPT_DEBUG allows you to manage whether your script outputs minified or not. You can also pass the value of SCRIPT_DEBUG to wp_localize_script to help you add debug messages within the console.
  2. Registering and Deregistering Scripts: Use wp_register_script to register a script on init hook and wp_deregister_script only if you plan to register an alternative to avoid any unintended effects.
  3. Enqueueing and Dequeueing Scripts: Use wp_enqueue_script to output the script on the page and wp_dequeue_script to prevent the script's output.
  4. Localizing Scripts: Use wp_localize_script to output any configuration and translations of any strings used within your script.
  5. Initializing Scripts: Use wp_add_inline_script to add additional inline scripts to initialize external scripts or any thing else than adding a configuration object.
  6. Modifying Script Tags on Print: Use script_loader_src and script_loader_tag to make more advanced changes to the script tag including adding attributes like async or defer, which Genesis makes easy for you.

Thank you to both Gary Jones and Mike Hemberger for pre-reviewing and providing some great insights!

Written by Travis Smith · Categorized: Tutorials

Jun 05 2018

Causes of WordPress Site Performance Slowdown

Previously, we discussed Why WordPress Performance Matters and the importance of your site's speed and performance. So what could be causing your site's slowdown? What is slowing down your WordPress website? What factors are affecting your site's performance?

Your site's slowdown can be cause by a myriad of things including:

  • HTTP Requests
  • WordPress Configuration
  • Theme
  • Plugins
  • Page Size, Images, & Videos
  • Hosting, Bad architecture or Infrastructure (servers)

The list could go on and on depending on the type of site you are running. Your site's speed could be fantastic but your admin horribly slow. So let's break some these down into some details.

HTTP Requests

When doing a site audit or analysis, one of the first things I check when analyzing a site is the number of HTTP Requests a site is making. With HTTP Requests, the fewer the better your site will perform.

Take my site for example. At the time of writing this article, my site has 47 total HTTP Requests:

  •  Javascript: 19 (620 KB)
  •  CSS: 12 (294 KB)
  •  Images: 9 (6978 KB)
  •  Font: 4 (105 KB)
  •  HTML: 3 (72 KB)

Now compare that to some ad-driven sites (speedtest.net has 381 total HTTP Requests), 47 is really good. However, it can be drastically improved with some good workflow(s), combining of files, etc. (which I haven't done on my site yet and probably should). If you dive into it, you will see some files that I do not use at all. If you dive even deeper, you will find some CSS that I do not use at all. So I could reduce my CSS file size by clearing out some of my CSS.

How many HTTP Requests does your site have? You can easily check out yours at Gift Of Speed's HTTP Requests Checker.

WordPress Database & Configuration

There are several things within WordPress that you can configure or add to your functions.php file to ease and improve the site's performance. These include:

  • Updates
  • Comments
  • Revisions
  • Emptying Trash
  • Database (DB) Optimization

Updates

Many WordPress site owners think that WordPress will take care of itself and never update WordPress or any of the plugins and themes in fear of something breaking, which is extremely valid (and why you should always have a staging site). If you are on a managed WordPress hosting company like WP Engine, then you are pretty much covered. However, if you are on Bluehost or Hostgator, then you need to add the ability for WordPress to update itself automatically.

To configure automatic updates, add one of the following to wp-config.php:

// Disable automatic updates.
define( 'AUTOMATIC_UPDATER_DISABLED', true );

// Enable all core updates.
define( 'WP_AUTO_UPDATE_CORE', true );

// Enable only minor core updates.
define( 'WP_AUTO_UPDATE_CORE', 'minor' );

Personally, I always use the minor core updates configuration and leave major updates to be done manually as this really could break sites that are using more than a few plugins.

Regardless, all active plugins and themes should be updated, and any minor updates should be applied immediately. Minor updates are any updates where the last number in the version changes. So if your plugin is currently version 1.1.2, a minor update would be 1.1.3 or 1.1.4. If your plugin is currently version 1.1, 1.2 could be considered a minor update. Minor updates are usually fixing some small bug or security hole (more below).

While on the subject of auto-updates, auto-updates of translations are enabled by default but can be disabled by adding the following to your functions.php file:

add_filter( 'auto_update_translation', '__return_false' );

And finally, WordPress sends emails regarding the state of automatic updates. To disable these emails, add the following to your functions.php file:

add_filter( 'auto_core_update_send_email', '__return_false' );

Comments

Content with lots of discussion can be a huge issue in the site's performance. More advanced configuration would shard comments from the database and host comments in its own instance. However, that is not even a possibility for most of us. Instead, you could use something like Jetpack's comment system or even Disqus, LiveFyre, etc. While I have not done my due diligence on Jetpack's comment system's performance, I have been most satisfied with theirs above the others. Yet, if you lazy load Disqus (e.g., using Disqus Conditional Load), then you can definitely improve the performance of your post page, or any page with comments.

Alternatively, you can break your comment section into pages, which is available by default within WordPress core. To paginate comments, just go to Settings » Discussions and then choose the number of comments you want per page. This should help improve memory consumption and boost page load times for posts and pages with tons of comments.

Finally, removing and preventing spam comments is extremely important. You can easily do this with the Empty Spam button on the Comments admin page.

Revisions

One example is revisions. Your site creates a lot of revisions when you are writing. This will pollute your database and make database queries take longer time to process. To fix this, you can either disable or limit the number of revisions.

To disable revisions, add the following to wp-config.php:

define( 'WP_POST_REVISIONS', false );

To limit revisions, add the following to wp-config.php:

define( 'WP_POST_REVISIONS', 3 );

By default, WordPress auto-saves your posts every 60s. You can also slow down how often (in seconds) revisions are taken, add the following to wp-config.php:

define( 'AUTOSAVE_INTERVAL', 300 );

Personally, I would never turn off autosave because I cannot tell you how many times it has saved me, my friends or my clients.

The Trash

Sometimes, you will have deleted items (e.g., posts, pages, images, comments, links, etc.) that have been placed into the trash. By default, WordPress empties the trash every 30 days. However, you can also configure this by adding the following to wp-config.php:

define( 'EMPTY_TRASH_DAYS', 14 );

Or you can even disable this (though I highly discourage this as it will bloat your database).

define( 'EMPTY_TRASH_DAYS', 0 );

Database Optimization

Finally, you should optimize your database often. First and foremost, before touching the database, always create a backup. Always! This cannot be over stated. In a subsequent article, I will discuss several plugins that will help you achieve this, but let me give you a high level overview.

Your database should be optimize to remove any unnecessary data such as spam comments, pingbacks, trackbacks, expired transients, and orphaned data (usually from plugins being deactivated. deleted, or removed improperly). Again, before you do any of this, backup your database.

Almost all plugins store something in the database, whether it is in the wp_options table or custom tables. However, not all plugins have properly implemented an uninstall method to remove the data associated with the plugin (or the plugin forgets about some of the settings it set). So, the WordPress database can accumulate a lot of additional data that is unused.

Now, WordPress does have the ability to repair and optimize itself. You can  read more about this in the Automatic Database Optimizing  within WordPress.org. To enable this, add the following to wp-config.php:

define( 'WP_ALLOW_REPAIR', true );

Then you can go to the repair page (/wp-admin/maint/repair.php) to use the optimization tool.

My favorite and go to plugin is the Advanced Database Cleaner (which has a free version) by Sigma Plugin, but more about that later.

Your WordPress Theme & Performance

WordPress themes are written for a wide audience of people focusing on features, nice and pretty presentations, and flexibility in order to garner sales and revenue. Your theme may be slowing down your WordPress site because the theme is:

  • Over-engineered
  • Not updated
  • Fonts

Generally speaking, WordPress themes (or child themes) do not focus on performance per se (e.g., conditionally outputting files when in use). For example, while Genesis focuses heavily on performance, the child theme that you are using may not focus on performance, and I have seen, even sadly created, some really bad child theme implementations.

Over-Engineered

In the past, ThemeForest themes have been notoriously bad about providing over-engineered, poorly coded, feature-rich themes. These themes are focused on providing as much functionality and as many features as possible in order to sell to the widest audience as possible. However, Evanto has made great efforts of late to begin mitigating some of these issues they had when the site first started, and there are some really good themes in their marketplace. Other private theme shops are also extremely bad about this (but I am not going to call any of these out explicitly).

One example is shortcodes. Shortocdes are often include as a part of a theme to provide additional functionality, but often theme developers assume that you are going to use them and output the JavaScript and CSS assets without outputting them conditionally (e.g., using has_shortcode on the post's content).

Not Updated

Your site should be using a child theme, or provide a mechanism for you to customize the site without your site breaking due to a theme update.

Automatic updates can be configured for themes. To add support for automatic theme updates, add the folllowing to your functions.php file:

add_filter( 'auto_update_theme', '__return_true' );

Or if you want to prevent automatic updates, then add:

add_filter( 'auto_update_theme', '__return_false' );

Alternatively, you can also have finer control over what theme(s) you may want to auto-update.

add_filter( 'auto_update_theme', 'prefix_auto_update_genesis', 10, 2 );
/**
 * Auto update specific theme.
 * 
 * @param bool   $update Whether to allow auto-update.
 * @param string $item   Item slug.
 *
 * @return bool Whether to update plugin.
 */
function prefix_auto_update_genesis( $update, $item ) {
    if ( 'genesis' === $item->theme ) {
        return true;
    } else {
        return $update; // Else, use the normal API response to decide whether to update or not
    }
}

Google Fonts Performance for All Weights and Styles

Fonts

Some themes will output several Google Fonts in all its font weights and styles but you may only use a subset. For example, if you were to select all styles and weights for the popular Open Sans, even Google tells you that it will have a slow load time.

The Plugins You Use & Performance


WordPress plugins are also written for a wide audience of people focusing on features and flexibility. The best plugins focus on a single feature and/or problem. Please note, it is not the number of plugins being used, but the quality of the plugins (see this article). However, many WordPress (non-performance) plugins also do not consider performance. The biggest culprit and example are slider and gallery plugins. Both Soliloquy and Envira Gallery entered the market and took its marketshare entirely based on its performance capabilities.

Your site could be slow because you have plugins that may:

  • Duplicate functionality
  • Not used
  • Use too much CPU and Memory (RAM)
  • Be old and out-of-date
  • Pirated
  • Poorly coded

Duplicate Functionality Plugins

We all use WordPress plugins to enhance our site(s). However, sometimes we use plugins that duplicate functionality that our theme has or even another plugin! Recently, I was working on site that had not one or two but three different slider plugins installed! That was the first thing that had to go! Another example is that we have plugins that we are using that your host may provide out of the box (e.g., No Revisions, Force Strong Passwords, etc. are some plugins that WP Engine already helps on their managed hosting).

Let me given you a couple really good and easy examples. First, many of us love icons, and if you look at many WordPress sites, you will find Dashicons and Font Awesome in the HTML but only one of them is used (except when the user is logged into the site and admin bar is showing). This is often because one will be outputted by the theme and the other will be outputted by a plugin. There are many reasons why this happens but is something that we all need to be aware.

Second, we all need good SEO and sitemaps. But you do not need Google XML Sitemaps if you are running Yoast's WordPress SEO plugin or his premium plugin.

Unused Plugins

Many people stop using a plugin but are afraid to deactivate the plugin because "it may be in use somewhere on the site." These excess plugins may be causing performance and/or security issues on your site that you are not even aware.

High CPU/Memory Plugins

Some plugins are detrimental to your site's performance because they require heavy CPU or memory (RAM) to run. This could be due to the plugin's code and/or use. WP Engine maintains a phenomenal (and dynamic) list of Disallowed Plugins, note especially the Related Posts Plugins, Broken Link Checker Plugins, and Email Plugins. One other is WordFence, which is a great security plugin but does affect performance.

These aren't bad plugins. They provide functionality that you may want or even need, but they are performance hogs preventing your site from performing its best. If you "need" any of these plugins, you may need to upgrade to a VPS or Dedicated Hosting plan (see below).

Old Plugins

All active plugins should be updated, and any minor updates should be applied immediately. Minor updates are any updates where the last number in the version changes. So if your plugin is currently version 1.1.2, a minor update would be 1.1.3 or 1.1.4. If your plugin is currently version 1.1, 1.2 could be considered a minor update. Minor updates are usually fixing some small bug or security hole. So always keep your plugins up-to-date!

Automatic updates can be configured for plugins. To add support for automaticplugin updates, add the folllowing to your functions.php file:

add_filter( 'auto_update_plugin', '__return_true' );

Or if you want to prevent automatic updates, then add:

add_filter( 'auto_update_plugin', '__return_false' );

Alternatively, you can also have finer control over what plugins you may want to auto-update.

add_filter( 'auto_update_plugin', 'prefix_auto_update_specific_plugins', 10, 2 );
/**
 * Auto update specific plugins.
 * 
 * @param bool   $update Whether to allow auto-update.
 * @param string $item   Item slug.
 *
 * @return bool Whether to update plugin.
 */
function prefix_auto_update_specific_plugins ( $update, $item ) {
    // Array of plugin slugs to always auto-update
    $plugins = array ( 
        'akismet',
        'buddypress',
    );
    if ( in_array( $item->slug, $plugins ) ) {
        return true; // Always update plugins in this array
    } else {
        return $update; // Else, use the normal API response to decide whether to update or not
    }
}

Pirated Plugins

Pirated Plugins Steal Your Performance

Face it. Some people hate paying for WordPress plugins. After all GPL means free, right? Wrong. GPL means that the code can be freely distributed but does not mean that the plugin itself is free. Developers provide a service above and beyond the plugin. They provide updates and fixes as WordPress continues to evolve. They provide support in helping identify plugin conflicts with other plugins, etc. But regardless, some people still download premium plugins from non-reputable sites. These sites can and often do inject some "phone home" or other malware. Just remember, there is no honor among thieves. If you do take this risk, Sucuri is a great premium plugin to use to watch your site's malware issues.

Poorly Coded Plugins

Like themes, not all plugins are created equal. Some are quick proof0-of-concepts to see if it can make money. Some are quickly done for clients to get the client off the developer's back. Some are coded by junior developers that do things "weirdly,"  or don't follow PHP/JavaScript coding best practices. Some are written by new WordPress developers (though they may be senior PHP/JavaScript developers or senior other-CMS developers) and just don't know how to code for WordPress, so they do not use WordPress best practices (e.g., not using WordPress's built-in JavaScript libraries).

Also, one of the best practices of figuring out what is causing your site slowness is to deactivate plugins one by one to see which one is causing said issue. So, when you add a new plugin, test your site thoroughly to make sure there are no ill effects.

Page Size, Images, & Videos

Page size is incredibly important. For example a light weight page of 929KB (average web page size in 2011) will always load faster than the average website page of 2-3MB (average web page size today) which will always load faster than the outrageously heavy pages of 30MB (for more information see SpeedCurve's post). The majority of new internet users are in regions where 2G is the basic internet connection. They also are paying for the data they use by the GB or even MB (how much does my site cost the user?). So in order to be fiscally and data responsible, we need to optimize our site the best we can.

Heavy pages are almost always caused by ads, images and videos.The performance of both images and video can be easily improved through lazy loading.

Images are a fantastic way to engage users and prettify your site. However, many sites are heavy laden with tons of images that are not optimized. As a result, the reverse affect could be happening. Instead of engaging your users, you may be annoying your users. To prevent this, I recommend that you optimize your web images whether that is via the command line, some optimization program, or image editing software (e.g., PhotoShop, etc.).

Optimizing an image includes two things: (1.) image height and width and (2.) image file size. The height and width will affect your image file size. For example, if your site is designed to only be 1280px wide, your images don't need to be larger than 1280px wide. Of course there are reasons why you may want to offer larger image sizes but these sizes should be loaded on a user action (e.g., click for a modal or download).

You can resize your images using Bulk Resize, which allows you to resize images without uploading them! This program will also optimize your image approximately 20%. WordPress also attempts to optimize your JPEG images to ~80% quality. However, this too can be changed to improve the quality (e.g., 90%, which improves the files size) or reduce the quality (e.g., 70%, which reduces the file size).

// Increase quality and image file size.
add_filter( 'jpeg_quality', create_function( '', 'return 90;' ) );

// Reduce quality and image file size.
add_filter( 'jpeg_quality', create_function( '', 'return 100;' ) );

Videos, in my opinion (from a user perspective), should never autoplay. From an advertiser/media company perspective, autoplay does provide some revenue potential, if properly pre-rolled (or even post-rolled). Also, videos should never be played from WordPress. Instead, use Vimeo or YouTube. Both of them have the ability to make the videos private and able to play only on certain domains.

Hosting, Architecture, & Infrastructure

Hosting is one of the biggest culprits of why your site is slow. However, this can only be determined by the process of eliminating all other issues. The primary indicator for determining that it is the host's performance is Time to First Byte (TTFB). Usually with many hosting companies like Hostgator or Bluehost, you have options like:

  • Shared hosting
  • WordPress hosting
  • VPS hosting
  • Dedicated hosting
  • Reseller Hosting

What is the difference between these?

Shared & WordPress Hosting

Shared Hosting is a web hosting environment that is shared across multiple customers/accounts and websites from a single server. Shared hosting are servers than generally can run any type of website, whether it is written in PHP (like WordPress, Drupal, Joomla, etc.), nodejs, GoLang, Ruby, Python, etc. While many hosting companies have explicit clauses against running Minecraft or gaming servers on shared hosting, some people still do this.

Let me give you an example. Let's say a server can hold and run 100 websites, and let's say a customer has an average of 2.5 websites. This means that a company will place 40 customers (100 sites/2.5 sites per customer) on that server. So if the customers on that server average 5 websites per customer then the server will under perform. Or, say the server is perfectly allocated among all the customers, if any one of those customers begin to spam or send out a plethora of emails, this will affect your site's performance. If any of those customers get hacked, besides being potentially hacked (though most hosting companies have decent isolation between customers), your site's performance will wane due to their hacking.

Now, most of these web hosting companies have monitoring, algorithms, scripts to prevent this from happening as much as possible. However, these monitoring tools, etc. are designed to maximize the use of the server, not your specific site's performance. So if your site's performance begins to slow because of shared hosting, you can ask your host to investigate.

At some point, the only way to improve your site's performance will be to move away from a shared hosting environment to a VPS or dedicated hosting environment.

WordPress Hosting is only another flavor of shared hosting except that these servers are often tuned specifically for WordPress. This means that support for other languages is often removed (though not always). WordPress Hosting at companies like Hostgator or Bluehost is very inexpensive and a great solution for low traffic websites or a proof-of-concept site.

VPS Hosting

Virtual Private Server (VPS) Hosting is a website hosting environment that dedicates a specific amount of resources (e.g., RAM, CPU, Bandwidth, Storage, etc.) to your account. This is one step above shared hosting, which does not guarantee any specific amount of resources to your account, and one step below dedicated hosting. Within the datacenter, a VPS hosted account will receive a single virtual private server, but the physical server may have multiple VPS accounts on it with its resources split among the accounts appropriately.

Let me give you a basic example. Let's say a physical server has 16 CPU Cores and 64GB RAM, and the VPS company offers VPS solutions of

  1. 4 Cores & 8GB RAM,
  2. 8 Cores & 16GB RAM,
  3. 8 Cores & 32GB RAM, and
  4. 16 Cores & 32GB RAM

Now, the hosting company can host:

  • four clients who select option #1,
  • 2 clients who select option #2,
  • 1 client who selects option #2 and 1 client who selects option #3,
  • 2 clients who select option #3,
  • 1 client who selects option #3.

Generally speaking, there are two types of VPS Hosts: managed and semi/un-managed. Managed means that you will be given admin portal(s), some automated solutions (e.g., backups) and support to help you do what you need. Semi/Un-managed simply means that you are given the VPS with access and can do whatever you want. While support is given to unmanaged solutions, often it is limited for a variety of good reasons.

At some point, the only way to improve your site's performance will be to move away from both a shared hosting environment and a VPS hosting environment to a dedicated hosting environment.

Dedicated Hosting

Dedicated Hosting is a website hosting environment on a dedicated server for your account. This means that only you and your website(s) are on this server. These servers are much faster because you are not sharing anything with anyone, so all the server's resources are yours entirely. With dedicated, you also get full control of everything, which is both good and bad. As Uncle Ben says, "With great power comes great responsibility."

Reseller Hosting

Reseller Hosting, depending on the company, is a VPS or Dedicated Hosting account for a company or agency to host (and bill) their clients without the overhead of server maintenance, server software, etc. It white labels the true, underlying hosting company. So say you are hosting with a Web Development Agency name WP Awesome Hosts. WP Awesome Hosts could be using Hostgator, Bluehost, Amazon, KnownHost, or any other cloud hosting company (with real data centers) white labeling it as though they are a true hosting company.

Summary

So what is causing your site slowdown? Is it:

  • HTTP Requests
  • WordPress Configuration
  • Theme
  • Plugins
  • Page Size, Images, & Videos
  • Hosting, Bad architecture or Infrastructure (servers)

In the next few posts, I will be diving further into WordPress performance and even more specifics around tooling and identifying performance bottlenecks and issues. Stay tuned!

Written by Travis Smith · Categorized: Performance

  • 1
  • 2
  • 3
  • …
  • 61
  • Next Page »
  • Twitter
  • Facebook
  • LinkedIn
  • Google+
  • RSS

Copyright © 2023 WP Smith on Genesis on Genesis Framework WordPress Log in