Skip to content

Loading External JavaScript

The problem with loading external JavaScript files is that it usually has a negative impact on Google page ranking metrics.
For that reason it is a good idea to defer loading external libraries until after a user has interacted with a page.

Instead of loading the external source file on page load and rendering the widget directly or after the document has loaded, it is often better to render a hardcoded fake version of the widget, and replace that with the real thing when the user interacts with the page.

First rendering a fake version helps avoid layout shifts.

Load Trigger

The following example will load a external library when a user interacts with the page by observing mouse and touch events.

In some cases a click or focus event is more suited, depending on the type of service or library being integrated (for example video players).

This approach is very suited for external services, for example, a live chat widgets or search providers.

Example: loading a vanilla JS library from vanilla JS

<div id="fake-widget-placeholder">
    ...
</div>

<script>
(function () {
    function init() {
        const script = document.createElement('script')
        script.src = '<?= $escaper->escapeUrl($block->getViewFileUrl('My_Module/js/my-library.js')) ?>';
        // In this example the function initLibrary would be provided by the loaded file
        script.addEventListener('load', () => initLibrary('fake-widget-placeholder'));
        document.head.append(script);
    }

    // trigger load on mobile by any interaction
    document.body.addEventListener('touchstart', init, {once: true});

    // trigger load on desktop on any mouse movement
    document.body.addEventListener('mouseover', init, {once: true});
})();
</script>

Alpine Components

To load an external JS library that provides an alpine component, the main obstacle is the calling of the initialization methods for x-data and x-init only when the external library has loaded.

This can be accomplished with the help of Alpine.initializeComponent(target).

Example: loading an Alpine component from vanilla JS

Here is an example using the Alpine Product 360 library.

<script>
    function initExternalAlpineComponent() {
        const script = document.createElement('script');
        script.type = 'module';
        script.src = 'https://cdn.jsdelivr.net/gh/moveideas/alpine-product-360@1.1.1/dist/index.min.js';

        script.onload = () => {
            const target = document.getElementById('target');
            const url1 = 'https://picsum.photos/300/350?id=1',
                  url2 = 'https://picsum.photos/300/350?id=2';
            // because the library is a JavaScript module,
            // we need to call alpineCarousel.default() 
            target.setAttribute('x-data', `alpineCarousel.default(['${url1}', '${url2}'], {infinite: true})`);
            target.setAttribute('x-init', 'start()');

            Alpine.initializeComponent(target);
        };

        document.head.appendChild(script);
    }
</script>
<div>
    <h2>Lazy Initialization of an Alpine Component</h2>

    <button type="button" class="btn"
            onclick="initExternalAlpineComponent()">Load Alpine Component</button>
    <div id="target">
        <img
            :src="carousel.currentPath"
            @mouseup="handleMouseUp"
            @mousedown="handleMouseDown"
            @mousemove="handleMouseMove"
            @mouseleave="handleMouseLeave"
            draggable="false"
        />
    </div>
</div>

Nested Alpine Component

Chances are the new library should be initialized as a nested component.
This can lead to issues because the Alpine.js markup for the nested component is not meant to be evaluated in the scope of the outer component.

The nested component DOM needs to be removed from evaluation, until the new nested component is initialized and ready.

There are many ways to accomplish this, but one way I like using is to hide the nested component HTML from the parent scope until the nested component is loaded, by placing it in a <script type="text/html"> element.
When the external library is loaded, the script tag innerHTML is then copied into the target component about to be initialized.

Example: loading a nested Alpine component

Here is the same example as above, except that this time it is initialized as a nested component.

<script>
    function initialComponent() {
        return {
            initProduct360() {
                const script = document.createElement('script');
                script.type = 'module';

                script.addEventListener('load', () => {
                    const target = document.getElementById('target');
                    const url1 = 'https://picsum.photos/300/350?id=1',
                          url2 = 'https://picsum.photos/300/350?id=2';
                    // because the library is a JavaScript module,
                    // we need to call alpineCarousel.default()
                    target.setAttribute('x-data', `alpineCarousel.default(['${url1}', '${url2}'], {infinite: true})`);
                    target.setAttribute('x-init', 'start()');

                    // inject the nested component into the DOM
                    target.innerHTML = document.getElementById('target-content').innerHTML;

                    Alpine.initializeComponent(target);
                });
                script.src = 'https://cdn.jsdelivr.net/gh/moveideas/alpine-product-360@1.1.1/dist/index.min.js';
                document.head.appendChild(script);
            }
        }
    }
</script>
<div x-data="initialComponent()">
    <h2>Lazy Initialization of an Alpine Component</h2>

    <button type="button" class="btn" @click="initProduct360()">Load Alpine Component</button>
    <div id="target"></div>
    <!-- hide the component from the browser -->
    <script type="text/html" id="target-content">
        <img
            :src="carousel.currentPath"
            @mouseup="handleMouseUp"
            @mousedown="handleMouseDown"
            @mousemove="handleMouseMove"
            @mouseleave="handleMouseLeave"
            draggable="false"
        />
    </script>
</div>