42

在使用Google PageSpeed分析 JSF 2.1 + PrimeFaces 4.0 webapp 的性能时,它建议延迟解析 JavaScript 文件。在带有 a 的测试页面上,带有and<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>在. 在最简单的形式中,旧的和有缺陷的浏览器被完全忽略,它看起来像这样:onloadwindow

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

好的,如果您可以控制这些脚本,这是可行的,但是列出的脚本都是 JSF 强制自动包含的。此外,PrimeFaces 将一堆内联脚本呈现为 HTML 输出,这些脚本直接调用$(xxx)fromjquery.jsPrimeFaces.xxx()from primefaces.js。这意味着很难将它们真正推迟到onload事件中,因为您最终只会遇到诸如$ is undefinedand之类的错误PrimeFaces is undefined

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

4

2 回答 2

33

利用<o:deferredScript>

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

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

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

用法相当简单,只需使用与,和<o:deferredScript>相同的方法即可。放置组件的位置无关紧要,但大多数自文档将在这样的末尾<h:outputScript>libraryname<h:head>

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

您可以拥有多个它们,它们最终将以与声明它们相同的顺序加载。


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

将其另存为/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()必须在onbeginof中调用,<o:deferredScript name="primefaces.js">并且DeferredPrimeFaces.apply()必须在onsuccesslast <o:deferredScript library="primefaces">调用。

如果您使用的是 PrimeFaces 6.0 或更高版本,其中primefaces.js已被替换为core.jsand components.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 并没有在其上自制另一个自定义层,因此无法<o:deferredScript>在其上使用。另请参阅什么是资源库以及应该如何使用它?

警告:如果您之后在同一个视图上添加新的 PrimeFaces 组件并且遇到 JavaScriptundefined错误,那么新组件还带有自己的 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" ... />

... 将在两个组合的延迟脚本中结束,这些脚本彼此同步加载。注意:该group属性是可选的。如果您没有任何资源,那么它们将全部组合成一个延迟资源。

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


奖励 #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>

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

于 2014-04-19T18:07:17.430 回答
3

最初发布为延迟 primefaces.js 加载的答案


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

您将需要自定义 primefacesHeadRenderer以实现 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组件在渲染器中移动到它的开头可能会更干净。让我知道是否有兴趣,我将创建一个分叉来演示如何。

这种方法是“面向未来的”,因为它不会随着新的资源依赖项添加到组件或新组件添加到视图而中断。

于 2015-04-02T18:52:21.920 回答