延迟加载和解析 PrimeFaces JavaScript 文件

Posted

技术标签:

【中文标题】延迟加载和解析 PrimeFaces JavaScript 文件【英文标题】:Defer loading and parsing of PrimeFaces JavaScript files 【发布时间】:2014-06-04 02:46:07 【问题描述】:

在使用 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,其中解释了延迟加载以及如何实现它。您基本上需要在windowonload 事件期间动态创建所需的&lt;script&gt;。最简单的形式是完全忽略旧的和有问题的浏览器,它看起来像这样:

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

好的,如果您可以控制这些脚本,这是可行的,但列出的脚本都是由 JSF 强制自动包含的。此外,PrimeFaces 将一堆内联脚本呈现到 html 输出,这些脚本直接从 jquery.js 调用 $(xxx) 和从 primefaces.js 调用 PrimeFaces.xxx()。这意味着将它们真正推迟到onload 事件并不容易,因为您最终只会遇到$ is undefinedPrimeFaces is undefined 之类的错误。

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

【问题讨论】:

【参考方案1】:

使用&lt;o:deferredScript&gt;

是的,可以使用自 OmniFaces 1.8.1 以来新增的 &lt;o:deferredScript&gt; 组件。对于技术上感兴趣的,这里是涉及的源代码:

UI 组件:DeferredScript HTML 渲染器:DeferredScriptRenderer JS 助手:deferred.unminified.js

基本上,组件将在postAddToView 事件期间(因此,在视图构建期间)通过UIViewRoot#addComponentResource() 将自身作为新的脚本资源添加到&lt;body&gt; 的末尾并通过Hacks#setScriptResourceRendered() 通知JSF 该脚本资源已经呈现(使用Hacks 类,因为没有标准的JSF API 方法(还没有?)),因此JSF 不会再强制自动包含/呈现脚本资源。对于 Mojarra 和 PrimeFaces,必须设置具有 name+library 键和 true 值的上下文属性,以禁用资源的自动包含。

渲染器将使用OmniFaces.DeferredScript.add() 写入&lt;script&gt; 元素,从而传递JSF 生成的资源URL。这个 JS 助手将依次收集资源 URL,并在 onload 事件期间为每个资源动态创建新的 &lt;script&gt; 元素。

用法相当简单,只需像&lt;h:outputScript&gt;一样使用&lt;o:deferredScript&gt;,加上libraryname。将组件放置在哪里并不重要,但大多数自文档将在 &lt;h:head&gt;end 中,如下所示:

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

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


如何在 PrimeFaces 中使用&lt;o:deferredScript&gt;

这有点棘手,确实是因为所有由 PrimeFaces 生成的内联脚本,但仍然可以使用辅助脚本并接受 jquery.js 不会被推迟(它可以通过 CDN 提供服务,见下文)。为了覆盖那些几乎 220KiB 大的对 primefaces.js 文件的内联 PrimeFaces.xxx() 调用,需要创建一个小于 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 中不可能有“包罗万象” " 方法来覆盖未确定的函数)。

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

您需要跳过jquery.js 文件并以完全相同的顺序为其余脚本创建&lt;o:deferredScripts&gt;。资源名称是/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() 必须在&lt;o:deferredScript name="primefaces.js"&gt;onbegin 中调用,DeferredPrimeFaces.apply() 必须在最后一个 &lt;o:deferredScript library="primefaces"&gt;onsuccess 中调用。

如果您使用的是 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 开发者工具的 Network 标签底部找到。使用 Tomcat 在 3 年旧笔记本电脑上提供的问题中所示的测试页面,它从 ~500ms 减少到 ~270ms。这是相对较大的(几乎是一半!),并且在手机/平板电脑上的差异最大,因为它们呈现 HTML 相对较慢,并且触摸事件在加载 DOM 内容之前被完全阻止。

请注意,您是否使用(自定义)组件库取决于它们是否遵守 JSF 资源管理规则/指南。例如,RichFaces 并没有在其上自制另一个自定义层,因此无法在其上使用 &lt;o:deferredScript&gt;。另见what is the resource library and how should it be used?

警告:如果您之后在同一个视图上添加新的 PrimeFaces 组件并且遇到 JavaScript undefined 错误,那么新组件也有自己的 JS 文件的可能性很大这也应该被推迟,因为它取决于primefaces.js。找出正确脚本的一种快速方法是检查生成的 HTML &lt;head&gt; 是否有新脚本,然后根据上述说明为其添加另一个 &lt;o:deferredScript&gt;


奖励:CombinedResourceHandler 识别 &lt;o:deferredScript&gt;

如果您碰巧使用了 OmniFaces CombinedResourceHandler,那么很高兴知道它可以透明地识别 &lt;o:deferredScript&gt; 并将所有具有相同 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 网站的&lt;body&gt; 底部。所有基本的 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>

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

【讨论】:

非常好的功能和有用的解释。谢谢你。通过CDNResourceHandler 检索到的资源是否也可以标记为延迟加载?在web.xml? 在 PrimeFaces 6 上,primefaces.js 分为 core.js 和 components.js。考虑到这个部门,你能更新答案吗?谢谢。 有人知道如何在 Primefaces 6.2 上应用这个吗? 我想通了。它在 Primefaces 6 中更短。&lt;h:outputScript library="js" name="primefaces.deferred.min.js" target="head" /&gt; &lt;o:deferredScript group="essential" library="primefaces" name="jquery/jquery-plugins.js" /&gt; &lt;o:deferredScript group="essential" library="primefaces" name="core.js" onbegin="DeferredPrimeFaces.begin()" /&gt; &lt;o:deferredScript group="essential" library="primefaces" name="components.js" onsuccess="DeferredPrimeFaces.apply()" /&gt;【参考方案2】:

最初作为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 组件在您的渲染器中移动到它的开头可能会更简洁。如果有兴趣,请告诉我,我将创建一个 fork 来演示。

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

【讨论】:

这不会延迟加载 PrimeFaces JS 文件。重新排序!=推迟。该问题包含一个示例JS代码sn-p,它实现了真正的延迟加载。 @BalusC:确实如此。我正在实现head 组件中的重新排序并将延迟委托给deferredScript 组件。 没问题。 Here's 使用omnifaces 的示例。 哦,现在我终于明白你了!从答案中并没有直接清楚地表明您将用 OmniFaces DeferredScript 组件替换它们。您可能想在答案中澄清这一点,以及相关的代码 sn-p。毕竟这确实也是一个不错的方法。它只不适用于CombinedResourceHandler

以上是关于延迟加载和解析 PrimeFaces JavaScript 文件的主要内容,如果未能解决你的问题,请参考以下文章

Primefaces 动态列不适用于延迟加载

当方法执行更新时刷新延迟加载的 Primefaces 数据表

如何在 JSF 上延迟 Primefaces AjaxStatus?

为Primefaces数据列表实现LazyModel时发生异常

primefaces keyup 或其他 ajax 事件延迟

与 primefaces 集成时无法加载 jsf.page