问题描述
在使用 Google PageSpeed 分析 JSF 2.1 + PrimeFaces 4.0 网络应用程序的性能时,它建议推迟解析 JavaScript 文件.在带有 和带有
和
的表单的测试页面上看起来如下...
While analyzing the performance of a JSF 2.1 + PrimeFaces 4.0 webapp with Google PageSpeed, it recommends among others to defer parsing of JavaScript files. On a test page with a <p:layout>
and a form with <p:watermark>
and <p:fileUpload>
which looks like follows ...
<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 文件:
... it lists the following JavaScript files which could be deferred:
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)
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 文章,其中解释了延迟加载以及如何实现它.您基本上需要在 window
的 onload
事件期间动态创建所需的 .最简单的形式是完全忽略旧的和有问题的浏览器,它看起来像这样:
It links to this Google Developers article wherein deferred loading is explained as well as how to achieve it. You basically need to dynamically create the desired <script>
during the onload
event of the window
. At its simplest form whereby old and buggy browsers are completely ignored, it looks like this:
<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
和 PrimeFaces.xxx()
调用 $(xxx)
> 来自 primefaces.js
.这意味着很难真正将它们推迟到 onload
事件,因为您最终只会遇到诸如 $ is undefined
和 PrimeFaces is undefined 之类的错误
.
Okay, this is doable if you have control over those scripts, but the listed scripts are all forcibly auto-included by JSF. Also, PrimeFaces renders a bunch of inline scripts to HTML output which are directly calling $(xxx)
from jquery.js
and PrimeFaces.xxx()
from primefaces.js
. This would mean that it would not easily be possible to really defer them to onload
event as you would only end up with errors like $ is undefined
and PrimeFaces is undefined
.
但是,这在技术上应该是可能的.鉴于只有 jQuery 不需要延迟,因为站点的许多自定义脚本也依赖它,我如何阻止 JSF 强制自动包含 PrimeFaces 脚本以便我可以延迟它们,我该如何处理这些内联 PrimeFaces.xxx()
调用?
But, it should be technically possible. Given that only jQuery doesn't need to be deferred as many of the site's custom scripts also rely on it, how could I block JSF from forcibly auto-including the PrimeFaces scripts so that I can defer them, and how could I deal with those inline PrimeFaces.xxx()
calls?
推荐答案
使用
是的,使用 <o:deferredScript>
组件是可能的,这是自 OmniFaces 以来的新组件1.8.1.对于技术上感兴趣的人,这里是所涉及的源代码:
Use <o:deferredScript>
Yes, it is possible with the <o:deferredScript>
component which is new since OmniFaces 1.8.1. For the technically interested, here's the involved source code:
- UI 组件:
DeferredScript
- HTML 渲染器:
DeferredScriptRenderer
- JS 助手:
deferred.unminified.js
基本上,组件将在 postAddToView
事件期间(因此,在视图构建期间)通过 UIViewRoot#addComponentResource()
在 的末尾和通过
Hacks#setScriptResourceRendered()
通知JSF脚本资源已经呈现(使用 Hacks
类,因为没有标准的 JSF API 方法(还没有?)),因此 JSF 不会强制自动包含/呈现脚本资源了.在 Mojarra 和 PrimeFaces 的情况下,必须设置具有 name+library
键和 true
值的上下文属性,以禁用资源的自动包含.
Basically, the component will during the postAddToView
event (thus, during the view build time) via UIViewRoot#addComponentResource()
add itself as a new script resource in end of <body>
and via Hacks#setScriptResourceRendered()
notify JSF that the script resource is already rendered (using Hacks
class as there's no standard JSF API approach for that (yet?)), so that JSF won't forcibly auto-include/render the script resource anymore. In case of Mojarra and PrimeFaces, a context attribute with key of name+library
and a value of true
has to be set in order to disable auto-inclusion of the resource.
渲染器将使用 OmniFaces.DeferredScript.add()
编写一个 元素,从而传递 JSF 生成的资源 URL.这个 JS 助手将依次收集资源 URL 并在
onload
事件期间为每个元素动态创建新的 元素.
The renderer will write a <script>
element with OmniFaces.DeferredScript.add()
whereby the JSF-generated resource URL is passed. This JS helper will in turn collect the resource URLs and dynamically create new <script>
elements for each of them during the onload
event.
用法相当简单,只需使用 和
一样的方法,加上一个
library代码>和.将组件放在哪里并不重要,但大多数自文档化会在
的end 中,如下所示:
The usage is fairly simple, just use <o:deferredScript>
the same way as <h:outputScript>
, with a library
and name
. It doesn't matter where you place the component, but most self-documenting would be in the end of the <h:head>
like this:
<h:head>
...
<o:deferredScript library="libraryname" name="resourcename.js" />
</h:head>
你可以有多个它们,它们最终会按照声明的顺序加载.
You can have multiple of them and they will ultimately be loaded in the same order as they're declared.
这有点棘手,确实因为 PrimeFaces 生成的所有内联脚本,但仍然可以使用辅助脚本并接受 jquery.js
不会被延迟(它 但是可以通过 CDN 提供服务,见下文).为了覆盖对几乎 220KiB 大的 primefaces.js
文件的内联 PrimeFaces.xxx()
调用,需要创建一个小于 0.5KiB 的辅助脚本缩小:
This is a little tricky, indeed because of all those inline scripts generated by PrimeFaces, but still doable with a helper script and accepting that jquery.js
won't be deferred (it can however be served via a CDN, see later). In order to cover those inline PrimeFaces.xxx()
calls to primefaces.js
file which is almost 220KiB large, a helper script needs to be created which is less than 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 中不可能有一个包罗万象"的方法来覆盖未确定的函数).
Save it as /resources/yourapp/scripts/primefaces.deferred.js
. Basically, all what it does is capturing the PrimeFaces.ab()
, cw()
and focus()
calls (as you can find in the bottom of the script) and deferring them to the DeferredPrimeFaces.apply()
call (as you can find halfway the script). Note that there are possibly more PrimeFaces.xxx()
functions which need to be deferred, if that is the case in your app, then you can add them yourself inside window.PrimeFaces = {}
(no, it's in JavaScript not possible to have a "catch-all" method to cover the undetermined functions).
在使用此脚本和 之前,我们首先需要确定生成的 HTML 输出中自动包含的脚本.对于问题中显示的测试页面,以下脚本会自动包含在生成的 HTML
中(您可以通过在 webbrowser 中右键单击页面并选择 View Source):
Before using this script and <o:deferredScript>
, we first need to determine the auto-included scripts in the generated HTML output. For the test page as shown in the question, the following scripts are auto-included in generated HTML <head>
(you can find this by rightclicking the page in webbrowser and choosing View Source):
<script type="text/javascript" src="/playground/javax.faces.resource/jquery/jquery.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/jquery/jquery-plugins.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/primefaces.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/layout/layout.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/watermark/watermark.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/fileupload/fileupload.js.xhtml?ln=primefaces&v=4.0"></script>
您需要跳过 jquery.js
文件并为其余脚本以完全相同的顺序创建 .资源名称是
/javax.faces.resource/
排除 JSF 映射之后的部分(在我的例子中是 .xhtml
).库名由ln
请求参数表示.
You need to skip the jquery.js
file and create <o:deferredScripts>
in exactly the same order for the remaining scripts. The resource name is the part after /javax.faces.resource/
excluding the JSF mapping (.xhtml
in my case). The library name is represented by ln
request parameter.
因此,应该这样做:
<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
中调用,并且DeferredPrimeFaces.apply()
必须在 last 的
.onsuccess
中调用;
Now all those scripts with a total size of about 516KiB are deferred to onload
event. Note that DeferredPrimeFaces.begin()
must be called in onbegin
of <o:deferredScript name="primefaces.js">
and that DeferredPrimeFaces.apply()
must be called in onsuccess
of the last <o:deferredScript library="primefaces">
.
如果您使用 PrimeFaces 6.0 或更新版本,其中 primefaces.js
已被 core.js
和 components.js
取代>,请改用以下内容:
In case you're using PrimeFaces 6.0 or newer, where the primefaces.js
has been replaced by core.js
and components.js
, use the below instead:
<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 内容之前完全阻止触摸事件.
As to performance improvement, important measuring point is the DOMContentLoaded
time as you can find in bottom of Network tab of Chrome's developer tools. With the test page as shown in the question served by Tomcat on a 3 year old laptop, it decreased from ~500ms to ~270ms. This is relatively huge (almost the half!) and makes the most difference on mobiles/tablets as they render HTML relatively slow and touch events are fully blocked until the DOM content is loaded.
应该注意的是,您使用的(自定义)组件库取决于它们是否遵守 JSF 资源管理规则/指南.例如,RichFaces 没有并在其上自制另一个自定义层,因此无法在其上使用 .另见什么是资源库,应该如何使用?
Noted should be that you're in case of (custom) component libraries dependent on whether they obey the JSF resource management rules/guidelines or not. RichFaces for example didn't and homebrewed another custom layer over it, making it impossible to use <o:deferredScript>
on it. See also what is the resource library and how should it be used?
警告:如果您之后在同一个视图中添加新的 PrimeFaces 组件并且面临 JavaScript undefined
错误,那么新组件也出现的可能性很大使用它自己的 JS 文件,它也应该被推迟,因为它取决于 primefaces.js
.找出正确脚本的一种快速方法是检查生成的 HTML 是否有新脚本,然后添加另一个
基于它按照上述说明.
Warning: if you're adding new PrimeFaces components on the same view afterwards and are facing JavaScript undefined
errors, then the chance is big that the new component also comes with its own JS file which should also be deferred, because it's depending on primefaces.js
. A quick way to figure the right script would be to check the generated HTML <head>
for the new script and then add another <o:deferredScript>
for it based on the above instructions.
如果您碰巧使用了 OmniFaces CombinedResourceHandler
,那就太好了要知道它透明地识别 并将所有具有相同
group
属性的延迟脚本组合到单个延迟资源中.例如.这...
If you happen to use OmniFaces CombinedResourceHandler
, then it's good to know that it transparently recognizes <o:deferredScript>
and combines all deferred scripts with the same group
attribute into a single deferred resource. E.g. this ...
<o:deferredScript group="essential" ... />
<o:deferredScript group="essential" ... />
<o:deferredScript group="essential" ... />
...
<o:deferredScript group="non-essential" ... />
<o:deferredScript group="non-essential" ... />
... 将在两个合并的延迟脚本中结束,这些脚本彼此同步加载.注意:group
属性是可选的.如果您没有任何资源,那么它们将全部合并为一个延迟资源.
... will end up in two combined deferred scripts which are loaded synchronously after each other. Note: the group
attribute is optional. If you don't have any, then they will just all be combined into a single deferred resource.
作为一个活生生的例子,检查 ZEEF 网站的 底部.所有必要的 PrimeFaces 相关脚本和一些特定于站点的脚本都组合在第一个延迟脚本中,所有非必要的社交媒体相关脚本都组合在第二个延迟脚本中.关于 ZEEF 的性能提升,在现代硬件上的测试 JBoss EAP 服务器上,
DOMContentLoaded
的时间从 ~3s 到 ~1s.
As a live example, check the bottom of <body>
of the ZEEF site. All essential PrimeFaces-related scripts and some site-specific scripts are combined in the first deferred script and all non-essential social media related scripts are combined in the second deferred script. As to performance improvement of ZEEF, on a test JBoss EAP server on modern hardware, the time to DOMContentLoaded
went from ~3s to ~1s.
无论如何,如果您已经在使用 OmniFaces,那么您始终可以使用 CDNResourceHandler
通过 web.xml
中的以下上下文参数将 PrimeFaces jQuery 资源委托给真正的 CDN:
In any case, if you're already using OmniFaces, then you can always use CDNResourceHandler
to delegate the PrimeFaces jQuery resource to a true CDN by the following context param in 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 上初始化拖放操作时,它节省了几百毫秒.
Note that jQuery 1.11 has some major performance improvements over 1.10 as internally used by PrimeFaces 4.0 and that it's fully backwards compatible. It saved a couple of hundred milliseconds when initializing drag'n'drop on ZEEF.
这篇关于延迟加载和解析 PrimeFaces JavaScript 文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!