Подтвердить что ты не робот

Отложить загрузку и разбор файлов JavaScript PrimeFaces

Анализируя производительность JSF 2.1 + PrimeFaces 4.0 webapp с Google PageSpeed ​​, он рекомендует, в частности, откладывать анализ файлов JavaScript. На тестовой странице с <p:layout> и формой с <p:watermark> и <p:fileUpload>, которая выглядит следующим образом...

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

... в нем перечислены следующие файлы JavaScript, которые можно отложить:

  • primefaces.js (219.5KiB)
  • jquery-plugins.js (191.8KiB)
  • jquery.js (95.3KiB)
  • layout.js (76.4KiB)
  • fileupload.js (23.8KiB)
  • watermark.js (4.7KiB)

Он ссылается на эту статью Google Developers, в которой объясняется отложенная загрузка, а также как ее достичь. Вам в основном нужно динамически создать желаемый <script> в течение onload события window. В своей простейшей форме, когда старые и багги-браузеры полностью игнорируются, это выглядит так:

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

Хорошо, это выполнимо, если у вас есть контроль над этими сценариями, но перечисленные скрипты все автоматически включаются JSF. Кроме того, PrimeFaces предоставляет кучу встроенных скриптов для вывода HTML, которые напрямую вызывают $(xxx) из jquery.js и PrimeFaces.xxx() из primefaces.js. Это означало бы, что нелегко было бы действительно отложить их до события onload, поскольку вы только закончите с ошибками, такими как $ is undefined и PrimeFaces is undefined.

Но это должно быть технически возможно. Учитывая, что только jQuery не нужно откладывать, так как многие пользовательские скрипты сайта также полагаются на него, как я могу блокировать JSF принудительно автоматически, включая скрипты PrimeFaces, чтобы я мог их отложить, и как я могу справиться с этими inline PrimeFaces.xxx() вызывает?

4b9b3361

Ответ 1

Используйте <o:deferredScript>

Да, возможно с компонентом <o:deferredScript>, который является новым, поскольку OmniFaces 1.8.1. Для технически заинтересованных, здесь задействован исходный код:

В принципе, компонент будет во время события postAddToView (таким образом, в течение времени построения представления) через UIViewRoot#addComponentResource() добавить себя как новый ресурс script в конце <body> и через Hacks#setScriptResourceRendered() уведомить JSF о том, что ресурс script уже отображен (используя Hacks, поскольку для этого не существует стандартного API API JSF (пока?)), так что JSF не будет принудительно автоматически включать/отображать ресурс script. В случае Mojarra и PrimeFaces должен быть установлен атрибут контекста с ключом name+library и значение true, чтобы отключить автоматическое включение ресурса.

Средство рендеринга будет писать элемент <script> с OmniFaces.DeferredScript.add(), посредством которого передается URL-адрес ресурса, генерируемого JSF. Этот помощник JS, в свою очередь, будет собирать URL-адреса ресурсов и динамически создавать новые элементы <script> для каждого из них во время события onload.

Использование довольно просто, просто используйте <o:deferredScript> так же, как <h:outputScript>, с library и name. Неважно, где вы размещаете компонент, но большая часть самодокументирования будет в конце <h:head> следующим образом:

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

У вас может быть несколько из них, и в конечном итоге они будут загружены в том же порядке, в каком они объявлены.


Как использовать <o:deferredScript> с помощью PrimeFaces?

Это немного сложно, ведь из-за всех встроенных скриптов, созданных PrimeFaces, но все же выполняемых с помощью помощника script и принятия того, что jquery.js не будет отложен (однако он может быть передан через CDN, увидим позже). Чтобы охватить те встроенные PrimeFaces.xxx() вызовы в файл primefaces.js, размер которого почти 220KiB, необходимо создать вспомогательный script, который меньше 0,5KiB уменьшенная:

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

Сохраните его как /resources/yourapp/scripts/primefaces.deferred.js. В основном, все, что он делает, это захват вызовов PrimeFaces.ab(), cw() и focus() (как вы можете найти в нижней части script) и отложить их до вызова DeferredPrimeFaces.apply() (как вы можете найти на полпути script). Обратите внимание, что возможно больше функций PrimeFaces.xxx(), которые нужно отложить, если это так в вашем приложении, то вы можете добавить их самостоятельно внутри window.PrimeFaces = {} (нет, на JavaScript не возможно иметь "все-таки" "метод для покрытия неопределенных функций).

Прежде чем использовать эти script и <o:deferredScript>, сначала нам нужно определить сценарии с автосохранением в сгенерированном HTML-выходе. Для тестовой страницы, как показано в вопросе, следующие сценарии автоматически включаются в сгенерированный HTML <head> (вы можете найти это, щелкнув правой кнопкой страницу в веб-браузере и выбрав "Просмотр источника" ):

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

Вам нужно пропустить файл jquery.js и создать <o:deferredScripts> в точно таком же порядке для остальных скриптов. Имя ресурса - это часть после /javax.faces.resource/, исключая отображение JSF (.xhtml в моем случае). Имя библиотеки представлено параметром запроса ln.

Таким образом, это должно сделать:

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

Теперь все те скрипты с общим размером около 516KiB откладываются до события onload. Обратите внимание, что DeferredPrimeFaces.begin() должен быть вызван в onbegin из <o:deferredScript name="primefaces.js"> и что DeferredPrimeFaces.apply() должен быть вызван в onsuccess last <o:deferredScript library="primefaces">.

Что касается улучшения производительности, важной точкой измерения является время DOMContentLoaded, как вы можете найти в нижней части вкладки "Сеть" инструментов разработчика Chrome. С тестовой страницей, как показано в вопросе, обслуживаемом Tomcat на 3-летнем ноутбуке, он уменьшился с ~ 500 мс до ~ 270 мс. Это относительно огромно (почти половина!) И делает наибольшую разницу на мобильных телефонах/планшетах, поскольку они делают HTML относительно медленным, а события касания полностью блокируются до загрузки содержимого DOM.

Отмечается, что вы в случае (пользовательских) библиотек компонентов зависят от того, подчиняются ли они правилам/правилам управления ресурсами JSF или нет. Например, RichFaces не делал и не вызывал на нем другой пользовательский слой, что делало невозможным использование <o:deferredScript> на нем. См. Также что такое библиотека ресурсов и как ее использовать?

Внимание:если вы добавляете новые компоненты PrimeFaces в одно и то же представление и сталкиваетесь с ошибками JavaScript undefined, тогда вероятность большая, что новый компонент также имеет свой собственный JS файл, который также должен быть отложен, поскольку он зависит от primefaces.js. Быстрый способ определить правильный script - проверить сгенерированный HTML <head> для нового script, а затем добавить еще один <o:deferredScript> для него на основе приведенных выше инструкций.


Бонус: CombinedResourceHandler распознает <o:deferredScript>

Если вы используете OmniFaces CombinedResourceHandler, тогда хорошо знать, что он прозрачно распознает <o:deferredScript> и объединяет все отложенные сценарии с тем же атрибутом group в один отложенный ресурс. Например. это...

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

... закончится двумя комбинированными отложенными сценариями, которые загружаются синхронно друг за другом. Примечание: атрибут group не является обязательным. Если у вас их нет, тогда все они будут объединены в один отложенный ресурс.

Как живой пример, проверьте нижнюю часть <body> сайта ZEEF. Все существенные сценарии, связанные с PrimeFaces, и некоторые скрипты, специфичные для сайта, объединены в первом отложенном script, а все несущественные сценарии, связанные со средой, объединены во втором отложенном script. Что касается улучшения производительности ZEEF, то на тестовом сервере JBoss EAP на современном оборудовании время до DOMContentLoaded перешло от ~ 3 с до ~ 1 с.


БонуС# 2: делегат PrimeFaces jQuery для CDN

В любом случае, если вы уже используете OmniFaces, вы всегда можете использовать CDNResourceHandler для делегирования ресурса jQuery PrimeFaces true CDN следующим параметром контекста в 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>

Обратите внимание, что jQuery 1.11 имеет некоторые основные улучшения производительности за 1.10, используемые внутри PrimeFaces 4.0 и полностью совместимые с ним. Он сохранил пару сотен миллисекунд при инициализации drag'n'drop на ZEEF.

Ответ 2

Первоначально опубликовано как ответ Отложить загрузку primefaces.js


Добавление другого решения к этому вопросу для всех, кто сталкивается с этим.

Вам нужно будет настроить прайс-листы HeadRenderer. Хотя это то, что может быть реализовано PrimeFaces, я не вижу его в версии v5.2.RC2. Это строки в encodeBegin, которые нуждаются в изменении:

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

Просто напишите пользовательский компонент для тега head, а затем привяжите его к рендереру, который переопределяет поведение выше.

Теперь вы не захотите дублировать весь метод только для этого изменения, может быть проще добавить фасет под названием "последний" и переместить ресурсы script в начало в вашем рендерере как новые компоненты deferredScript. Дайте мне знать, если будет интерес, и я создам вилку, чтобы продемонстрировать, как это сделать.

Этот подход является "будущим доказательством" в том смысле, что он не прерывается, поскольку новые компоненты ресурсов добавляются к компонентам или как новые компоненты добавляются в представление.