推迟加载和解析PrimeFaces JavaScript文件

Bal*_*usC 41 javascript jsf deferred-loading primefaces jsf-2

在使用Google PageSpeed分析JSF 2.1 + PrimeFaces 4.0 webapp的性能时,它建议推迟解析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>
Run Code Online (Sandbox Code Playgroud)

...它列出了可以推迟的以下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开发人员文章,其中解释了延迟加载以及如何实现它.你基本上需要<script>onload事件期间动态创建所需的window.在最简单的形式下,旧的和错误的浏览器被完全忽略,它看起来像这样:

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

好吧,如果您可以控制这些脚本,这是可行的,但所列出的脚本都被JSF强制自动包含.此外,PrimeFaces呈现一堆内嵌的脚本到HTML被直接调用输出$(xxx)jquery.jsPrimeFaces.xxx()primefaces.js.这意味着要将它们真正推迟到onload事件是不可能的,因为你最终会遇到类似$ is undefined和的错误PrimeFaces is undefined.

但是,它应该在技术上是可行的.鉴于只有jQuery不需要延迟,因为许多网站的自定义脚本也依赖它,我怎么能阻止JSF强行自动包括PrimeFaces脚本以便我可以推迟它们,我怎么能处理那些内联PrimeFaces.xxx()电话?

Bal*_*usC 32

使用 <o:deferredScript>

是的,可以使用OmniFaces 1.8.1 <o:deferredScript>以来的新组件.对于技术上感兴趣的,这是涉及的源代码:

基本上,组件将在postAddToView事件期间(因此,在视图构建时间期间)通过UIViewRoot#addComponentResource()将自身添加为新的脚本资源<body>并通过Hacks#setScriptResourceRendered()通知JSF 已经呈现脚本资源(使用Hacks类,因为没有标准的JSF API方法那个(还?)),这样JSF就不会强行自动包含/渲染脚本资源了.在Mojarra和PrimeFaces的情况下,必须设置具有键值name+library和值的上下文属性true以禁用资源的自动包含.

渲染器将​​编写一个<script>元素,通过该元素OmniFaces.DeferredScript.add()传递JSF生成的资源URL.这个JS帮助器将依次收集资源URL并<script>onload事件期间为每个URL动态创建新元素.

用法很简单,只需使用<o:deferredScript>方法一样<h:outputScript>,具有libraryname.不要紧,你放置元件,但大多数自文档将在月底<h:head>是这样的:

<h:head>
    ...
    <o:deferredScript library="libraryname" name="resourcename.js" />
</h:head>
Run Code Online (Sandbox Code Playgroud)

您可以拥有多个,并且最终将按照它们声明的顺序加载它们.


如何使用<o:deferredScript>PrimeFaces?

这有点棘手,实际上是因为所有那些由PrimeFaces生成的内联脚本,但仍然可以使用帮助脚本并且接受jquery.js不会被延迟(但它可以通过CDN提供,请参阅后面的内容).为了覆盖几乎220KiB大的文件内联PrimeFaces.xxx()调用,primefaces.js需要创建一个小于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;
}();
Run Code Online (Sandbox Code Playgroud)

保存为/resources/yourapp/scripts/primefaces.deferred.js.基本上,它的作用是捕获PrimeFaces.ab(),cw()focus()调用(你可以在脚本的底部找到),并推迟到DeferredPrimeFaces.apply()通话(你可以中途找到脚本).请注意,可能有更多的PrimeFaces.xxx()功能需要延迟,如果您的应用程序就是这种情况,那么您可以自己添加它们window.PrimeFaces = {}(不,它在JavaScript中不可能有一个"全能"方法来覆盖未确定的功能).

在使用此脚本之前<o:deferredScript>,我们首先需要在生成的HTML输出中确定自动包含的脚本.对于问题中显示的测试页面,以下脚本会自动包含在生成的HTML中<head>(您可以通过右键单击webbrowser中的页面并选择" 查看源"来查找):

<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>
Run Code Online (Sandbox Code Playgroud)

您需要跳过该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>
Run Code Online (Sandbox Code Playgroud)

现在所有总大小约为516KiB的脚本都被推迟到onload事件中.请注意,DeferredPrimeFaces.begin()必须调用onbegin,<o:deferredScript name="primefaces.js">并且DeferredPrimeFaces.apply()必须onsuccess最后 调用<o:deferredScript library="primefaces">.

如果您正在使用PrimeFaces 6.0或更新版本,其中primefaces.js已被替换为core.jscomponents.js,请使用以下代码:

<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>
Run Code Online (Sandbox Code Playgroud)

至于性能改进,重要的测量点是DOMContentLoaded您可以在Chrome开发人员工具的" 网络"选项卡底部找到的时间.Tomcat在一台3岁的笔记本电脑上提供的问题中显示的测试页面,从~500ms减少到~270ms.这是相对巨大的(几乎是一半!)并且在移动设备/平板电脑上产生最大的差异,因为它们使HTML相对较慢并且触摸事件被完全阻止,直到加载DOM内容.

值得注意的是,(自定义)组件库的存在取决于它们是否遵守JSF资源管理规则/指南.例如,RichFaces没有和自制的另一个自定义层,使其无法使用<o:deferredScript>它.另请参阅什么是资源库以及如何使用它?

警告:如果您之后在同一视图上添加新的PrimeFaces组件并且面临JavaScript undefined错误,那么新组件也有自己的JS文件也应该推迟的机会很大,因为它依赖于它primefaces.js.确定正确脚本的快速方法是检查生成<head>的新脚本HTML ,然后<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" ... />
Run Code Online (Sandbox Code Playgroud)

...将以两个组合的延迟脚本结束,这些脚本会相互同步加载.注意:该group属性是可选的.如果您没有,那么它们将全部合并为一个延迟资源.

作为一个活生生的例子,检查底部<body>的的ZEEF网站.所有与PrimeFaces相关的基本脚本和一些特定于站点的脚本在第一个延迟脚本中组合在一起,所有非必要的社交媒体相关脚本在第二个延迟脚本中组合在一起.至于ZEEF的性能改进,在现代硬件上的测试JBoss EAP服务器上,时间DOMContentLoaded从~3s变为~1s.


奖金#2:将PrimeFaces jQuery委托给CDN

在任何情况下,如果您已经在使用OmniFaces,那么您始终可以使用CDNResourceHandler以下上下文参数将PrimeFaces jQuery资源委派给真正的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>
Run Code Online (Sandbox Code Playgroud)

请注意,jQuery 1.11在PrimeFaces 4.0内部使用的1.10上有一些主要的性能改进,并且它完全向后兼容.在初始化ZEEF上的拖放时,它节省了几百毫秒.