web-dev-qa-db-fra.com

Différer le chargement et l'analyse des fichiers JavaScript PrimeFaces

Lors de l'analyse des performances d'une webapp JSF 2.1 + PrimeFaces 4.0 avec Google PageSpeed , il recommande entre autres de différer l'analyse des fichiers JavaScript. Sur une page de test avec un <p:layout> Et un formulaire avec <p:watermark> Et <p:fileUpload> Qui ressemble à ce qui suit ...

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

... il répertorie les fichiers JavaScript suivants qui pourraient être différés:

  • primefaces.js (219,5 Ko)
  • jquery-plugins.js (191,8 Ko)
  • jquery.js (95,3 Ko)
  • layout.js (76,4 Ko)
  • fileupload.js (23,8 Ko)
  • watermark.js (4,7 Ko)

Il renvoie à cet article de Google Developers dans lequel le chargement différé est expliqué ainsi que comment y parvenir. Vous devez essentiellement créer dynamiquement le <script> Souhaité pendant l'événement onload du window. Dans sa forme la plus simple où les navigateurs anciens et bogués sont complètement ignorés, cela ressemble à ceci:

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

D'accord, c'est faisable si vous avez le contrôle sur ces scripts, mais les scripts répertoriés sont tous automatiquement inclus de force par JSF. De plus, PrimeFaces rend un tas de scripts en ligne en sortie HTML qui appellent directement $(xxx) à partir de jquery.js Et PrimeFaces.xxx() à partir de primefaces.js. Cela signifierait qu'il ne serait pas facilement possible de vraiment les reporter à l'événement onload car vous ne vous retrouveriez qu'avec des erreurs comme $ is undefined Et PrimeFaces is undefined.

Mais cela devrait être techniquement possible. Étant donné que seul jQuery n'a pas besoin d'être différé car de nombreux scripts personnalisés du site en dépendent également, comment pourrais-je empêcher JSF d'inclure automatiquement de force les scripts PrimeFaces afin de pouvoir les différer, et comment pourrais-je les gérer appels en ligne PrimeFaces.xxx()?

41
BalusC

Utilisez <o:deferredScript>

Oui, c'est possible avec le composant <o:deferredScript> Qui est nouveau depuis OmniFaces 1.8.1. Pour les techniquement intéressés, voici le code source impliqué:

Fondamentalement, le composant va lors de l'événement postAddToView (donc, pendant la construction de la vue) via UIViewRoot#addComponentResource() s'ajouter en tant que nouvelle ressource de script à la fin de <body> Et via Hacks#setScriptResourceRendered() notifier à JSF que la ressource de script est déjà rendue (en utilisant la classe Hacks car il n'y a pas approche standard de l'API JSF pour cela (encore?)), de sorte que JSF n'inclut/ne rendra plus automatiquement la ressource de script. Dans le cas de Mojarra et PrimeFaces, un attribut de contexte avec la clé name+library Et une valeur de true doit être défini afin de désactiver l'inclusion automatique de la ressource.

Le moteur de rendu écrira un élément <script> Avec OmniFaces.DeferredScript.add() par lequel l'URL de la ressource générée par JSF est transmise. Cet assistant JS collectera à son tour les URL de ressources et créera dynamiquement de nouveaux éléments <script> Pour chacun d'eux lors de l'événement onload.

L'utilisation est assez simple, il suffit d'utiliser <o:deferredScript> De la même manière que <h:outputScript>, Avec un library et name. Peu importe où vous placez le composant, mais la plupart des auto-documentations se trouveraient dans le end du <h:head> Comme ceci:

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

Vous pouvez en avoir plusieurs et ils seront finalement chargés dans le même ordre qu'ils ont été déclarés.


Comment utiliser <o:deferredScript> Avec PrimeFaces?

C'est un peu délicat, en effet à cause de tous ces scripts en ligne générés par PrimeFaces, mais toujours faisable avec un script d'aide et en acceptant que jquery.js Ne sera pas différé (il peut cependant être servi via un CDN, voir plus loin). Afin de couvrir ces appels en ligne PrimeFaces.xxx() au fichier primefaces.js D'une taille de près de 220 Ko, un script d'assistance doit être créé qui est inférieur à 0,5 Ko minifié :

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) {
            for (var i = 0; i < calls.length; i++) {
                window.PrimeFaces[calls[i].name].apply(window.PrimeFaces, calls[i].args);
            }

            window.PrimeFaces.settings = settings;
        }

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

Enregistrez-le sous /resources/yourapp/scripts/primefaces.deferred.js. Fondamentalement, tout ce qu'il fait est de capturer les appels PrimeFaces.ab(), cw() et focus() (comme vous pouvez le trouver au bas du script) et de les reporter au DeferredPrimeFaces.apply() call (comme vous pouvez le trouver à mi-chemin du script). Notez qu'il y a peut-être plus de fonctions PrimeFaces.xxx() qui doivent être différées, si c'est le cas dans votre application, alors vous pouvez les ajouter vous-même dans window.PrimeFaces = {} (Non, c'est en JavaScript pas possible avoir une méthode "fourre-tout" pour couvrir les fonctions indéterminées).

Avant d'utiliser ce script et <o:deferredScript>, Nous devons d'abord déterminer les scripts inclus automatiquement dans la sortie HTML générée. Pour la page de test comme indiqué dans la question, les scripts suivants sont automatiquement inclus dans le HTML généré <head> (Vous pouvez le trouver en cliquant avec le bouton droit sur la page dans le navigateur Web et en choisissant Voir la source ):

<script type="text/javascript" src="/playground/javax.faces.resource/jquery/jquery.js.xhtml?ln=primefaces&amp;v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/jquery/jquery-plugins.js.xhtml?ln=primefaces&amp;v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/primefaces.js.xhtml?ln=primefaces&amp;v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/layout/layout.js.xhtml?ln=primefaces&amp;v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/watermark/watermark.js.xhtml?ln=primefaces&amp;v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/fileupload/fileupload.js.xhtml?ln=primefaces&amp;v=4.0"></script>

Vous devez ignorer le fichier jquery.js Et créer <o:deferredScripts> Dans exactement le même ordre pour les scripts restants. Le nom de la ressource est la partie après /javax.faces.resource/ excluant le mappage JSF (.xhtml Dans mon cas). Le nom de la bibliothèque est représenté par le paramètre de requête ln.

Ainsi, cela devrait faire:

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

Désormais, tous ces scripts d'une taille totale d'environ 516 Ko sont reportés à l'événement onload. Notez que DeferredPrimeFaces.begin() doit être appelée dans onbegin de <o:deferredScript name="primefaces.js"> Et que DeferredPrimeFaces.apply() doit être appelée dans onsuccess du dernier <o:deferredScript library="primefaces">.

Si vous utilisez PrimeFaces 6.0 ou une version plus récente, où primefaces.js A été remplacé par core.js Et components.js, Utilisez plutôt ce qui suit:

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

Quant à l'amélioration des performances, le point de mesure important est le temps DOMContentLoaded comme vous pouvez le trouver en bas de l'onglet Réseau des outils de développement de Chrome. Avec la page de test comme indiqué dans la question servie par Tomcat sur un ordinateur portable de 3 ans, elle est passée de ~ 500ms à ~ 270ms. C'est relativement énorme (presque la moitié!) Et fait la plus grande différence sur les mobiles/tablettes car ils rendent le HTML relativement lent et les événements tactiles sont complètement bloqués jusqu'à ce que le contenu DOM soit chargé.

Il convient de noter que vous êtes dans le cas de bibliothèques de composants (personnalisés) selon qu'elles obéissent ou non aux règles/directives de gestion des ressources JSF. RichFaces, par exemple, ne l'a pas fait et a homebrewé un autre calque personnalisé dessus, rendant impossible l'utilisation de <o:deferredScript> Dessus. Voir aussi qu'est-ce que la bibliothèque de ressources et comment doit-elle être utilisée?

Attention: si vous ajoutez de nouveaux composants PrimeFaces sur la même vue par la suite et que vous faites face à des erreurs JavaScript undefined, alors les chances sont grandes que le nouveau composant est également livré avec son propre fichier JS qui devrait également être différé, car il dépend de primefaces.js. Un moyen rapide de trouver le bon script serait de vérifier le code HTML généré <head> Pour le nouveau script, puis d'ajouter un autre <o:deferredScript> Pour celui-ci en fonction des instructions ci-dessus.


Bonus: CombinedResourceHandler reconnaît <o:deferredScript>

Si vous utilisez OmniFaces CombinedResourceHandler , alors il est bon de savoir qu'il reconnaît de manière transparente <o:deferredScript> Et combine tous les scripts différés avec le même attribut group en une seule ressource différée. Par exemple. ce ...

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

... se terminera par deux scripts différés combinés qui sont chargés de manière synchrone l'un après l'autre. Remarque: l'attribut group est facultatif. Si vous n'en avez pas, ils seront tous combinés en une seule ressource différée.

À titre d'exemple en direct, vérifiez le bas de <body> Du site ZEEF . Tous les scripts essentiels liés à PrimeFaces et certains scripts spécifiques au site sont combinés dans le premier script différé et tous les scripts non essentiels liés aux médias sociaux sont combinés dans le second script différé. Quant à l'amélioration des performances de ZEEF, sur un serveur JBoss EAP de test sur du matériel moderne, le temps de DOMContentLoaded est passé de ~ 3s à ~ 1s.


Bonus n ° 2: déléguez PrimeFaces jQuery à CDN

Dans tous les cas, si vous utilisez déjà OmniFaces, vous pouvez toujours utiliser CDNResourceHandler pour déléguer la ressource PrimeFaces jQuery à un vrai CDN par le paramètre de contexte suivant dans web.xml:

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

Notez que jQuery 1.11 a quelques améliorations de performances majeures par rapport à 1.10 comme utilisé en interne par PrimeFaces 4.0 et qu'il est entièrement rétrocompatible. Cela a permis de gagner quelques centaines de millisecondes lors de l'initialisation du drag'n'drop sur ZEEF.

32
BalusC

Initialement posté comme réponse à Defer primefaces.js loading


Ajout d'une autre solution à cette question pour toute autre personne qui rencontre la même chose.

Vous devrez personnaliser les primefaces HeadRenderer pour obtenir la vitesse de pages recommandée. Bien que ce soit quelque chose qui aurait pu être implémenté par PrimeFaces, je ne le vois pas dans la v5.2.RC2. Voici les lignes de encodeBegin qui doivent être modifiées:

96         //Registered Resources
97         UIViewRoot viewRoot = context.getViewRoot();
98         for (UIComponent resource : viewRoot.getComponentResources(context, "head")) {
99             resource.encodeAll(context);
100        }

Écrivez simplement un composant personnalisé pour la balise head, puis liez-le à un moteur de rendu qui remplace le comportement ci-dessus.

Maintenant, vous ne voudriez pas dupliquer la méthode entière juste pour ce changement, il peut être plus simple d'ajouter une facette appelée "last" et de déplacer les ressources de script à son début dans votre moteur de rendu en tant que nouveaux composants deferredScript. Faites-moi savoir s'il y a de l'intérêt, et je créerai une fourchette pour montrer comment.

Cette approche est "à l'épreuve du temps" en ce sens qu'elle ne se rompt pas lorsque de nouvelles dépendances de ressources sont ajoutées aux composants ou que de nouveaux composants sont ajoutés à la vue.

3
Timir