Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

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:

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:

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.

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:

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

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';

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',
) );

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
);
}
}

 

Credits: Image From Kristina Alexanderson.

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:

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

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;

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';

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}):

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();

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();
}

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}):

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' );
}

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"
}
}

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' );
}

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();

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();
}

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}):

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();

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();
}

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!

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";

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' );

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';
]

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';

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:

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" );

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
);
}

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
);
}

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
);
}

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
);
}

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' );

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
);
}

<?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' );
}

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' );
}

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' );
}

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' );
}

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

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' );
}

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:

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' );
}
}

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 ) ),
);

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 );

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,
),
) );

With this example, you will have:

// Overwritten
var pluginScriptExt = {
config: {
something: 'some-value',
somethingElse: 3
}
};

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' );
}

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
};

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!')
}
});

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>

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>

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
);
}

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

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' );
}

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 );
}
}

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;
}

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
);
}

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 );
}

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!

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:

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:

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

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:

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 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:

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:

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:

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!