Reporter le chargement et l'analyse des fichiers JavaScript de PrimeFaces

en analysant les performances d'un JSF 2.1 + PrimeFaces 4.0 webapp avec Google PageSpeed , il recommande entre autres de différer l'analyse des fichiers JavaScript. Sur une page 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 énumère les fichiers JavaScript suivants qui pourraient être différés:

  • primefaces.js (219.5 KiB)
  • jquery-plugins.js (à 191,8 Kio)
  • jquery.js (95.3 Kio)
  • layout.js (76.4 Kio)
  • fileupload.js (23.8 Kio)
  • watermark.js (4.7 Kio)

il renvoie à cet article de Google Developers dans lequel le chargement différé est expliqué ainsi que la façon de le réaliser. Vous devez créer dynamiquement le souhaité <script> pendant l'événement onload du window . À sa forme la plus simple où les navigateurs anciens et buggy sont complètement ignorés, il ressemble à ceci:

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

OK, c'est faisable si vous avez le contrôle sur ces scripts, mais les scripts listés sont tous automatiquement inclus par JSF. En outre, PrimeFaces rend un tas de scripts en ligne à la sortie HTML qui appellent directement $(xxx) de jquery.js et PrimeFaces.xxx() de primefaces.js . Cela signifierait qu'il ne serait pas facile de vraiment les reporter à l'événement onload car vous ne finiriez 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 beaucoup de scripts personnalisés du site s'y réfèrent également, comment pourrais-je bloquer JSF de force auto-y compris les scripts PrimeFaces afin que je puisse les reporter, et comment pourrais-je traiter ceux en ligne PrimeFaces.xxx() ?

38
demandé sur BalusC 2014-04-19 22:07:17

2 réponses

Utiliser <o:deferredScript>

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

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

le renderer écrira un élément <script> avec OmniFaces.DeferredScript.add() par lequel L'URL de la ressource générée par JSF est passée. Ce helper js recueillera à son tour les URLs de la ressource et créera dynamiquement de nouveaux <script> éléments pour chacun d'entre eux lors de l'événement onload .

l'usage 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 seraient dans le fin 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 que elles sont déclarées.


comment utiliser <o:deferredScript> avec PrimeFaces?

C'est un peu délicat, en effet à cause de tous ces scripts inline générés par PrimeFaces, mais encore faisable avec un script helper et accepter que jquery.js ne sera pas différé (it can cependant être servi via un CDN, voir plus loin). Afin de couvrir ceux inline PrimeFaces.xxx() appels à primefaces.js fichier qui est presque 220KiB grand, un script helper doit être créé qui est inférieur à 0.5 KiB 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) {
            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;
}();

Enregistrer en tant que /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 trouver dans le bas du script) et de les reporter à l'appel DeferredPrimeFaces.apply() (comme vous pouvez trouver à moitié le script). Notez qu'il y a peut-être d'autres fonctions PrimeFaces.xxx() qui doivent être différé, si c'est le cas dans votre application, alors vous pouvez les ajouter vous-même dans window.PrimeFaces = {} (Non, il est en JavaScript impossible d'avoir une méthode "catch-all" pour couvrir les fonctions indéterminées).

avant d'utiliser ce script et <o:deferredScript> , nous devons d'abord déterminer les scripts auto-inclus dans la sortie HTML générée. Pour la page de test comme indiqué dans la question, les scripts suivants sont auto-inclus dans le HTML généré <head> (vous pouvez trouver ceci par droit de coller la page dans webbrowser et en choisissant View 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 sauter le fichier jquery.js et créer <o:deferredScripts> dans le même ordre pour les scripts restants. Le nom de la ressource est la partie après /javax.faces.resource/ excluant la cartographie JSF ( .xhtml dans mon cas). Le nom de la Bibliothèque est représenté par le paramètre de requête ln .

ainsi, ce 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>

maintenant tous les scripts avec une taille totale d'environ 516KiB sont reportés à l'événement onload . Notez que DeferredPrimeFaces.begin() doit être appelé onbegin de <o:deferredScript name="primefaces.js"> et que DeferredPrimeFaces.apply() doit être appelé onsuccess de la dernier <o:deferredScript library="primefaces"> .

en ce qui concerne l'amélioration de la performance, le point de mesure important est le DOMContentLoaded comme vous pouvez trouver en bas de 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, il est passé de ~500ms à ~270ms. C'est relativement énorme (presque la moitié!) et fait la plus grande différence sur les mobiles / tablettes car ils rendent HTML relativement lent et les événements tactiles sont complètement bloqués jusqu'à ce que le contenu DOM est chargé.

devrait être noté que vous êtes dans le cas de bibliothèques de composants (personnalisées) dépendant de si elles obéissez ou non aux règles/lignes directrices de gestion des ressources de JSF. RichFaces par exemple ne l'a pas fait et homebrewed une autre couche personnalisée au-dessus de lui, ce qui rend impossible d'utiliser <o:deferredScript> sur elle. Voir aussi qu'est-ce que la bibliothèque de ressources et comment doit-il être utilisé?

avertissement: si vous ajoutez de nouveaux PrimeFaces composants sur la même vue après et sont confrontés à des erreurs JavaScript undefined , alors la chance est grande que le nouveau component 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 comprendre le bon script serait de vérifier le HTML généré <head> pour le nouveau script et ensuite ajouter un autre <o:deferredScript> pour lui basé sur les instructions ci-dessus.


Bonus: CombinedResourceHandler reconnaît <o:deferredScript>

si vous utilisez des OmniFaces CombinedResourceHandler , alors il est bon de savoir qu'il reconnaît de façon transparente <o:deferredScript> et combine tous les scripts différés avec le même attribut group dans une seule ressource différée. E. g. c' ...

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

... se retrouvera dans deux scripts d'différé combinés qui sont chargés de façon synchrone l'un après l'autre. Note: l'attribut group est facultatif. Si vous n'en avez pas, ils seront tous combinés en une seule ressource différée.

comme exemple vivant, 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é. En ce qui concerne L'amélioration des performances de ZEEF, sur un serveur EAP de test JBoss sur le matériel moderne, le temps de DOMContentLoaded est passé de ~3s à ~1s.


Bonus #2: délégué PrimeFaces jQuery pour CA

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 param de contexte 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>

noter que jQuery 1.11 a quelques améliorations de performance majeures par rapport à 1.10 utilisé à L'interne par PrimeFaces 4.0 et que c'est parfaitement compatible. Il a sauvé quelques centaines de millisecondes en initialisant drag'n'Drop sur ZEEF.

32
répondu BalusC 2017-05-23 12:32:02

initialement publié comme réponse à déférer primefaces.chargement js


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

vous aurez besoin de personnaliser les primefaces HeadRenderer pour obtenir l'ordre recommandé pageespeed. Bien que ce soit quelque chose qui aurait pu être implémenté par PrimeFaces, Je ne le vois pas dans v5.2.RC2. Ce sont les lignes encodeBegin qui ont besoin de changement:

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

il suffit D'écrire un composant personnalisé pour la balise head , puis de le lier à un renderer qui l'emporte sur le comportement ci-dessus.

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

cette approche est une" preuve de l'avenir " dans le sens où elle ne casse pas car de nouvelles dépendances de ressources sont ajoutées aux composants, ou alors que de nouveaux composants sont ajoutés à la vue.

3
répondu Timir 2017-05-23 12:32:02