推迟加载和parsingPrimeFaces JavaScript文件

在分析使用Google PageSpeed的JSF 2.1 + PrimeFaces 4.0 web应用程序的性能时,build议除其他外推迟parsingJavaScript文件。 在带有<p:layout>的testing页面上,以及带有<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文章,其中介绍了延迟加载以及如何实现它。 您基本上需要在windowonload事件期间dynamic创build所需的<script> 。 在最简单的forms下,旧的和bug的浏览器完全被忽略,看起来像这样:

 <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()调用?

使用<o:deferredScript>

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

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

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

渲染器将​​使用OmniFaces.DeferredScript.add()编写一个<script>元素,从而传递JSF生成的资源URL。 这个JS帮助程序将依次收集资源URL,并在onload事件中为它们中的每一个dynamic创build新的<script>元素。

用法相当简单,只需使用<o:deferredScript><h:outputScript>相同的方式,使用libraryname 。 放置组件的位置并不重要,但大多数自我logging将会在<h:head>末尾 ,如下所示:

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

你可以有多个,他们最终将被加载在他们宣布的相同的顺序。


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

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

在使用这个脚本和<o:deferredScript> ,我们首先需要在生成的HTML输出中确定自动包含的脚本。 对于问题中显示的testing页面,以下脚本将自动包含在生成的HTML <head> (您可以通过右键单击webbrowser中的页面并selectView 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文件,并以与剩余脚本完全相同的顺序创build<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事件。 请注意,必须在<o:deferredScript name="primefaces.js"> onsuccess中调用DeferredPrimeFaces.apply()必须在最后一个 <o:deferredScript library="primefaces"> onsuccess中调用DeferredPrimeFaces.apply()

关于性能改进,重要的测量点是DOMContentLoaded时间,您可以在Chrome开发人员工具的“ networking”选项卡底部find。 在3年前的笔记本电脑上,由Tomcat提供的问题中显示的testing页面从〜500ms减less到〜270ms。 这是相当巨大的(差不多一半!),并在移动设备/平板电脑上带来最大的差异,因为它们渲染HTML相对较慢,触摸事件被完全阻止,直到DOM内容被加载。

值得注意的是,如果(自定义)组件库依赖于它们是否遵从JSF资源pipe理规则/指导原则,那么应该是这样。 例如,RichFaces没有自制另一个自定义图层,因此无法在其上使用<o:deferredScript> 。 另请参阅什么是资源库,以及如何使用它?

警告:如果您之后在同一视图中添加新的PrimeFaces组件,并且面临JavaScript undefined错误,那么新组件也带有自己的JS文件,这个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属性是可选的。 如果你没有,那么他们将被合并成一个延期资源。

作为一个实例,检查ZEEF站点的<body>的底部。 所有基本的PrimeFaces相关的脚本和一些站点特定的脚本被合并到第一个延迟脚本中,所有非必需的社交媒体相关的脚本被合并到第二个延迟脚本中。 关于ZEEF的性能改进,在现代硬件上testingJBoss EAP服务器时, DOMContentLoaded的时间从大约3秒缩短到大约1秒。


奖金#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时拖动了几百毫秒。

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


为遇到同样问题的其他人添加另一个解决scheme。

您将需要定制HeadRenderer的HeadRenderer以实现pagespeedbuild议的订购。 尽pipe这是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组件移动到您的渲染器的开头。 让我知道是否有兴趣,我会创build一个分叉来演示如何。

这种方法是“未来的certificate”,因为当新的资源依赖被添加到组件或者新的组件被添加到视图时,它不会中断。