Skip to content

Javascript Best Practices

Google page & site speed are increasingly important factors in your SEO ranking.

As of March 2021, Google has continued to update their PageSpeed Insights tool. They’ve overhauled some of their calculations to make achieving page speeds in the 90s difficult if your site uses a lot of interactivity and Javascript.

While Mattermind isn’t a web development & design shop, we do undertake that on behalf of several of our clients. And, we’ve learned some Javascript best practices to help keep your page speed fast while also allowing for the modern, interactive web.

These should not be considered gospel. We ourselves are still learning, and as noted we aren’t a webdev shop. If you’re experienced in this field, they’ll seem like rudimentary tweaks. But, if you’re starting from scratch, these concepts and approaches may be useful to you. They have absolutely helped our clients’ web properties post blistering speeds on PageSpeed Insights.

Minify/package Javascript

This one is low-hanging fruit. Make sure you’re at the very least minifying your own Javascript. If you’re able to refactor your site to use asynchronous module definition (AMD) — great! This usually requires a more application-like approach to building a website than a lot of web designers are used to, but this approach helps optimize your Javascript by only loading what’s needed.

Place before closing <body>

When a browser is parsing your page’s HTML and it hits a <script> tag — either inline/local or external scripts — it stops the process of processing the HTML & building the DOM tree while the browser loads and executes the Javascript. This is a blocking operation that not only hurts your PageSpeed but also user experience.

To make matters worse, these scripts can’t see the DOM items below unless they’re configured to only execute after a DOM ready event. In which case… why would they need to be higher up in the page?

It’s usually best to place your script tags right before your page’s closing <body> tag. However, while this solves the blocking issue, it can cause a Javascript form of FoUS (“flash of unstyled content”), especially if the script is a large external one and the end user is on a slower internet connection.

But, generally, this is a pretty good habit to use. For more, you can defer/async the <script>.

Use defer/async

These are only for external <script>s (e.g. those loaded from a src URL)

defer

The defer attribute on your <script> tag instructs the browser to not worry about waiting for the script to load and instead continue processing the HTML and build the DOM. The browser will load this script in the background and then run it when the DOM is built/ready. In other words, this has the added side effect of being a clever way to wrap your script in a DOM ready event.

Any script you defer keeps the same relative load & execution order, just like a regular non-defer <script> tag.

async

The async attribute invokes somewhat similar behavior to defer when used in that it makes the script non-blocking, but with one very important difference: async scripts are COMPLETELY independent from the DOM and other <script>s on the page.

While defer scripts are executed on DOM ready and are loaded in the same relative order, async scripts will load and execute in whatever order they happen to load. A larger external script set to async that’s the first <script> tag may load and execute after a smaller async script that comes later. The DOM and other scripts don’t wait for async scripts, and likewise async scripts don’t wait for anything.

These fully independent scripts are great for third-party scripts that don’t impact the page’s functionality, or if you build your site to allow true asynchronous initialization.

async can also override defer if both are present, commonly done for older browsers.

Be mindful of page load!

When using defer or async, users may see your page before the scripts load. Don’t forget to put some “loading” indicator or give users other visual cues for them to know what they can and can’t do until the scripts load & initialize.

Remove unused Javascript

A common approach is to include all of the Javascript <script> tags your various pages will need in your base page template. But often, most pages on your site don’t require all of the Javascript, and your page speed suffers as a result.

For example, several pages on your site may use Google Maps API. Loading the Google Maps API on every single page would be a waste of resources (and cause a major overhead due to GMAPI’s size and render blocking!)

Instead, a better approach is to use Phosis’s multiple page template feature to have different wrappers for pages by Javascript features required. For example, you can have a separate page template for pages that require Google Maps, and use that page template only on those pages.

There are several more advanced approaches you can also try, such as using custom fields in Article Styles, content blocks in Page Templates, dynamically adding required Javascript (which we’ll cover below), or a combination of these approaches features.

Dynamically include Javascript

We’ve found that this is perhaps the most impactful thing you can do to dramatically decrease various Javascript-related penalties in PageSpeed.

At its most basic, this involves dynamically adding <script> tags to your DOM after the DOM is ready. These behave as async by default, but you can customize their behavior:

function dynamicLoadScript(src) {
    // Create our script element
    let script = document.createElement('script');

    // Configure its attributes
    script.src = src;
    // Uncomment this line to force synchronous script load
    // script.async = false;

    document.body.append(script);
}

On DOM loaded

Then, here’s a basic example of using that function:

<script>
    document.addEventListener('DOMContentLoaded', () => {
        dynamicLoadScript("https://some/external/script.js");
        dynamicLoadScript("/yourlocal/script.min.js");
    });
</script>

But even when implementing this approach, you may see that PageSpeed still complains about some of your Javascript. Fortunately, now that you’ve gone down this road, you can now take things one step further and really reduce your page’s load times.

The DOMContentLoaded event vs. load

Note that DOMContentLoaded is not the same as load. Mixing these two up is a really common mistake when you’re getting started.

  1. DOMContentLoaded fires earlier in the initial load & render lifecycle when the HTML document has been completely loaded and parsed, but does not wait for images, stylesheets, frames, etc. to load.
  2. load fires towards the end of the initial load & render lifecycle when the whole page and all of its dependendent resources have loaded or errored out, such as images and stylesheets.

Use other events

DOMContentLoaded isn’t the only thing you can listen for. Perhaps you have a help/support chat widget that you load from a third-party source? It would likely make sense to load its script on the document’s mousemove and touchmove events.

Something like this could work (warning: this specific code isn’t tested, but the concept has been used on sites built by Mattermind with success).

<script>
    // Prevent double initialization, just in case
    // the browser's fast enough to call events
    // multiple times before they're removed.
    let isJsInit = false;

    // Pass a space-delimited list of events
    function addMultipleListeners(el, s, fn) {
      s.split(' ').forEach(e => el.addEventListener(e, fn, false));
    }
    function removeMultipleListeners(el, s, fn) {
      s.split(' ').forEach(e => el.removeEventListener(e, fn, false));
    }

    // Breaking this out into its own function 
    // so that it can be easily unbound rather
    // than relying on our isJsInit bool, which
    // is just belt & suspenders.
    function initJs() {
        if(!isJsInit) {
            removeMultipleListeners(window, 'mousemove touchmove', initJs);
            dynamicLoadScript("https://some/external/script.js");
            dynamicLoadScript("/yourlocal/script.min.js");
            isJsInit = true;
        }
    }

    // And attach!
    addMultipleListeners(window, 'mousemove touchmove', initJs);
</script>

Defer until scroll/in view

Or perhaps you’re using a search widget or Google Maps API autocomplete, but they are further down the page. You can listen for scroll events and only load those widgets when the user begins scrolling. You can take this much further and do 100% deferred loading of images AND scripts that are out of view — in fact, we recommend it, but it’s outside the scope of this single page of suggestions.

Computer or mobile detection

Or maybe you have a script that’s appropriate for desktop but not mobile (or vice-versa). You can use any method of detecting mobile devices in Javascript that you prefer and only include the scripts for one or the other. We recommend AGAINST doing navigator.userAgent or navigator.platform-based detection, and ourselves use either feature detection or CSS media query:

// Feature detection
let isMobile = window.matchMedia("(pointer:coarse)").matches;

// CSS media query
let isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;

You can then use this boolean to load only what’s needed for the user’s current platform:

<script>
    let isMobile = window.matchMedia("(pointer:coarse)").matches;

    document.addEventListener('DOMContentLoaded', () => {
        if(isMobile) {
            // Load your mobile-specific dynamic scripts
            // Do other things
        } else {
            dynamicLoadScript("https://some/external/script.js");
            dynamicLoadScript("/yourlocal/script.min.js");
        }
    });
</script>