When it comes to loading scripts in web development every millisecond counts, there are several approaches, but the best practice is to use the following methods:
- Place script tags at the end of the body element: This method helps in faster page loading since the browser can render the page without waiting for the scripts to load.
- Use the async or defer attributes in script tags: These attributes help in loading scripts asynchronously or deferred, respectively, which improves page performance. The async attribute loads the script in parallel with the HTML document, while the defer attribute loads the script after the HTML document has been parsed.
- Minimize script file size: Minimizing the script file size can help in reducing the time it takes to load the script, thereby improving page performance.
- Use Content Delivery Network (CDN) for script files: Using a CDN for script files can help in faster loading of the script since the file is stored in multiple servers around the world, and the browser can fetch it from the nearest server.
- Use modern script loading techniques: Modern script loading techniques like module loading can help in loading only the necessary parts of a script, thereby improving performance.
- Use preload,
preconnect
, prefetch, dns-prefetch, prerender, modulepreload
By following these best practices, you can ensure that scripts are loaded efficiently, which can help in improving page performance and user experience.
WordPress is a popular platform for building websites and web applications. However, one common issue that developers face is slow page load. This can often be due to inefficient loading of scripts on the site.
Here are some technical best practices with code examples for loading scripts in WordPress:
Use Content Delivery Network (CDN)
function weszty_web_load_assets() {
wp_enqueue_style( 'slick', 'https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.css' );
wp_enqueue_script( 'slick', 'https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.js', array(), '1.8.1', false );
}
add_action( 'wp_enqueue_scripts', 'weszty_web_load_assets' );
We can also remove local version of scripts with FREE CDN versions. (Remember to select the minified version!)
function weszty_web_load_assets() {
wp_deregister_script( 'jquery' );
wp_register_script( 'jquery', "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js", array(), '3.6.4', false );
wp_enqueue_script( 'jquery' );
}
add_action( 'wp_enqueue_scripts', 'weszty_web_load_assets' );
“Cut the Fat” technique
WordPress offers a variety of plugins that can assist you in obtaining any function you require; however, this has the drawback that most plugins load scripts throughout the page whether they are used or not.
This is normal, it does not know on what page you plan to use it. Let’s take the Contact form 7 plugin as example. After you install it you will notice all the pages will load some extra CSS and JS files.
We don’t want that on all the pages, so we need to take them out and load them only on pages where we actually use them.
There are a lot of ways to do this in code, let’s see a few examples:
Using constants and/or hooks:
Example (1)
// Disable CF7 assets
define( 'WPCF7_LOAD_JS', false );
define( 'WPCF7_LOAD_CSS', false );
// OR
add_filter( 'wpcf7_load_css', '__return_false' ); // Disable CF7 CSS
add_filter( 'wpcf7_load_js', '__return_false' ); // Disable CF7 Javascript
remove_action( 'wp_enqueue_scripts','wpcf7_recaptcha_enqueue_scripts', 20 );
function weszty_web_asset_logic_cf7 ( $output, $shortcode ) {
if ( 'contact-form-7' == $shortcode ) {
wpcf7_recaptcha_enqueue_scripts();
wpcf7_enqueue_scripts();
wpcf7_enqueue_styles();
}
return $output;
}
add_filter( 'pre_do_shortcode_tag', 'weszty_web_asset_logic_cf7', 10, 2 );
Example (2)
function weszty_web_asset_logic_cf7() {
$load_scripts = false;
if( is_singular() ) {
$post = get_post();
if( has_shortcode( $post->post_content, 'contact-form-7' ) ) {
$load_scripts = true;
wp_enqueue_style( 'contact-form-7' );
wp_enqueue_script( 'contact-form-7' );
wp_enqueue_script( 'google-recaptcha' );
wp_enqueue_script( 'wpcf7-recaptcha' );
}
}
if( ! $load_scripts ) {
wp_dequeue_style( 'contact-form-7' );
wp_dequeue_script( 'contact-form-7' );
wp_dequeue_script( 'google-recaptcha' );
wp_dequeue_script( 'wpcf7-recaptcha' );
}
}
add_action( 'wp_enqueue_scripts', 'weszty_web_asset_logic_cf7', 99 );
Example (3)
function weszty_web_asset_logic_cf7( $posts ) {
if ( empty( $posts ) || is_admin() ) return $posts;
foreach ( $posts as $post ) {
if( has_shortcode( $post->post_content, 'contact-form-7' ) ) {
wp_enqueue_style( 'contact-form-7' );
wp_enqueue_script( 'contact-form-7' );
wp_enqueue_script( 'google-recaptcha' );
wp_enqueue_script( 'wpcf7-recaptcha' );
break;
}
}
return $posts;
}
add_action( 'the_posts', 'weszty_web_asset_logic_cf7' );
These approaches are elegant but not really efficient since most of the themes out there have a large content thanks to builders. This function will search for shortcodes and enqueue the right scripts based on what we found.
The advantage of this method is that you can include a script or style in the head section of the page. This also has its disadvantages, searching for shortcodes in the content of every post is quite inefficient especially when it’s inside of builder pages mixed up with the builder blocks. (shortcodes)
If we don’t use builders and only our custom shortcodes to create the custom pages it is much faster because it will have 4-5 shortcodes only in the content. Nothing more.
Both of these solutions have their own advantages and disadvantages. I personally prefer the former since it is faster and more intuitive.
So how can we make it efficient for those kind of pages? Simple, we enqueue them by page.
add_filter( 'wpcf7_load_css', '__return_false' ); // Disable CF7 CSS
add_filter( 'wpcf7_load_js', '__return_false' ); // Disable CF7 Javascript
remove_action( 'wp_enqueue_scripts','wpcf7_recaptcha_enqueue_scripts', 20 );
/**
* Cut the fat by shortcode
*/
function weszty_web_asset_logic_cf7 () {
if ( is_page( 'contact' ) ) {
if ( function_exists( 'wpcf7_recaptcha_enqueue_scripts' ) ) wpcf7_recaptcha_enqueue_scripts();
if ( function_exists( 'wpcf7_enqueue_scripts' ) ) wpcf7_enqueue_scripts();
if ( function_exists( 'wpcf7_enqueue_styles' ) ) wpcf7_enqueue_styles();
}
}
add_filter( 'wp_enqueue_scripts', 'weszty_web_asset_logic_cf7', 10, 2 );
As we can see here we remove all the Contact Form 7 scripts from everywhere with the exception of the contact page.
The middle ground
I used to register all of my assets and enquire them as needed. How do I know when is needed ?
function shortcode_check_styles( $name ) {
if( ! wp_style_is( $name, $list = 'enqueued' ) ) {
// load necessary styles (this will load it in the footer since the head is already rendered)
wp_enqueue_style( $name );
}
}
function shortcode_check_scripts( $name ) {
if( ! wp_script_is( $name, $list = 'enqueued' ) ) {
// load necessary scripts (this will load it in the footer since the head is already rendered)
wp_enqueue_script( $name );
}
}
Here I verify the registration of the script/style and enqueue it in my shortcode. As we know, it will load the scripts in the footer, but there are some scripts that must be loaded on top of the page, well for that problem we will enqueue the script by page and in our shortcode we check if is enqueued, if not, we display a small error/notice to let us know the problem.
When we finally have the finished page and need to manage and optimize the scripts, this will enable us to stay organized. We’ll look at the footer, which will have a few scripts, and decide which one belongs where (header or footer).
Use Async or Defer Attributes in Script Tags
As web pages and apps become more interactive, it’s important to optimize their performance by only loading necessary scripts as needed. When a browser loads a web page, it first constructs the Document Object Model (DOM) by rendering each tag in the HTML.
However, when the parser encounters a script tag, it stops rendering the HTML and waits for the script to be fetched and executed. This can cause delays in rendering the rest of the page, which is why it’s important to optimize script loading to minimize these delays and provide a better user experience.
Luckily, there are two <script>
attributes that solve the problem for us: defer
and async
.
PS: You should always load third-party scripts asynchronously unless the script has to run before the page can be rendered. The way third-party JavaScript is loaded matters a lot. If it’s done synchronously in the critical rendering path (all resources that the browser needs to display the first screen’s worth of content.) it delays parsing of the rest of the document.
defer script attribute (Document order (as they go in the document))
The defer
attribute tells the browser not to wait for the script. Instead, the browser will continue to process the HTML, build the DOM. The script loads “in the background”, and then runs when the DOM is fully built.
In other words:
- Scripts with
defer
never block the page. - Scripts with
defer
always execute when the DOM is ready (but beforeDOMContentLoaded
event).
Deferred scripts keep their relative order, just like regular scripts. Let’s say, we have two deferred scripts: the long.js
and then small.js
.
<script defer src="https://domain.com/assets/js/long.js"></script>
<script defer src="https://domain.com/assets/js/small.js"></script>
Browsers scan the page for scripts and download them in parallel, to improve performance. So in the example above both scripts download in parallel. The small.js
probably finishes first.
…But the defer
attribute, besides telling the browser “not to block”, ensures that the relative order is kept. So even though small.js
loads first, it still waits and runs after long.js
executes.
That may be important for cases when we need to load a JavaScript library and then a script that depends on it.
Let’s see how can we do that in WordPress:
First we need a list of scripts loaded in the page:
/**
* Display all scripts on the header
*
* @return void
* @since 1.0.0
*/
function detect_enqueued_scripts() {
if ( !current_user_can( 'administrator' ) ) return;
global $wp_scripts;
echo '<pre> Scripts: ';
foreach( $wp_scripts->queue as $handle ) :
echo $handle . ' | ';
endforeach;
echo '</pre>';
}
add_action( 'wp_print_scripts', 'detect_enqueued_scripts' );
After that we can add the drefer attribute to them manually one by one:
/**
* Usage Example
* wp_script_add_data( 'script_name', 'defer', true );
*
* @param String $tag The original enqueued <script src="...> tag
* @param String $handle The registered unique name of the script
* @since 1.0.0
*/
function defer_scripts( $tag, $handle ) {
if ( wp_scripts()->get_data( $handle, 'defer' ) ) {
return str_replace( ' src', ' defer src', $tag );
}
return $tag;
}
add_filter( 'script_loader_tag', 'defer_scripts', 10, 2 );
We can also apply to all scripts and make our own logic:
/**
* Will add defer attribute to all scripts
*
*/
function add_defer_attribute_to_all( $tag, $handle ) {
if ( FALSE === strpos( $tag, '.js' ) ) {
// not our file
return $tag;
}
if ( strpos( $tag, 'jquery' ) ) return $tag;
return str_replace( ' src', ' defer src', $tag );
}
add_filter( 'script_loader_tag', 'add_defer_attribute_to_all', 10, 2 );
async script attribute (Load-first order. Their document order doesn’t matter – which loads first runs first)
The async
attribute is somewhat like defer
. It also makes the script non-blocking. But it has important differences in the behavior. The async
attribute means that a script is completely independent:
- The browser doesn’t block on
async
scripts (likedefer
). - Other scripts don’t wait for
async
scripts, andasync
scripts don’t wait for them. DOMContentLoaded
and async scripts don’t wait for each other
In other words, async
scripts load in the background and run when ready. The DOM and other scripts don’t wait for them, and they don’t wait for anything. A fully independent script that runs when loaded.
Here’s an example similar to what we’ve seen with defer
: two scripts long.js
and small.js
, but now with async
instead of defer
.
They don’t wait for each other. Whatever loads first (probably small.js
) – runs first:
<script async src="https://domain.com/assets/js/long.js"></script>
<script async src="https://domain.com/assets/js/small.js"></script>
- The page content shows up immediately:
async
doesn’t block it. DOMContentLoaded
may happen both before and afterasync
, no guarantees here.- A smaller script
small.js
goes second, but probably loads beforelong.js
, sosmall.js
runs first. Although, it might be thatlong.js
loads first, if cached, then it runs first. In other words, async scripts run in the “load-first” order.
Async scripts are great when we integrate an independent third-party script into the page: counters, ads and so on, as they don’t depend on our scripts, and our scripts shouldn’t wait for them:
<!-- Google Analytics is usually added like this -->
<script async src="https://google-analytics.com/analytics.js"></script>
Let’s see how can we do this in WordPress:
No, I will not repeat myself, it is the same function as the above, but with async instead of defer.
PS: Please note: if you’re using defer
or async
, then user will see the page before the script loads. In such case, some graphical components are probably not initialized yet. Don’t forget to put “loading” indication and disable buttons that aren’t functional yet. Let the user clearly see what he can do on the page, and what’s still getting ready.
Preload CSS
One way to improve the performance of your site is to preload CSS files. Preloading CSS files allows the browser to download and cache the CSS files in advance, which can result in faster page load.
<link rel="preload">
tells the browser to download and cache a resource (like a script or a stylesheet) as soon as possible. It’s helpful when you need that resource a few seconds after loading the page, and you want to speed it up.
The browser doesn’t do anything with the resource after downloading it. Scripts aren’t executed, stylesheets aren’t applied. It’s just cached – so that when something else needs it, it’s available immediately.
First of all let’s see what styles we have on the page:
/**
* Display all styles on the header
*
* @return void
* @since 1.0.0
*/
function detect_enqueued_styles() {
if ( !current_user_can( 'administrator' ) ) return;
global $wp_styles;
echo '<pre> Styles: ';
foreach( $wp_styles->queue as $handle ) :
echo $handle . ' | ';
endforeach;
echo '</pre>';
}
add_action( 'wp_print_styles', 'detect_enqueued_styles' );
After we have the full list we can start working and adding preload to our css.
/**
* Usage Example
* wp_style_add_data( 'style_name', 'preload', true );
*
* @link https://developer.wordpress.org/reference/functions/wp_style_add_data/
*
* @param String $tag The original enqueued <script src="...> tag
* @param String $handle The registered unique name of the script
* @param String $href The style tag href parameter as sent by `script_loader_tag` filter on WP_Styles::do_item
* @param String $media The style tag media parameter as sent by `script_loader_tag` filter on WP_Styles::do_item
* @since 1.0.0
*/
function register_preload_attribute( $tag, $handle, $href, $media ) {
if ( is_admin () ) return $tag;
if ( wp_styles()->get_data( $handle, 'preload' ) ) {
return '<link rel="stylesheet preload" as="style" href="' . $href . '" media="all" />';
}
return $tag;
}
add_filter( 'style_loader_tag', 'register_preload_attribute', 10, 4 );
We can also apply to all styles and make our own logic:
function add_rel_preload_to_all( $tag, $handle, $href, $media ) {
if ( is_admin () ) return $tag;
if ( strpos( $handle, 'preload' ) ) return $tag;
return '<link rel="stylesheet preload" as="style" href="' . $href . '" media="all" />';
}
add_filter( 'style_loader_tag', 'add_rel_preload_to_all', 10, 4 );
Load CSS Asynchronously
This can improve your speed score but it should be tested to not cause bad user exp.
function async_css_to_all( $tag, $handle, $href, $media ) {
if ( is_admin() ) return $tag;
$tag = <<<EOT
<link rel='preload' as='style' onload="this.onload=null;this.rel='stylesheet'"
id='$handle' href='$href' type='text/css' media='all' />
EOT;
return $tag;
}
add_filter( 'style_loader_tag', 'async_css_to_all', 10, 4 );
Establish early connections
Preconnecting is the process of establishing an early connection to external resources that your site relies on, such as scripts, stylesheets, and fonts. By preconnecting, you can reduce the time it takes for the browser to fetch these resources, which can improve the performance of your site.
You can save 100–500 ms by establishing early connections to important third-party origins.
Two <link>
types can help here:
preconnect
dns-prefetch
<link rel="preconnect">
informs the browser that your page intends to establish a connection to another origin, and that you’d like the process to start as soon as possible. When the request for a resource from the pre-connected origin is made, the download starts immediately.
<link rel="preconnect" href="https://cdn.example.com">
function preconnect_scripts( $hints, $relation_type ) {
if ( 'preconnect' === $relation_type ) {
$hints[] = 'https://example.com';
}
return $hints;
}
add_filter( 'wp_resource_hints', 'preconnect_scripts', 10, 2 );
<link rel="dns-prefetch>
handles a small subset of what is handled by <link rel="preconnect">
. Establishing a connection involves the DNS lookup and TCP handshake, and for secure origins, TLS negotiations. dns-prefetch
instructs the browser to only resolve the DNS of a specific domain before it has been explicitly called.
The preconnect
hint is best used for only the most critical connections; for less critical third-party domains use <link rel=dns-prefetch>
.
<link rel="dns-prefetch" href="http://example.com">
function dns_prefetch() {
echo '<link rel="dns-prefetch" href="//cdn.example.com">';
}
add_action( 'wp_head', 'dns_prefetch', 0 );
<link rel="prerender">
asks the browser to load a URL and render it in an invisible tab. When a user clicks on a link to that URL, the page should be rendered immediately. It’s helpful when you’re really sure a user will visit a specific page next, and you want to render it faster.
<link rel="prerender" href="https://wesztyweb.com/pricing/" />
<link rel="modulepreload">
tells the browser to download, cache, and compile a JS module script as soon as possible. It’s helpful when you use ES modules in production, and you want to load your app faster.
// Our modules / classes
import MobileMenu from './modules/MobileMenu';
import HeroSlider from './modules/HeroSlider';
...
import GoogleMap from './modules/GoogleMap';
import Search from './modules/Search';
...
<link rel="modulepreload" href="/static/Logo.js" />
<link rel="modulepreload" href="/static/GoogleMap.js" />
<link rel="modulepreload" href="/static/Search.js" />
Summary
In conclusion, following these best practices can help in loading scripts efficiently in WordPress, which can improve page performance and user experience.
By minimizing script file size, using a CDN, placing script tags at the end of the body element, using async or defer attributes in script tags, and using modern script loading techniques, you can ensure that your WordPress site (well… any site, not just WP) loads scripts efficiently and provides a good user experience.