javascriptjsfjsf-2primefacesdeferred-loading

Defer loading and parsing of PrimeFaces JavaScript files


While analyzing the performance of a JSF 2.1 + PrimeFaces 4.0 webapp with Google PageSpeed, it recommends among others to defer parsing of JavaScript files. On a test page with a <p:layout> and a form with <p:watermark> and <p:fileUpload> which looks like follows ...

<p:layout>
    <p:layoutUnit position="west" size="100">Test</p:layoutUnit>
    <p:layoutUnit position="center">
        <h:form enctype="multipart/form-data">
            <p:inputText id="input" />
            <p:watermark for="input" value="watermark" />
            <p:focus for="input" />
            <p:fileUpload/>
            <p:commandButton value="submit" />
        </h:form>
    </p:layoutUnit>
</p:layout>

... it lists the following JavaScript files which could be deferred:

It links to this Google Developers article wherein deferred loading is explained as well as how to achieve it. You basically need to dynamically create the desired <script> during the onload event of the window. At its simplest form whereby old and buggy browsers are completely ignored, it looks like this:

<script>
    window.addEventListener("load", function() {
        var script = document.createElement("script");
        script.src = "filename.js";
        document.head.appendChild(script);
    }, false);
</script>

Okay, this is doable if you have control over those scripts, but the listed scripts are all forcibly auto-included by JSF. Also, PrimeFaces renders a bunch of inline scripts to HTML output which are directly calling $(xxx) from jquery.js and PrimeFaces.xxx() from primefaces.js. This would mean that it would not easily be possible to really defer them to onload event as you would only end up with errors like $ is undefined and PrimeFaces is undefined.

But, it should be technically possible. Given that only jQuery doesn't need to be deferred as many of the site's custom scripts also rely on it, how could I block JSF from forcibly auto-including the PrimeFaces scripts so that I can defer them, and how could I deal with those inline PrimeFaces.xxx() calls?


Solution

  • Use <o:deferredScript>

    Yes, it is possible with the <o:deferredScript> component which is new since OmniFaces 1.8.1. For the technically interested, here's the involved source code:

    Basically, the component will during the postAddToView event (thus, during the view build time) via UIViewRoot#addComponentResource() add itself as a new script resource in end of <body>.

    The renderer will write a <script> element with OmniFaces.DeferredScript.add() whereby the JSF-generated resource URL is passed. This JS helper will in turn collect the resource URLs and dynamically create new <script> elements for each of them during the onload event.

    The usage is fairly simple, just use <o:deferredScript> the same way as <h:outputScript>, with a library and name. It doesn't matter where you place the component, but most self-documenting would be in the end of the <h:head> like this:

    <h:head>
        ...
        <o:deferredScript library="libraryname" name="resourcename.js" />
    </h:head>
    

    You can have multiple of them and they will ultimately be loaded in the same order as they're declared.


    How to use <o:deferredScript> with PrimeFaces?

    This is a little tricky, indeed because of all those inline scripts generated by PrimeFaces, but still doable with a helper script and accepting that its jquery.js won't be deferred (it can however be served via a CDN, see later). In order to cover those inline PrimeFaces.xxx() calls to its components.js file which is almost 500KiB large in PrimeFaces 13, a helper script needs to be created which is less than 0.5KiB minified:

    DeferredPrimeFaces = function() {
        var deferredPrimeFaces = {};
        var calls = [];
        var settings = {};
        var primeFacesLoaded = !!window.PrimeFaces;
    
        function defer(name, args) {
            calls.push({ name: name, args: args });
        }
        
        deferredPrimeFaces.begin = function() {
            if (!primeFacesLoaded) {
                settings = window.PrimeFaces.settings;
                delete window.PrimeFaces;
            }
        };
    
        deferredPrimeFaces.apply = function() {
            if (window.PrimeFaces) {
                window.PrimeFaces.settings = settings;
    
                for (var i = 0; i < calls.length; i++) {
                    window.PrimeFaces[calls[i].name].apply(window.PrimeFaces, calls[i].args);
                }
            }
    
            delete window.DeferredPrimeFaces;
        };
    
        if (!primeFacesLoaded) {
            window.PrimeFaces = {
                ab: function() { defer("ab", arguments); },
                cw: function() { defer("cw", arguments); },
                focus: function() { defer("focus", arguments); },
                settings: {}
            };
        }
    
        return deferredPrimeFaces;
    }();
    

    Save it as /resources/yourapp/scripts/primefaces.deferred.js. Basically, all what it does is capturing the PrimeFaces.ab(), cw() and focus() calls (as you can find in the bottom of the script) and deferring them to the DeferredPrimeFaces.apply() call (as you can find halfway the script). Note that there are possibly more PrimeFaces.xxx() functions which need to be deferred, if that is the case in your app, then you can add them yourself inside window.PrimeFaces = {} (no, it's in JavaScript not possible to have a "catch-all" method to cover the undetermined functions).

    Before using this script and <o:deferredScript>, we first need to determine the auto-included scripts in the generated HTML output. For a test page using PrimeFaces 13 <p:inputText>, <p:fileUpload> and <p:commandButton>, the following scripts are auto-included in generated HTML <head> (you can find this by rightclicking the page in webbrowser and choosing View Source):

    <script type="text/javascript" src="/playground/jakarta.faces.resource/jquery/jquery.js.xhtml?ln=primefaces&amp;v=13.0.0"></script>
    <script type="text/javascript" src="/playground/jakarta.faces.resource/core.js.xhtml?ln=primefaces&amp;v=13.0.0"></script>
    <script type="text/javascript" src="/playground/jakarta.faces.resource/components.js.xhtml?ln=primefaces&amp;v=13.0.0"></script>
    <script type="text/javascript" src="/playground/jakarta.faces.resource/jquery/jquery-plugins.js.xhtml?ln=primefaces&amp;v=13.0.0"></script>
    <script type="text/javascript" src="/playground/jakarta.faces.resource/fileupload/fileupload.js.xhtml?ln=primefaces&amp;v=13.0.0"></script>
    

    You need to skip the jquery.js file and create <o:deferredScripts> in exactly the same order for the remaining scripts. The resource name is the part after /jakarta.faces.resource/ excluding the JSF mapping (.xhtml in my case). The library name is represented by ln request parameter.

    Thus, this should do:

    <h:head>
        ...
        <h:outputScript library="yourapp" name="scripts/primefaces.deferred.js" target="head" />
        <o:deferredScript library="primefaces" name="core.js" onbegin="DeferredPrimeFaces.begin()" />
        <o:deferredScript library="primefaces" name="components.js" />
        <o:deferredScript library="primefaces" name="jquery/jquery-plugins.js" />
        <o:deferredScript library="primefaces" name="fileupload/fileupload.js" onsuccess="DeferredPrimeFaces.apply()" />
    </h:head>
    

    Now all those scripts with a total size of almost 750KiB in PrimeFaces 13 are deferred to onload event. Note that DeferredPrimeFaces.begin() must be called in onbegin of the first <o:deferredScript library="primefaces" ...> and that DeferredPrimeFaces.apply() must be called in onsuccess of the last <o:deferredScript library="primefaces" ...>.

    As to performance improvement, important measuring point is the DOMContentLoaded time as you can find in bottom of Network tab of Chrome's developer tools. With the test page as shown in your question served by Tomcat on a 3 year old laptop, it decreased from ~500ms to ~270ms. This is relatively huge (almost the half!) and makes the most difference on mobiles/tablets as they render HTML relatively slow and touch events are fully blocked until the DOM content is loaded.

    Warning: if you're adding new PrimeFaces components on the same view afterwards and are facing JavaScript undefined errors, then the chance is big that the new component also comes with its own JS file which should also be deferred, because it's depending on primefaces.js. A quick way to figure the right script would be to check the generated HTML <head> for the new script and then add another <o:deferredScript> for it based on the above instructions.


    Bonus: CombinedResourceHandler recognizes <o:deferredScript>

    If you happen to use OmniFaces CombinedResourceHandler, then it's good to know that it transparently recognizes <o:deferredScript> and combines all deferred scripts with the same group attribute into a single deferred resource. E.g. this ...

    <o:deferredScript group="essential" ... />
    <o:deferredScript group="essential" ... />
    <o:deferredScript group="essential" ... />
    ...
    <o:deferredScript group="non-essential" ... />
    <o:deferredScript group="non-essential" ... />
    

    ... will end up in two combined deferred scripts which are loaded synchronously after each other. Note: the group attribute is optional. If you don't have any, then they will just all be combined into a single deferred resource.


    Bonus #2: delegate PrimeFaces jQuery to CDN

    In any case, if you're already using OmniFaces, then you can always use CDNResourceHandler to delegate the PrimeFaces jQuery resource to a true CDN by the following context param in web.xml:

    <context-param>
        <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
        <param-value>primefaces:jquery/jquery.js=https://code.jquery.com/jquery-3.7.0.min.js</param-value>
    </context-param>
    

    This can possibly yet further improve performance if the user has beforehand already visited another website which also uses the very same jQuery from CDN.