推迟加载和解析PrimeFaces JavaScript文件

问题描述 投票:39回答: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>

...它列出了可以推迟的以下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)

它链接到this Google Developers article,其中解释了延迟加载以及如何实现它。你基本上需要在<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 undefinedPrimeFaces is undefined这样的错误。

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

javascript jsf jsf-2 primefaces deferred-loading
2个回答
32
投票

使用<o:deferredScript>

是的,从<o:deferredScript> 1.8.1开始,OmniFaces组件可能是新的。对于技术上感兴趣的,这是涉及的源代码:

基本上,组件将在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>事件期间为每个URL动态创建新的onload元素。

使用相当简单,只需使用<o:deferredScript><h:outputScript>相同,使用libraryname。放置组件的位置并不重要,但大多数自我记录将在<h:head>的末尾像这样:

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

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


如何在PrimeFaces中使用<o:deferredScript>

这有点棘手,实际上是因为所有那些由PrimeFaces生成的内联脚本,但仍然可以使用帮助脚本并接受jquery.js不会被延迟(但它可以通过CDN提供,请参阅后面的内容)。为了覆盖那些几乎220KiB大的PrimeFaces.xxx()文件的内联primefaces.js调用,需要创建一个小于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) {
            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()调用(你可以在脚本的底部找到)并将它们推迟到DeferredPrimeFaces.apply()调用(你可以找到脚本的一半)。请注意,可能有更多的PrimeFaces.xxx()函数需要延迟,如果您的应用程序就是这种情况,那么您可以在window.PrimeFaces = {}中自己添加它们(不,它在JavaScript中不可能有一个“全能”方法来覆盖未确定的功能)。

在使用此脚本和<o:deferredScript>之前,我们首先需要在生成的HTML输出中确定自动包含的脚本。对于问题中显示的测试页面,以下脚本会自动包含在生成的HTML <head>中(您可以通过右键单击webbrowser中的页面并选择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>

您需要跳过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中调用<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>

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

值得注意的是,(自定义)组件库的存在取决于它们是否遵守JSF资源管理规则/指南。例如RichFaces没有和homebrewed上面的另一个自定义层,使得无法在其上使用<o:deferredScript>。另见what is the resource library and how should it be used?

警告:如果您之后在同一视图上添加新的PrimeFaces组件并且面临JavaScript undefined错误,那么新组件也带有自己的JS文件的机会很大,该文件也应该延迟,因为它取决于primefaces.js。确定正确脚本的快速方法是检查生成的HTML <head>以获取新脚本,然后根据上述说明为其添加另一个<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相关的基本脚本和一些特定于站点的脚本在第一个延迟脚本中组合在一起,所有非必要的社交媒体相关脚本在第二个延迟脚本中组合在一起。至于ZEEF的性能改进,在现代硬件上的测试JBoss EAP服务器上,到DOMContentLoaded的时间从~3s变为~1s。


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

无论如何,如果你已经在使用OmniFaces,那么你总是可以使用CDNResourceHandler通过web.xml中的以下上下文参数将PrimeFaces jQuery资源委托给真正的CDN:

<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在PrimeFaces 4.0内部使用的1.10上有一些主要的性能改进,并且它完全向后兼容。在初始化ZEEF上的拖放时,它节省了几百毫秒。


3
投票

最初发布作为Defer primefaces.js loading的答案


为遇到相同问题的其他人添加另一个解决方案。

您需要自定义primefaces HeadRenderer以实现pagespeed建议的排序。虽然这可以由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标记编写自定义组件,然后将其绑定到覆盖上述行为的渲染器。

现在您不希望仅为此更改复制整个方法,添加名为“last”的构面并将脚本资源作为新的deferredScript组件移动到渲染器的开头可能更简洁。让我知道是否有兴趣,我将创建一个分支来演示如何。

这种方法是“未来证明”,因为它不会因为新的资源依赖关系被添加到组件中,或者随着新组件添加到视图中而中断。

© www.soinside.com 2019 - 2024. All rights reserved.