Performance
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11) with Eclipse Mojarra
4.1.17 and Apache MyFaces Core 4.1.4, Java 17 or later (the examples were compiled with --release 21), PrimeFaces 16.0.0,
OmniFaces 5.5.5 and Apache JMeter 5.6.3 (the current release on the JMeter download page on 2026-10-08). Faces 4.0 is expected to behave
the same for everything below (only 4.1 was run), except where What Changed in Which Version says otherwise. The examples were deployed as an exploded Bookshelf WAR
on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with scripted HTTP
requests and, for Ajax, a headless Chrome (all observed on 2026-10-08). The project stage was Production unless a section
says otherwise. Sizes and counts quoted below are exact for that setup. Timings are not: they come from a single-threaded
loop on the loopback interface against pages of a few hundred components, so they only show a direction. For numbers you can
quote, load test your own application as described in Load Testing with JMeter. Claims taken from an implementation’s documentation
and not run are marked as such. Faces 5.0 is still a milestone release at the time of writing and nothing here describes it
as current behaviour.
Performance work on a Faces application is mostly about doing less. Faces is a stateful framework: every request restores a component tree, runs it through the six lifecycle phases, renders it and saves its state again. The costs therefore scale with the size of the tree, with the amount of state that has to be saved, with how often your own code is called while that happens and with how many bytes travel to the browser. Each section below takes one of these, shows how to see it, and gives the cheapest fix. The first rule comes before all of them: measure before you change anything, because the usual bottleneck is not Faces but the database query behind a getter (the PrimeFaces documentation says the same about its own components).
Where the Time Goes
The following table shows where each cost lands in the request processing
lifecycle. A postback walks all six phases; an Ajax request walks them too, but only for the components named in execute
(and renders only the ones named in render, see Scope Ajax Requests with Execute and Render); an initial GET builds the view and renders it.
| Phase | What costs time or memory | Section |
|---|---|---|
Restore View |
Server state: look up the saved state in the session. Client state: decode, decrypt, decompress and deserialise it. Then Facelets re-applies the page to rebuild the tree (the "build view" step). |
State Saving: Partial, Full, Server, Client, MyFaces: EL Caching, View Pooling and Other Switches |
Apply Request Values, Process Validations, Update Model Values |
Walk the tree (or the executed part of it) and call decode, validators, converters and setters. |
Component Tree Size, Scope Ajax Requests with Execute and Render |
Invoke Application |
Your action and listener methods, normally the heaviest part. |
|
Render Response |
Evaluate every |
Component Tree Size, Getter Cost, State Saving: Partial, Full, Server, Client |
Between requests |
Session memory: saved view states, view-scoped beans, session beans. |
View Scope Size, State Saving: Partial, Full, Server, Client |
On the wire |
HTML size, view state field, Ajax payload, number and size of resources. |
Scope Ajax Requests with Execute and Render, Resource Caching and Compression |
The MyFaces documentation gives a rule of thumb from tests it ran long ago: rendering the response took about 70% of the time Faces itself used, and building the view 15% to 20%, while walking the tree took far less than either. On an Ajax request the build step weighs more the larger the tree is. That is the argument behind view pooling (MyFaces: EL Caching, View Pooling and Other Switches). Treat those percentages as a hint about where to look first, not as a measurement of your application.
Run in Production Stage
The cheapest speed-up is a context parameter (the stages are described on the configuration page). In the Development project stage Faces re-checks every view file on every
request so that you can edit a page and reload, shows detailed error pages. None of
that belongs on a server that real users use.
<context-param>
<param-name>jakarta.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>jakarta.faces.FACELETS_REFRESH_PERIOD</param-name>
<param-value>-1</param-value>
</context-param>
<context-param>
<param-name>jakarta.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
Source: Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters and Mojarra 4.1 context parameters
-
jakarta.faces.PROJECT_STAGEtakesDevelopment,UnitTest,SystemTestorProduction. Mojarra’s reference listsProductionas the value used when the parameter is absent, and the Jakarta EE Tutorial shows the parameter being set inweb.xml. -
jakarta.faces.FACELETS_REFRESH_PERIODis the number of seconds between two checks of a view file on disk. The declared default is0, which means "check on every request". Mojarra’s reference says the runtime itself applies-1(never check) when the stage isProductionand the parameter is not set; the MyFaces reference gives the default as "0, -1 in Production". Setting-1explicitly makes the intent visible and does not depend on the stage. -
jakarta.faces.FACELETS_SKIP_COMMENTSdrops XML comments (<!-- ... -->) while compiling the view, so they are not sent to the browser. With the parameter off, every comment in a template is delivered to every visitor. See the Facelets page. -
jakarta.faces.FACELETS_BUFFER_SIZEis the number of bytes the response is buffered with while a view is rendered (default 1024,-1leaves the response’s own buffer alone). A larger buffer flushes less often and, as the Mojarra reference puts it, makes sure an error cannot arrive after the response was partly sent; the price is memory per concurrent request. -
Mojarra only:
com.sun.faces.disableIdUniquenessCheck(false,trueorauto) skips the walk that verifies that every component id in a naming container is unique.autoskips it in theProductionstage. MyFaces has a similar switch,org.apache.myfaces.CHECK_ID_PRODUCTION_MODE(true,auto,false;autochecks only components that do not wrap markup). Both are documented by the implementations; the effect was not measured here.
A last measurement for the case you are weighing up. The same three Bookshelf pages (300 components, a 300-row ui:repeat
and 300 rows of plain markup, all described in Component Tree Size) were requested 500 times in a loop in each stage. On the
loopback interface and with server state, the Development stage was between about 1.2 and 4 times slower than
Production on both implementations, and the gap was the largest on MyFaces. The ratio depends on
page size and on the file system; the lesson is only that the stage matters.
Component Tree Size
Every Faces tag that has a component behind it (h:outputText, h:inputText, h:panelGroup, f:facet children and so
on) becomes a UIComponent in the tree for as long as the view lives. The tree is built when the page is first requested
and rebuilt, in part, on each postback; every component takes part in every lifecycle phase it is not skipped for, costs
memory in the saved state (see State Saving: Partial, Full, Server, Client) and produces markup. Two pages that look the same in the browser can differ
by an order of magnitude in tree size.
The Bookshelf scratch app has three pages that print 300 labelled text boxes. They use one binding (#{bulkEditBean.title})
so only the structure differs:
c:forEach builds 300 times two components<h:form id="f">
<c:forEach begin="1" end="300" var="i">
<h:outputText value="Row #{i}"/> <h:inputText value="#{bulkEditBean.title}"/><br/>
</c:forEach>
<h:commandButton id="go" value="Save" action="#{bulkEditBean.save}"/>
</h:form>
ui:repeat keeps one template, repeated at render time<h:form id="f">
<ui:repeat value="#{bulkEditBean.rows}" var="row">
<h:outputText value="#{row}"/> <h:inputText value="#{bulkEditBean.title}"/><br/>
</ui:repeat>
<h:commandButton id="go" value="Save" action="#{bulkEditBean.save}"/>
</h:form>
ui:repeat<h:form id="f">
<ui:repeat value="#{bulkEditBean.rows}" var="row">
#{row} <input type="text" name="x"/><br/>
</ui:repeat>
<h:commandButton id="go" value="Save" action="#{bulkEditBean.save}"/>
</h:form>
(bulkEditBean is the view-scoped bean listed in Scope Ajax Requests with Execute and Render.) Measured on the three pages, with the saved state of a
fresh GET:
| Page | Mojarra, HTML | Mojarra, client state field | MyFaces, HTML | MyFaces, client state field |
|---|---|---|---|---|
|
18,730 bytes |
about 750 characters |
25,743 bytes |
about 14,300 characters |
|
21,385 bytes |
about 2,350 characters |
28,773 bytes |
about 620 characters |
|
15,787 bytes |
about 800 characters |
15,785 bytes |
about 620 characters |
The HTML sizes are with server state (where the state field is 32 to 41 characters on both implementations, a lookup key).
Three observations. The tree-html page is the smallest and was the quickest in every timing loop on both implementations,
because it creates only the form, the button and the ui:repeat. The tree-forEach page was the slowest in all but one of
the loops and is the one JSTL tags are blamed for: c:forEach runs while the view is built, so it creates all
those components (see the JSTL page for the correctness side of that
choice). And the client state sizes cross over between the implementations: the page with 300 real components has a small
state on Mojarra and a large one on MyFaces, while the ui:repeat page is the reverse. I did not investigate why; the
practical lesson is to measure the state of your own pages with the implementation you deploy.
What shrinks a tree:
-
Iterate with a template, not with a loop of components.
ui:repeatandh:dataTablehold one set of child components and re-run it per row (see the comparison). A table withrows="25"renders 25 rows, whatever the size of the list behind it. -
Use markup where you need no component. Static HTML, EL in template text and passthrough elements add nothing to the tree. The PrimeFaces documentation advises plain EL in the page instead of
h:outputTextwhen you need neither converters nor the output tag’s other features, and a plaindivinstead ofp:outputPanel. A plaindivthat Ajax has to update can be made addressable with a passthrough attribute (<div faces:id="...">); see the HTML5 friendly markup page. Keeph:outputTextwhere you want its features: the page on EL in template text explains what escaping you get. -
Do not build what the user cannot see. A
rendered="false"component is still built and still saved, but it is skipped by decode, validation, model update and encoding. The scratch run confirmed the decode part on both implementations: a forged value posted for an input inside arendered="false"group never reached the setter. Content that is rarely shown (tabs, dialogs, details) is better loaded on demand; see PrimeFaces: Lazy Loading, Caching and Settings fordynamic="true". -
Split the page. Several small views joined by navigation or Ajax (
rendertargets) cost less than one view carrying every widget the application has. -
Keep a whole view stateless if it has no postback state to keep (State Saving: Partial, Full, Server, Client).
Getter Cost
Faces evaluates an expression every time it needs its value: a rendered attribute is read in several phases, the
value of a table is read per phase and a getter used in three places on the page is called three times or more. The
page that calls #{bean.books} therefore calls getBooks() several times per request. Putting a database
query, a web-service call or any other expensive work into such a getter is the best-known Faces performance mistake. The
request processing page lists the phases; the data tables page observed that a table’s value is read three times
before the action runs and once more to render on a postback.
The following pair of beans and views shows it. The service counts every call to catalogue(...) as a query, and a
second counter counts getter calls:
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.example.bookshelf.service.HitCounter;
/** The anti-pattern: the getter queries every time Faces or the page calls it. */
@Named
@RequestScoped
public class QueryInGetterBean {
@Inject
BookService service;
@Inject
HitCounter hits;
private String filter = "";
public List<Book> getBooks() {
hits.hit("getter.queryInGetter");
return service.catalogue(5);
}
public boolean isNoBooks() {
return getBooks().isEmpty();
}
public String getFilter() {
return filter;
}
public void setFilter(String filter) {
this.filter = filter;
}
public String search() {
return null;
}
}
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.example.bookshelf.service.HitCounter;
/** The fix: load once per request, expose the field. */
@Named
@RequestScoped
public class LoadOnceBean {
@Inject
BookService service;
@Inject
HitCounter hits;
private List<Book> books;
private String filter = "";
@PostConstruct
void load() {
books = service.catalogue(5);
}
public List<Book> getBooks() {
hits.hit("getter.loadOnce");
return books;
}
public boolean isNoBooks() {
return books.isEmpty();
}
public String getFilter() {
return filter;
}
public void setFilter(String filter) {
this.filter = filter;
}
public String search() {
return null;
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:c="jakarta.tags.core"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<title>Catalogue</title>
</h:head>
<h:body>
<h:form id="f">
<h:outputText id="none" value="No books" rendered="#{queryInGetterBean.noBooks}"/>
<h:dataTable id="books" value="#{queryInGetterBean.books}" var="book"
rendered="#{not empty queryInGetterBean.books}">
<h:column>#{book.title}</h:column>
</h:dataTable>
<h:inputText id="filter" value="#{queryInGetterBean.filter}"/>
<h:commandButton id="go" value="Search" action="#{queryInGetterBean.search}"/>
</h:form>
</h:body>
</html>
Source: Jakarta EE Tutorial: Expression Language and Jakarta Faces 4.1 specification, Chapter 2 (lifecycle)
Counted by the instrumentation, one GET and then one postback of the form (five books, Production stage):
| View | Mojarra GET | Mojarra postback | MyFaces GET | MyFaces postback |
|---|---|---|---|---|
|
4 calls, 4 queries |
13 calls, 13 queries |
3 calls, 3 queries |
10 calls, 10 queries |
|
3 calls, 1 query |
9 calls, 1 query |
2 calls, 1 query |
6 calls, 1 query |
The getter is called a different number of times by each implementation (the specification does not fix it), so never write code that depends on the count. Notice what the table says about the design too: even the good bean’s getter is called six to nine times on a postback. That is fine because it only returns a field.
The rules that follow:
-
A getter returns a field or a cheap derived value, nothing else. No query, no remote call, no object construction, no logging that matters.
-
Load in a place that runs once. A request-scoped bean’s
@PostConstruct(as above, created at the first expression that touches the bean, see the backing beans page), anf:viewActionor an action method are such places. For data that must survive a postback without a second query, keep it in a view-scoped bean and mind its size (View Scope Size). -
Do not return a new collection or a new
DataModelper call. A table asks for its value several times per request, so a new object each time re-queries each time (the data tables page shows the stale-list bug this also causes). -
Do not hide the cost behind a boolean. The
isNoBooks()method above looks cheap and is not, because it calls the expensive getter.renderedexpressions are evaluated more often than anything else. -
Avoid computation in EL, too. The PrimeFaces documentation recommends moving inline conditions out of expressions into the bean: it is faster and easier to read. Expression syntax is on the expression language page.
-
Cache data used several times in one view in a request-scoped bean, as above. Avoid abusing session and view scopes for it (View Scope Size).
View Scope Size
A @ViewScoped bean (jakarta.faces.view.ViewScoped) lives as long as the view does and is kept in the session, in the
view map of a saved view. Two things follow. Everything the bean references is held in the session for the lifetime of the
view, for every open view, for every user. And the bean has to be Serializable because the container may passivate the
session (the scopes page lists the passivation rules).
The commonest mistake is to keep a whole result set in the bean to avoid re-querying. The scratch bean below holds 5000 search results twice, once as entities and once as ids, and measures both with Java serialisation, which is a fair proxy for what a clustered session would have to replicate:
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
/** Two ways of keeping 5000 search results in view scope; the sizes are measured by Java serialisation. */
@Named
@ViewScoped
public class ResultsBean implements Serializable {
@Inject
transient BookService service;
private List<Book> entities;
private List<Long> ids;
private String sizes = "";
@PostConstruct
void init() {
entities = service.catalogue(5000);
ids = service.ids(5000);
}
public String measure() throws IOException {
sizes = "entities=" + size(new ArrayList<>(entities)) + " bytes, ids=" + size(new ArrayList<>(ids)) + " bytes";
return null;
}
private static int size(Object value) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(value);
}
return bytes.size();
}
public String getSizes() {
return sizes;
}
}
On both implementations the same figures came back: the 5000 Book records serialise to 211,107 bytes, the 5000 ids to
70,122 bytes. Both ways are far from free (the entities are roughly 42 bytes per record, the ids roughly 14), and a view
scope that holds them is paid for once for every view the user has open. The advice follows:
-
Keep ids and filters, not results. Store what is needed to repeat the query (search text, page number, sort column) and fetch the page again, or store the one selected entity’s id. A
LazyDataModel(PrimeFaces: Lazy Loading, Caching and Settings) or a page-by-pageDataModelloads only what the table shows. -
Keep large read-only data out of the scope altogether. Application-scoped caches, or the request scope, are the place for data that does not belong to one view.
-
Mark what must not be saved as
transient(as the scratch bean does for the injected service). -
Know when a view bean dies. The scopes page shows that a view bean is destroyed on a navigation that builds a new view, when its view is evicted and when the session ends; a view left by a plain
faces-redirect=trueor a new GET is not destroyed at that moment. -
Know the limits that bound the total. The session can hold only so many saved views and view maps. Mojarra keeps the last 15 logical views with 15 views each (
com.sun.faces.numberOfLogicalViews,com.sun.faces.numberOfViewsInSession) and destroys the least recently used of 25 view maps (com.sun.faces.numberOfActiveViewMaps, default 25). MyFaces keeps 20 views (org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION), and an evicted view takes its beans with it. The scopes page observed that a postback to an evicted view ends inViewExpiredException, so lowering these numbers saves memory and costs users who keep many tabs open. The same page has the details.
State Saving: Partial, Full, Server, Client
Faces must restore the tree on a postback. It does so from saved view state, kept either on the server (the default, and what
the jakarta.faces.ViewState hidden field then holds is a short key) or in the page itself (client state: the whole state
encoded in the field). The setting is jakarta.faces.STATE_SAVING_METHOD, with the values server and client; the mechanism, the keys of client state and the cluster consequences are on the state saving page.
Partial state saving (PSS, since Faces 2.0, jakarta.faces.PARTIAL_STATE_SAVING, default true) saves only what
differs from the initial state, which Facelets can rebuild by running the page again. It is the reason a restored view needs a
build step at all. Disabling it (full state saving, or FSS) saves the whole tree.
Measured on the Bookshelf pages with client state, so that the state is visible. The figure is the length of the
jakarta.faces.ViewState value:
| Page | Mojarra, partial | Mojarra, full | MyFaces, partial | MyFaces, full |
|---|---|---|---|---|
|
704 |
2,476 |
152 |
5,824 |
|
2,348 |
3,884 |
620 |
5,528 |
|
748 |
16,812 |
14,336 |
234,988 |
Partial state saving is smaller in every cell, by a factor of about 1.7 to 38. Server state is the cheaper on the wire (32 to 41 characters in these runs) and costs session memory instead. Client state is unbounded by the session limits and survives a session that expires, which is why it is chosen for stateless clusters, and it costs CPU and bandwidth on every request. Its integrity and confidentiality rely on the implementation’s encryption, so never turn that off in production (Mojarra has a switch that exists for debugging only; the security page returns to this).
Related parameters:
| Mojarra | MyFaces | Effect (from the implementations' documentation) |
|---|---|---|
|
same name |
Serialise the state to bytes before storing it in the session. Costs a serialise and a deserialise per request and buys the guarantee that the state is serialisable. |
|
|
GZIP-compress the serialised state. For Mojarra it matters with client state or serialised server state. |
|
|
How many saved views the session keeps (server state). |
|
- |
Buffer used while writing client state. |
Three more points. First, the PrimeFaces performance page recommends switching view state compression off for speed; read
the table above before copying that: with Mojarra’s default of server state it has no effect, and with client state it makes
the page larger. Second, com.sun.faces.refreshTransientBuildOnPSS (false) and com.sun.faces.restoreBuildTimeDecisions
(false), both added to Mojarra’s 4.0 line (4.0.19 and 4.0.23), exist for views whose structure changes between restore and render because
of build-time tags; the Mojarra reference says the first costs a second full build of the view on every postback and only
papers over a problem you should fix. The JSTL page explains those structural problems.
Third, full state saving is deprecated in Faces 4.1. The specification’s change log lists "Deprecate full state saving"
(issue 1829) and marks jakarta.faces.PARTIAL_STATE_SAVING and jakarta.faces.FULL_STATE_SAVING_VIEW_IDS as deprecated.
Setting PARTIAL_STATE_SAVING to false made both Mojarra 4.1.17 and MyFaces 4.1.4 log a warning that the parameter is
deprecated as of Faces 4.1 at startup; setting it to true did not. Leave the default.
Stateless views. A view that needs no state between requests (a search form, a landing page) can be marked transient with
<f:view transient="true">. Faces then saves nothing; the view state field of such a page held the text stateless on both
implementations. The price: no view-scoped beans (a new instance each request, see the scopes page), and a required view
parameter fails on a postback. Pooled views and stateless views exclude each other in MyFaces. The MyFaces documentation
adds that the stateless optimisation has no noticeable effect for MyFaces, "because its PSS algorithm […] is very efficient";
the page on state saving and stateless views covers the topic in full.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:c="jakarta.tags.core"
xmlns:ui="jakarta.faces.facelets">
<f:view transient="true">
<h:head>
<title>Search</title>
</h:head>
<h:body>
<h:form id="f">
<h:inputText id="q" value="#{loadOnceBean.filter}"/>
<h:commandButton id="go" value="Search" action="#{loadOnceBean.search}"/>
</h:form>
</h:body>
</f:view>
</html>
Scope Ajax Requests with Execute and Render
f:ajax has two lists. execute names the components that are decoded, validated and written to the model in the request;
render names the components that are encoded into the response. Their defaults are @this and @none. The common
performance mistakes are execute="@form" where one input matters and render="@form" (or a distant, large container) where
one text has changed: the first runs conversion, validation and every setter in the form, the second re-encodes and re-sends
every component in it.
The view and the bean (the view-scoped bean has two inputs and a list of 300 rows next to them):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:c="jakarta.tags.core"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<title>Scope</title>
</h:head>
<h:body>
<h:form id="f">
<h:inputText id="title" value="#{bulkEditBean.title}"/>
<h:inputText id="author" value="#{bulkEditBean.author}"/>
<h:panelGroup id="list" layout="block">
<ui:repeat value="#{bulkEditBean.rows}" var="row"><h:outputText value="#{row}"/> </ui:repeat>
</h:panelGroup>
<h:outputText id="saves" value="#{bulkEditBean.saves}"/>
<h:commandButton id="narrow" value="Narrow" action="#{bulkEditBean.save}">
<f:ajax execute="@this" render="saves"/>
</h:commandButton>
<h:commandButton id="wide" value="Wide" action="#{bulkEditBean.save}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.example.bookshelf.service.HitCounter;
/** A form with two inputs next to a long read-only list. */
@Named
@ViewScoped
public class BulkEditBean implements Serializable {
@Inject
transient HitCounter hits;
private final List<String> rows = new ArrayList<>();
private String title = "";
private String author = "";
private int saves;
public BulkEditBean() {
for (int i = 0; i < 300; i++) {
rows.add("row " + i);
}
}
public List<String> getRows() {
return rows;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
hits.hit("set.title");
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
hits.hit("set.author");
this.author = author;
}
public int getSaves() {
return saves;
}
public void save() {
hits.hit("action.save");
saves++;
}
}
Source: Jakarta Faces 4.1 VDL: f:ajax and Jakarta EE Tutorial: Using Ajax with Jakarta Faces Technology
The server-side counters and the Ajax response for each button, hand-posted on both implementations with the identifiers
that faces.js sends:
| Button | Setters and action called | Mojarra response | MyFaces response |
|---|---|---|---|
Narrow ( |
|
270 bytes |
280 bytes |
Wide ( |
|
2,861 bytes |
2,961 bytes |
The wide response is 10 times larger for a list of 300 short rows and grows with the list. Because it re-renders the form, the whole view also pays encode time again.
A real browser showed one more difference that a hand-posted request cannot. Intercepting the XMLHttpRequest that faces.js
issues in headless Chrome for the narrow button, Mojarra’s script sent 284 bytes (the form id, view state, the source and
the Ajax parameters) whereas MyFaces' script sent 308 bytes, including f:title and f:author as well: it posts every
input of the form regardless of execute. With a form of a hundred inputs that is a lot of bytes for a one-field update.
The client resolves the keywords: @this became the button id and @form became f:wide f on Mojarra and f f:wide on
MyFaces. (A hand-written request is a different matter: sending the literal @form as the execute value ran nothing on Mojarra, while
MyFaces accepted it. Send resolved identifiers, as the script does, when you replay Ajax traffic, see Load Testing with JMeter.)
PrimeFaces adds a switch for that payload. primefaces.SUBMIT (full by default, or partial) tells p:ajax and
p:commandButton to serialise only the components that are processed; the partialSubmit attribute overrides it per
component. The PrimeFaces documentation says the global setting reduces network traffic; primefaces.SUBMIT was confirmed
to be read by PrimeFaces 16.0.0 (its configuration class reads it), but the traffic reduction itself was not measured here.
Rules for Ajax:
-
Execute the least you can, usually
@this. Then the rest of the form is neither validated nor written. This also stops an unrelated validation failure from blocking the request (see the Ajax page). -
Render the smallest container that contains the change, with an id, instead of
@formor@all. A table’s single cell or a message component is usually enough. -
Give the page stable container ids so that a narrow
rendercan name them. Search expressions (Search Expressions) such as@parentand@child(0)keep the targets short. -
Group related actions on one Ajax request instead of firing several, and consider
delayfor per-keystroke events (the Ajax page coversdelayandf:ajax disabled). -
Prefer Ajax over a full postback for small interactions, as the PrimeFaces documentation advises, once the scoping above is right: an Ajax request that executes and renders the whole form is not cheaper than a postback.
-
Maintainability counts too: the PrimeFaces documentation itself warns to weigh readability against the last bit of speed when scoping
processandupdate.
Resource Caching and Compression
Stylesheets, scripts and images included with h:outputStylesheet, h:outputScript and h:graphicImage are served by the
ResourceHandler under /jakarta.faces.resource/ (the resource handling page explains the
lookup, libraries and versions). What a browser may cache is decided by the response headers the handler adds. The scratch
runs requested one stylesheet and one view from each implementation:
| Response | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
|
|
A Facelets view ( |
no cache headers |
no cache headers |
Seven days is the default on both. The parameters:
| Mojarra | MyFaces | Effect (from the implementations' documentation) |
|---|---|---|
|
|
How long a client may keep a resource without asking again. |
|
|
Remember the last-modified time instead of reading it from the file system on every request; Mojarra’s reference says |
|
- |
How often a cached resource is checked for modification. |
|
- |
Comma-separated MIME types (a trailing |
|
|
Buffer used while streaming a resource. |
- |
|
Remember whether a resource exists. |
A long cache lifetime only works if a changed file gets a new URL. Faces helps by letting a library carry a version
(a version directory below the library, covered on the resource handling page); the OmniFaces combined resource of the next
section (see also the OmniFaces page) adds a v parameter; and a build step can put a content hash in the file name. Changing the default to a short time
instead of versioning gives up most of the benefit.
Views must not be cached by the browser. A cached page with a jakarta.faces.ViewState field brings back a view the server
may no longer remember when the user presses the back button. The OmniFaces CacheControlFilter exists for this: with no
parameters it sends headers that forbid caching, and its documentation says it deliberately skips Faces resources, whose
headers the ResourceHandler controls. Compressing the HTML helps the other direction. Both filters, mapped on the Faces
servlet, were run on Mojarra:
<filter>
<filter-name>compressedResponseFilter</filter-name>
<filter-class>org.omnifaces.filter.CompressedResponseFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>compressedResponseFilter</filter-name>
<servlet-name>facesServlet</servlet-name>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<filter>
<filter-name>noCache</filter-name>
<filter-class>org.omnifaces.filter.CacheControlFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>noCache</filter-name>
<servlet-name>facesServlet</servlet-name>
</filter-mapping>
Results for the 21,383-byte tree-repeat.xhtml response: with Accept-Encoding: gzip it was delivered as
Content-Encoding: gzip in 2,061 bytes; the filter chain also added Cache-Control: no-cache,no-store,must-revalidate,
Expires: Thu, 01 Jan 1970 00:00:00 GMT, Pragma: no-cache and a short-lived BFCache-Buster cookie to the view response (the Javadoc lists all four headers, the cookie since
OmniFaces 2.7.28, and says the filter’s caching headers are disabled in the Development stage). Servlet containers can usually compress responses themselves, which is
preferable when you control the container; the filter’s Javadoc names it as the container-independent alternative. Note that
the PrimeFaces performance page still points at a GzipResponseFilter from OmniFaces; in OmniFaces 5.5.5 the class is
CompressedResponseFilter and a GzipResponseFilter does not exist in the jar.
Two PrimeFaces notes from the same page: compress and optimise your own JavaScript and CSS in the build (it names the
PrimeFaces Extensions resources optimizer Maven plugin), and use a servlet filter to set expiry headers on static files that
are not served through the Faces ResourceHandler.
Combining Resources with OmniFaces CombinedResourceHandler
Every h:outputStylesheet and h:outputScript is one more HTTP request. OmniFaces'
org.omnifaces.resourcehandler.CombinedResourceHandler removes the separate stylesheet and script resources from the head
and replaces them with one combined stylesheet and one combined script. It is not registered automatically (OmniFaces
registers its other resource handlers, but not this one); you add it to faces-config.xml:
<application>
<resource-handler>org.omnifaces.resourcehandler.CombinedResourceHandler</resource-handler>
</application>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:c="jakarta.tags.core"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<title>Resources</title>
<h:outputStylesheet library="bookshelf" name="layout.css"/>
<h:outputStylesheet library="bookshelf" name="page.css"/>
<h:outputStylesheet library="bookshelf" name="print.css" media="print"/>
<h:outputScript library="bookshelf" name="a.js" target="head"/>
<h:outputScript library="bookshelf" name="b.js" target="head"/>
</h:head>
<h:body>
<h:form id="f"><h:commandButton id="go" value="Go"/></h:form>
</h:body>
</html>
The page has three stylesheets (one of them media="print") and two scripts. Without the handler the head references five
files; with it, the browser got:
<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/print.css.xhtml?ln=bookshelf" media="print" />
<script src="/bookshelf/jakarta.faces.resource/eNpLys_PLs5IzUmzStTLKq5JgnOTgFwArAQLRA.js.xhtml?ln=omnifaces.combined&v=1791457876570"></script>
<link rel="stylesheet" href="/bookshelf/jakarta.faces.resource/eNpLys_PLs5IzUmzykmszC8t0UsuLq5JggsWJKangoQAO44PlA.css.xhtml?ln=omnifaces.combined&v=1791457876570" />
Fetching those two URLs returned the files concatenated in page order (layout.css then page.css; a.js then b.js),
with ETag, Last-Modified and an Expires header one week ahead, on Mojarra and on MyFaces. The odd-looking file name is the
list of combined resources encoded by the handler; the v parameter changes when one of the sources changes. (The Javadoc says
it is the last-modified time in minutes; the values observed looked like millisecond timestamps, so do not parse it.)
The rules, from the Javadoc and confirmed in the run:
-
Only head resources are combined.
h:outputStylesheetdefaults totarget="head";h:outputScriptdoes not, so anh:outputScriptin the head needstarget="head"explicitly. Without it the two scripts stayed separate in the scratch run (resources-notarget.xhtml). -
media="print"stylesheets stay separate, as the run shows (since OmniFaces 3.11 per the Javadoc). -
Resources added by a renderer or written by hand are not combined: a hard-coded
<link>or<script>, or a file a renderer puts in the page itself (the Javadoc names PrimeFaces'theme.css). -
Order is preserved within each kind. An excluded resource is emitted before the combined ones.
The context parameters, all documented in the Javadoc (names confirmed in the 5.5.5 jar):
| Parameter | Effect |
|---|---|
|
|
|
Comma-separated resource identifiers ( |
|
Resources removed from the page altogether. |
|
|
|
Seconds to cache the combined content on the server (default 0, off). Considered only when the project stage is not |
|
Caps the application-scoped cache shared with |
With PrimeFaces 16.0.0 on Mojarra (a p:dataTable page) the six scripts the page loaded became two: one combined script of
897,342 bytes and PrimeFaces' locale-en.js, which stayed separate. Of the three stylesheets, components.css was combined
(112,070 bytes); theme.css and primeicons.css stayed separate. On MyFaces the PrimeFaces combination was not run.
When is it worth it? With HTTP/1.1 every file costs a connection round trip, so fewer, bigger files load faster. With HTTP/2 the saving shrinks, and one changed file invalidates the whole bundle (reasoning, not measured here). A build-time bundle that gives the combined file a content hash is an alternative the PrimeFaces performance page suggests (it points to the PrimeFaces Extensions resources optimizer for JavaScript and CSS). Whatever you choose, test it against a cold cache.
MyFaces: EL Caching, View Pooling and Other Switches
MyFaces has several optimisations of its own. All of them are MyFaces-only context parameters, so they do nothing (and change no behaviour) on Mojarra; keep that in mind when a team runs different servers in development and in production.
Caching EL expressions. Each time a view is built, the Facelets compiler creates the value and method expressions of the
page again. org.apache.myfaces.CACHE_EL_EXPRESSIONS lets MyFaces reuse them. The valid values, from the MyFaces documentation:
| Value | Behaviour |
|---|---|
|
Every expression is created each time the view is built. |
|
Do not cache only inside user tags or when the expression uses a variable resolved through the |
|
Like |
|
Like |
|
Recommended since MyFaces 2.1.12: when it finds a new user tag attribute or parameter it recompiles the Facelet and marks the expressions that must not be cached, which solves the problems the |
The documentation lists the traps of the cheaper modes: two pages that include one template with different numbers of
ui:param values can see the first page’s cached var2, and a c:set inside a conditional can leave later expressions
uncached. The switch is a plain context parameter:
<context-param>
<param-name>org.apache.myfaces.CACHE_EL_EXPRESSIONS</param-name>
<param-value>alwaysRecompile</param-value>
</context-param>
The scratch app ran every example of this page, including the Ajax and postback counts above, with alwaysRecompile and
produced identical counts and output. The speed-up itself was not measured (loopback timings of a 1 ms request were noise).
View pooling. Instead of rebuilding a view for every request, MyFaces keeps a pool of views for reuse: after rendering, the view is reset to its initial state (this relies on the partial-state-saving mechanism) and parked, and the next request for the same view id, locale, render kit and contracts takes it from the pool and, for a view that has dynamic parts, refreshes only those. The MyFaces documentation reports, from tests with its demo application, about 25% less transient memory, about 9% more throughput for non-Ajax requests and 9% to 30% or more for Ajax requests depending on the tree size. Those figures are the project’s, not mine, and the documentation itself recommends the pool only where performance is a critical concern and only for the busiest views.
You enable it in one of two ways. A faces-config.xml mapping, which also lets you override the global parameters per URL
pattern:
<faces-config-extension>
<view-pool-mapping>
<url-pattern>/*</url-pattern>
<parameter>
<name>org.apache.myfaces.VIEW_POOL_MAX_POOL_SIZE</name>
<value>5</value>
</parameter>
</view-pool-mapping>
</faces-config-extension>
or a marker on the top-level page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:c="jakarta.tags.core"
xmlns:ui="jakarta.faces.facelets">
<f:view oamEnableViewPool="true">
<h:head>
<title>Pooled</title>
</h:head>
<h:body>
<h:form id="f">
<h:inputText id="q" value="#{loadOnceBean.filter}"/>
<h:commandButton id="go" value="Search" action="#{loadOnceBean.search}"/>
</h:form>
</h:body>
</f:view>
</html>
The parameters (defaults from the MyFaces 4.1 configuration page):
| Parameter | Default | Effect |
|---|---|---|
|
5 |
Views kept per view metadata definition. |
|
2 |
Dynamic views that may be reused as partial views. |
|
|
|
|
|
Reuse views left by default-algorithm navigation. The documentation says it does not follow the specification. |
What the run showed, on MyFaces 4.1.4. Both ways of enabling the pool were accepted and the pages behaved as before. Without
the EL caching switch the pool refused to start: the startup log said that CACHE_EL_EXPRESSIONS was noCache and had to be
alwaysRecompile to enable view pooling, and "View Pooling disabled". So the pool needs the previous switch. A transient
(stateless) view and a pooled view exclude each other, as the documentation says, because the pool reuses the state saving
algorithm. The documentation also lists the costs: every component in a pooled view must be able to reset itself (components
that extend UIComponent directly, rather than UIComponentBase with its StateHelper, need extra code in saveState),
the detection is not fully failsafe, and listeners subscribed from a tag handler with UIViewRoot.subscribeToViewEvent are
not restored. Custom components such as those on the custom components page have to be checked against that list.
Other switches, all from the MyFaces configuration or the PrimeFaces performance page and not run, unless noted:
-
org.apache.myfaces.EARLY_FLUSH_ENABLED(defaultfalse) sends the response head to the browser while the rest renders so that it can start fetching resources. -
org.apache.myfaces.USE_LAMBDA_METAFACTORY(defaultfalse) uses lambdas instead of reflection for getters and setters. The PrimeFaces page notes that it might fail in some environments and that the parameter is calledUSE_LAMBDAS_OVER_REFLECTIONin MyFaces 5.0. -
org.apache.myfaces.VIEW_UNIQUE_IDS_CACHE_ENABLED,org.apache.myfaces.VIEWID_EXISTS_CACHE_ENABLEDand the otherVIEWID_*cache switches are on by default. -
Whitespace compression, configured in
faces-config.xmlwith<faces-config-extension><facelets-processing><file-extension>.xhtml</file-extension><process-as>xhtml</process-as><oam-compress-spaces>true</oam-compress-spaces></facelets-processing></faces-config-extension>, trims unnecessary white space at compile time. It was started successfully; on the scratch pages it saved 2 bytes of 28,773, because Facelets output there already contained little indentation, so the gain depends on how your templates are formatted.
PrimeFaces: Lazy Loading, Caching and Settings
PrimeFaces gives one performance advice before any other: the bottlenecks are almost always in the back end and in database queries. Its toolbox follows from that.
Lazy loading of data. A p:dataTable with a plain List value holds every row in memory on every request. Binding a
LazyDataModel makes the table ask for one page at a time (the PrimeFaces data components page has the full lazy model). The PrimeFaces data table documentation states the difference: regular
Ajax pagination renders one page but still needs all the data in memory, a lazy table also loads only the page into memory. The
model implements count and load; the table calls load on every paging, sorting or filtering. It also advises against
keeping the model in a long-lived scope such as @ViewScoped (large sessions and serialisation) and shows a request-scoped
bean. The example below adapts the data tables page’s BookService (its find and count methods) and was run on
Mojarra with PrimeFaces 16.0.0:
package org.example.bookshelf.web;
import java.util.List;
import java.util.Map;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta;
import org.primefaces.model.SortOrder;
/** Loads one page per request from the service; nothing but the current rows is held in memory. */
public class CatalogueLazyModel extends LazyDataModel<Book> {
private final transient BookService service;
public CatalogueLazyModel(BookService service) {
this.service = service;
}
@Override
public int count(Map<String, FilterMeta> filterBy) {
return service.count();
}
@Override
public List<Book> load(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
String field = "title";
boolean ascending = true;
if (!sortBy.isEmpty()) {
SortMeta sort = sortBy.values().iterator().next();
field = sort.getField();
ascending = sort.getOrder() != SortOrder.DESCENDING;
}
return service.find(first, pageSize, field, ascending);
}
@Override
public String getRowKey(Book book) {
return String.valueOf(book.id());
}
}
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.model.LazyDataModel;
@Named
@RequestScoped
public class PrimeCatalogueBean {
@Inject
BookService service;
private LazyDataModel<Book> books;
@PostConstruct
void init() {
books = new CatalogueLazyModel(service);
}
public LazyDataModel<Book> getBooks() {
return books;
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:p="primefaces">
<h:head>
<title>Lazy catalogue</title>
</h:head>
<h:body>
<h:form id="f">
<p:dataTable id="books" value="#{primeCatalogueBean.books}" var="book" lazy="true"
paginator="true" rows="10" rowKey="#{book.id}">
<p:column headerText="Title" sortBy="#{book.title}"><h:outputText value="#{book.title}"/></p:column>
<p:column headerText="Author" sortBy="#{book.author}"><h:outputText value="#{book.author}"/></p:column>
<p:column headerText="Year" sortBy="#{book.year}"><h:outputText value="#{book.year}"/></p:column>
</p:dataTable>
</h:form>
</h:body>
</html>
The first request rendered exactly 10 rows from a 1000-row source. The page only asks service.find(first, pageSize, ...) for
the rows it shows and service.count() for the pager; replace BookService with a repository that issues a LIMIT/OFFSET
or keyset query (the persistence and pagination material is linked from the persistence page) and a COUNT query that honours the
filters. Selection needs a row key: rowKey="#{book.id}" on the table and a getRowKey override as above. PrimeFaces also
ships DefaultLazyDataModel (a builder over a lambda, which its documentation says performs up to 30% better than binding a
simple list; its supplier returns the whole list, so by reasoning it saves rendering and filtering work, not the query) and
JPALazyDataModel. DefaultLazyDataModel was checked to exist in
the 16.0.0 jar with a valueSupplier and a rowKeyProvider builder method, but was not run. Row grouping does not work with
lazy loading and live scroll, according to the documentation. Virtual scrolling (virtualScroll="true") combined with a lazy
model fetches data on demand as the user scrolls.
Load content on demand. p:tabView and p:accordionPanel render all tabs by default. With dynamic="true" only the
active tab’s content is rendered and the others are fetched with Ajax when first selected. p:tabView keeps a loaded tab
in the browser (cache="true", the default) and cache="false" reloads it on every activation. The scratch page below,
requested once, contained the first tab’s text and not the second’s:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:p="primefaces">
<h:head>
<title>Book page</title>
</h:head>
<h:body>
<h:form id="f">
<p:tabView id="tabs" dynamic="true" cache="true">
<p:tab title="Details">
<h:outputText value="#{frontPageBean.summary}"/>
</p:tab>
<p:tab title="Reviews">
<h:outputText value="REVIEWS-TAB-CONTENT"/>
</p:tab>
</p:tabView>
</h:form>
</h:body>
</html>
Source: PrimeFaces 16 documentation: TabView (Dynamic Tabs) and PrimeFaces 16 documentation: AccordionPanel (Dynamic Content Loading)
Cache rendered fragments. p:cache stores the rendered output of its content in a cache region and serves it on later
requests and postbacks instead of rendering the content again. Its attributes are region, key, disabled and
processEvents, and the provider is configured with primefaces.CACHE_PROVIDER (the default provider is based on a
ConcurrentHashMap; Ehcache 2 and 3 providers exist). The scratch page below showed a summary that the bean said it computed once
across three requests, on both implementations:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:p="primefaces">
<h:head>
<title>Front page</title>
</h:head>
<h:body>
<p:cache region="bookshelf" key="front-summary">
<h:outputText id="summary" value="#{frontPageBean.summary}"/>
</p:cache>
</h:body>
</html>
Cached output is the same for every user and every request until you clear the region, so use it only for fragments that do not depend on the user, the locale or the request, and never for anything security-relevant. The default provider has no expiry or size bound (reasoning from its description as a map; the Ehcache providers add them).
Settings and patterns from the PrimeFaces performance page. The page recommends, besides the Production stage, the refresh
period of -1 and server state saving covered above:
<context-param>
<param-name>primefaces.SUBMIT</param-name>
<param-value>partial</param-value>
</context-param>
<context-param>
<param-name>primefaces.MOVE_SCRIPTS_TO_BOTTOM</param-name>
<param-value>true</param-value>
</context-param>
primefaces.SUBMIT is explained in Scope Ajax Requests with Execute and Render. primefaces.MOVE_SCRIPTS_TO_BOTTOM (false by default; true, false
or defer) moves inline scripts to the end of the body, which the documentation says gives a better visible rendering and
smaller HTML. Both names are read by PrimeFaces 16.0.0 (checked in its configuration class); the effect was not measured.
Two more PrimeFaces parameters touch the page weight. primefaces.CLIENT_SIDE_LOCALISATION is true by default in PrimeFaces 16
(the configuration table of the documentation still says false; the 14.0.0 migration guide says it became the default), which makes
the pages add a script locales/locale-<language>.js for the locale of the view (observed for a Spanish request); set it to false only if the
application loads or needs no client-side locale. primefaces.HIDE_RESOURCE_VERSION (false by default) drops v=16.0.0 from the URLs of PrimeFaces
resources; it does not make them faster, and it removes the version that cache busting relies on (see
versioning), so prefer the version in the URL unless you are hiding the library version
(the parameter table, where both defaults were observed).
The patterns, summarised: define process and update precisely; use plain HTML instead of Faces tags where you need no
component; keep logic out of getters, EL and long-lived beans; cache data used several times in a view in a request-scoped
bean; set required on inputs whose label should show the required marker instead of making p:outputLabel look the
constraint up (the page says the default of indicateRequired is auto and then looks up Bean Validation constraints);
and mark views you do not post back as stateless. Each of these is also on this page under the matching heading.
Load Testing with JMeter
Nothing above replaces a load test. It is the only way to see how your application behaves with concurrent users, with the JIT warmed up, with real think time and with the garbage collector. Apache JMeter is the usual tool. This section covers what is specific to Faces; building plans, thread group sizing, properties and running from Maven is on the existing Performance Testing with Apache JMeter page (for Spring Boot, but the JMeter part is the same). A separate load testing page for the Java frameworks is planned in issue #260 (prose only here); the testing page has a short load-testing note, and the deployment page covers monitoring.
What makes a Faces plan different: every request carries state. A GET returns a page with a jakarta.faces.ViewState hidden
field; the POST that follows must send it back or the server answers with a ViewExpiredException. So the plan has three kinds
of element beyond the plain HTTP samplers:
-
An HTTP Cookie Manager, so that each virtual user keeps its session cookie. Server-side view state lives in that session.
-
A Regular Expression Extractor as a child of the GET sampler, which reads the view state out of the response body and stores it in a JMeter variable.
-
In the POST sampler, a parameter whose value is that variable, with URL encode ticked. The JMeter manual says to select it when you are unsure, and a client state value is base64 text, which contains plus, slash and equals characters.
The extractor settings that worked against both implementations (verified with an equivalent regular expression on the responses of Mojarra and MyFaces; the JMeter elements themselves were not run, because JMeter is not installed in the scratch environment):
| Field | Value |
|---|---|
Apply to / Field to check |
Main sample only / Body |
Name of created variable |
|
Regular Expression |
|
Template |
|
Match No. |
|
Default Value |
|
The POST sampler then sends the form: the form id marker (f=f for the form with id f on Mojarra; MyFaces additionally
needed a hidden f_SUBMIT=1 marker in hand-made posts, which a real browser sends because it posts every hidden
field), the input values, the button name and jakarta.faces.ViewState=${viewState}. Record a real session with a browser and copy the
parameters rather than guessing them.
For Ajax, the request is the same form post with these additions, as observed above for f:ajax:
-
A HTTP Header Manager entry
Faces-Request: partial/ajax. Without it Mojarra 4.1.17 answered the Ajax post with the whole HTML page (text/html) rather than a partial response, while MyFaces answered with the partial response either way. A plan that omits the header silently measures a full-page render on Mojarra. -
Parameters
jakarta.faces.partial.ajax=true,jakarta.faces.source,jakarta.faces.partial.execute,jakarta.faces.partial.render,jakarta.faces.behavior.event(for a buttonaction) andjakarta.faces.partial.event, with the resolved client ids thatfaces.jswould send (see Scope Ajax Requests with Execute and Render). -
A second extractor on the response, because the partial response carries the next view state in an
updateelement: Mojarra returned<update id="j_id1:jakarta.faces.ViewState:0">and MyFaces<update id="j_id__v_0:jakarta.faces.ViewState:1">, each with the value in a CDATA section. Match it with the expression in the listing below and use the new value in the next request.
The extractor for the Ajax response, as a second child of the Ajax sampler:
jakarta\.faces\.ViewState:[0-9]+"><!\[CDATA\[([^\]]*)\]\]>
Assertions make a load test meaningful: a Response Assertion on the response code, and on the text of the response (the
partial response element for Ajax, a fixed text of your page for GETs, and a check that the text of your
ViewExpiredException error page is absent). A test that counts only HTTP 200 will happily count error pages. See the
exception handling page for how such errors are shown.
What to look at when you run it (from the JMeter best-practices page and the behaviour described above):
-
Run in CLI mode (
jmeter -n -t test.jmx -l test.jtl), with as few listeners as possible and not the View Results Tree during the load, as the JMeter best-practices page advises. Keep it for debugging the script. -
Run the application in the
Productionstage (Run in Production Stage), with the settings you intend to ship. -
Give every virtual user its own session and enough think time (a timer such as the Uniform Random Timer, or the Gaussian Random Timer), and run long enough for the session count to reach a steady state: server state accumulates per session up to the limits in View Scope Size and State Saving: Partial, Full, Server, Client, and heap and garbage collection only show their cost over minutes.
-
Watch for
ViewExpiredException: a user who opens more views thannumberOfViewsInSession(or its MyFaces counterpart) allows loses the oldest one. A script that opens many views per virtual user without navigating away reproduces that. -
The HTTP Request sampler can fetch embedded resources (stylesheets, scripts, images) with a concurrent pool; decide deliberately whether the test includes them, and measure the static files separately if a CDN will serve them.
-
Change one thing at a time (a context parameter, a getter, the Ajax
renderlist) and compare the same plan before and after, on the same machine.
Mojarra and MyFaces
| Mojarra 4.1.17 | MyFaces 4.1.4 | |
|---|---|---|
Getter calls per request (Bookshelf catalogue) |
4 on GET, 13 on postback |
3 on GET, 10 on postback |
Client state of 600 real components |
about 750 characters (partial), 16,812 (full) |
about 14,300 characters (partial), 234,988 (full) |
Client state of a 300-row |
about 2,350 characters |
about 620 characters |
Ajax request payload, |
only the Ajax parameters and state (284 bytes) |
every input of the form (308 bytes for a two-input form) |
Ajax post without |
answered with the full HTML page |
answered with the partial response |
Resource headers |
|
|
State saving limits |
|
|
EL caching, view pooling, lambda access, early flush |
not available |
|
Full state saving ( |
deprecation warning at startup |
deprecation warning at startup |
Literal |
nothing executed |
executed |
Both implementations produced identical output and identical server-side counts for every page of this section, with and
without alwaysRecompile and view pooling on MyFaces, and with CombinedResourceHandler on both.
What Changed in Which Version
-
2.0: partial state saving, Facelets as the standard view technology, the
ResourceHandler(and with it cacheable, combinable resources), thePROJECT_STAGEparameter andFACELETS_REFRESH_PERIOD. MyFaces 2.0.8 and 2.1.2 introduced EL expression caching, andalwaysRecompilearrived in 2.1.12 (from the MyFaces documentation). -
2.2: stateless views (
<f:view transient="true">) andjakarta.faces.SERIALIZE_SERVER_STATE(then in thejavaxnamespace). -
2.3.x / 4.0.x: Mojarra added
numberOfActiveViewMaps(2.3.18),refreshTransientBuildOnPSS(4.0.19) andrestoreBuildTimeDecisions(4.0.23), according to its reference. -
4.0: the
jakartanamespace andfaces.jsinstead ofjsf.js; the parameters that older material spellsjavax.faces.*arejakarta.faces.*from here on. -
4.1: full state saving is deprecated; Mojarra and MyFaces log a warning when
PARTIAL_STATE_SAVINGis set tofalse. -
5.0 (upcoming): the milestone releases remove deprecated APIs; this page was not checked against them. The PrimeFaces performance page mentions that a MyFaces parameter is renamed in 5.0 (
USE_LAMBDA_METAFACTORYtoUSE_LAMBDAS_OVER_REFLECTION).
Where the Books Are Outdated
The JSF 1.x books in the bibliography (the Mann and Jacobi and Fallows titles) describe a framework in which every view
saved its complete component tree; partial state saving arrived only in 2.0, which is why their warnings about page size
and session size are harsher than today’s. Their tuning advice about STATE_SAVING_METHOD is still the right lever, but it
now applies to a much smaller state, and full state saving is deprecated. The component-library chapters of those books
(ajax4jsf, Weblets, hand-written XMLHttpRequest code) are replaced by f:ajax and the ResourceHandler, which is
also where caching and combining now live.
JSF 2.0 Cookbook (2010) has recipes for testing and load-testing JSF applications and for serving resources through custom
resource handlers. The advice to load test and to serve images, CSS and JavaScript as Faces resources still holds; the
tools and the javax.faces code do not, and the cache headers and parameters in Resource Caching and Compression are the current form. The
PrimeFaces Cookbook, Second Edition (2015, PrimeFaces 5.x) shows lazy data tables, partial submit and the cache component.
Those recipes still exist in PrimeFaces 16, but LazyDataModel changed (the 16 documentation shows count and load with
Map<String, SortMeta> and Map<String, FilterMeta> arguments, and builders for lambdas and JPA), partial submit is now a
global primefaces.SUBMIT setting, and the cache component has a default provider and Ehcache 2 and 3 providers. Check the
PrimeFaces version before copying its code. None of the books covers CDI @ViewScoped, OmniFaces 5, MyFaces view pooling or
Production-stage defaults.
Pitfalls
-
Tuning without measuring. Change one setting, keep a baseline and repeat the load test. The loopback timings on this page are directions, not benchmarks.
-
Development stage in production. The per-request file checks and consistency walks are real costs; the project stage is the first thing to check.
-
Queries in getters and
renderedexpressions. Up to 13 queries per request on a page with five rows (see Getter Cost). -
Entities in view scope. Memory per open view, per user (View Scope Size).
-
execute="@form"andrender="@form"as a habit. Large payloads and unrelated validation failures (Scope Ajax Requests with Execute and Render). -
Turning off partial state saving. It is deprecated and the state grew by a factor of about 1.7 to 38 in the runs above.
-
Disabling client state encryption or integrity checks to look at the state. A debugging switch is not a tuning option.
-
Copying
compressViewState=falsefrom a tuning guide with server state: it has no effect there (State Saving: Partial, Full, Server, Client). -
Combining scripts without
target="head". They are silently left alone (Combining Resources with OmniFaces CombinedResourceHandler). -
Expecting
CombinedResourceHandlerto catch everything. Hard-coded<link>and<script>elements and renderer resources are not combined. -
MyFaces view pooling without
alwaysRecompile. The pool is disabled and the log says so; custom components that cannot reset themselves misbehave in a pooled view. -
Caching a view with a view state in the browser. Use cache headers that forbid it, as with
CacheControlFilter. -
Load tests that count HTTP 200 only. A
ViewExpiredExceptionerror page returns 200 on many setups; assert on content. -
A JMeter Ajax sampler without the
Faces-Requestheader. Mojarra then renders the full page. -
Putting user-specific content inside
p:cache. The fragment is shared by everyone.
References
-
Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
-
Jakarta EE Tutorial: Using Ajax with Jakarta Faces Technology
-
Leonard: JSF 2.0 Cookbook, Packt, 2010, publisher page
-
Çalışkan, Varaksin: PrimeFaces Cookbook, Second Edition, Packt, 2015, publisher page