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.

Table 1. Where each cost lands
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.

Getter Cost

Render Response

Evaluate every rendered, value and styleClass expression, encode every component, then save the state.

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.

Production-ready context parameters (web.xml)
<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>
  • jakarta.faces.PROJECT_STAGE takes Development, UnitTest, SystemTest or Production. Mojarra’s reference lists Production as the value used when the parameter is absent, and the Jakarta EE Tutorial shows the parameter being set in web.xml.

  • jakarta.faces.FACELETS_REFRESH_PERIOD is the number of seconds between two checks of a view file on disk. The declared default is 0, which means "check on every request". Mojarra’s reference says the runtime itself applies -1 (never check) when the stage is Production and the parameter is not set; the MyFaces reference gives the default as "0, -1 in Production". Setting -1 explicitly makes the intent visible and does not depend on the stage.

  • jakarta.faces.FACELETS_SKIP_COMMENTS drops 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_SIZE is the number of bytes the response is buffered with while a view is rendered (default 1024, -1 leaves 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, true or auto) skips the walk that verifies that every component id in a naming container is unique. auto skips it in the Production stage. MyFaces has a similar switch, org.apache.myfaces.CHECK_ID_PRODUCTION_MODE (true, auto, false; auto checks 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:

tree-forEach.xhtml: 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>
tree-repeat.xhtml: 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>
tree-html.xhtml: plain markup inside the same 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:

Table 2. Size of a GET response and of the view state field, 300 rows
Page Mojarra, HTML Mojarra, client state field MyFaces, HTML MyFaces, client state field

tree-forEach (about 600 components)

18,730 bytes

about 750 characters

25,743 bytes

about 14,300 characters

tree-repeat (one template)

21,385 bytes

about 2,350 characters

28,773 bytes

about 620 characters

tree-html (markup)

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:repeat and h:dataTable hold one set of child components and re-run it per row (see the comparison). A table with rows="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:outputText when you need neither converters nor the output tag’s other features, and a plain div instead of p:outputPanel. A plain div that Ajax has to update can be made addressable with a passthrough attribute (<div faces:id="...">); see the HTML5 friendly markup page. Keep h:outputText where 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 a rendered="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 for dynamic="true".

  • Split the page. Several small views joined by navigation or Ajax (render targets) 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:

QueryInGetterBean (the anti-pattern)
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;
    }
}
LoadOnceBean (the fix)
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;
    }
}
getter-query.xhtml (the other view only changes the bean name)
<!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>

Counted by the instrumentation, one GET and then one postback of the form (five books, Production stage):

Table 3. Getter calls and queries per request
View Mojarra GET Mojarra postback MyFaces GET MyFaces postback

getter-query (query in the getter)

4 calls, 4 queries

13 calls, 13 queries

3 calls, 3 queries

10 calls, 10 queries

getter-once (loaded in @PostConstruct)

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), an f:viewAction or 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 DataModel per 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. rendered expressions 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:

ResultsBean
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-page DataModel loads 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=true or 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 in ViewExpiredException, 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:

Table 4. Length of the view state field with client state saving
Page Mojarra, partial Mojarra, full MyFaces, partial MyFaces, full

getter-once (small form)

704

2,476

152

5,824

tree-repeat

2,348

3,884

620

5,528

tree-forEach

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:

Table 5. State saving parameters
Mojarra MyFaces Effect (from the implementations' documentation)

jakarta.faces.SERIALIZE_SERVER_STATE (default false)

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.

com.sun.faces.compressViewState (default true)

org.apache.myfaces.COMPRESS_STATE_IN_CLIENT (default false), org.apache.myfaces.COMPRESS_STATE_IN_SESSION (default true)

GZIP-compress the serialised state. For Mojarra it matters with client state or serialised server state.

com.sun.faces.numberOfLogicalViews, com.sun.faces.numberOfViewsInSession (15 and 15)

org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION (20)

How many saved views the session keeps (server state).

com.sun.faces.clientStateWriteBufferSize (8192)

-

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.

stateless.xhtml
<!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):

ajax-scope.xhtml
<!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>
BulkEditBean
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++;
    }
}

The server-side counters and the Ajax response for each button, hand-posted on both implementations with the identifiers that faces.js sends:

Table 6. Server effect and response size of the two buttons
Button Setters and action called Mojarra response MyFaces response

Narrow (execute="@this", render="saves")

save() only

270 bytes

280 bytes

Wide (execute="@form", render="@form")

setTitle, setAuthor, save()

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 @form or @all. A table’s single cell or a message component is usually enough.

  • Give the page stable container ids so that a narrow render can name them. Search expressions (Search Expressions) such as @parent and @child(0) keep the targets short.

  • Group related actions on one Ajax request instead of firing several, and consider delay for per-keystroke events (the Ajax page covers delay and f: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 process and update.

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:

Table 7. Cache headers observed (Production stage)
Response Mojarra 4.1.17 MyFaces 4.1.4

/jakarta.faces.resource/layout.css.xhtml?ln=bookshelf

Cache-Control: max-age=604800, ETag, Last-Modified

Cache-Control: max-age=604800, Last-Modified

A Facelets view (tree-repeat.xhtml)

no cache headers

no cache headers

Seven days is the default on both. The parameters:

Table 8. Resource caching parameters
Mojarra MyFaces Effect (from the implementations' documentation)

com.sun.faces.defaultResourceMaxAge (milliseconds, 604800000)

org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES (milliseconds, 604800000)

How long a client may keep a resource without asking again.

com.sun.faces.cacheResourceModificationTimestamp (false)

org.apache.myfaces.RESOURCE_CACHE_LAST_MODIFIED (true)

Remember the last-modified time instead of reading it from the file system on every request; Mojarra’s reference says true is what you want in production, MyFaces applies it in the production stage.

com.sun.faces.resourceUpdateCheckPeriod (minutes, 5; -1 never)

-

How often a cached resource is checked for modification.

com.sun.faces.compressableMimeTypes (none)

-

Comma-separated MIME types (a trailing /* is a wildcard) the handler compresses with GZIP.

com.sun.faces.resourceBufferSize (2048)

org.apache.myfaces.RESOURCE_BUFFER_SIZE (2048)

Buffer used while streaming a resource.

-

org.apache.myfaces.RESOURCE_HANDLER_CACHE_ENABLED (true), org.apache.myfaces.RESOURCE_HANDLER_CACHE_SIZE (500)

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:

web.xml fragment (OmniFaces 5.5.5)
<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:

WEB-INF/faces-config.xml
<application>
    <resource-handler>org.omnifaces.resourcehandler.CombinedResourceHandler</resource-handler>
</application>
resources.xhtml
<!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:outputStylesheet defaults to target="head"; h:outputScript does not, so an h:outputScript in the head needs target="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):

Table 9. CombinedResourceHandler context parameters
Parameter Effect

org.omnifaces.COMBINED_RESOURCE_HANDLER_DISABLED

true turns the handler off. The Javadoc shows an EL value such as #{facesContext.application.projectStage eq 'Development'} to disable it in development.

org.omnifaces.COMBINED_RESOURCE_HANDLER_EXCLUDED_RESOURCES

Comma-separated resource identifiers (library:name, or just name) which stay separate. Run with bookshelf:page.css: that link stayed in the head and the combined stylesheet held only layout.css.

org.omnifaces.COMBINED_RESOURCE_HANDLER_SUPPRESSED_RESOURCES

Resources removed from the page altogether.

org.omnifaces.COMBINED_RESOURCE_HANDLER_INLINE_CSS, org.omnifaces.COMBINED_RESOURCE_HANDLER_INLINE_JS

true renders the combined content inline in the HTML instead of as a resource.

org.omnifaces.COMBINED_RESOURCE_HANDLER_CACHE_TTL

Seconds to cache the combined content on the server (default 0, off). Considered only when the project stage is not Development. The cache holds one entry per distinct combination requested.

org.omnifaces.CACHE_SETTING_APPLICATION_MAX_CAPACITY

Caps the application-scoped cache shared with o:cache, recommended by the Javadoc for deployments with many distinct combinations (not run).

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:

Table 10. CACHE_EL_EXPRESSIONS modes
Value Behaviour

noCache (default)

Every expression is created each time the view is built.

always

Do not cache only inside user tags or when the expression uses a variable resolved through the VariableMapper.

allowCset

Like always, but not when ui:param was used in the current template context.

strict

Like allowCset, but also not when c:set with var and value only is used. Any c:if or ui:param disables caching of the expressions inside.

alwaysRecompile

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 always mode has with ui:param.

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:

web.xml
<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:

WEB-INF/faces-config.xml
<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:

pooled.xhtml
<!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):

Table 11. View pool parameters
Parameter Default Effect

org.apache.myfaces.VIEW_POOL_MAX_POOL_SIZE

5

Views kept per view metadata definition.

org.apache.myfaces.VIEW_POOL_MAX_DYNAMIC_PARTIAL_LIMIT

2

Dynamic views that may be reused as partial views.

org.apache.myfaces.VIEW_POOL_ENTRY_MODE

soft

soft or weak reference: how the garbage collector treats pooled views.

org.apache.myfaces.VIEW_POOL_DEFERRED_NAVIGATION

false

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 (default false) sends the response head to the browser while the rest renders so that it can start fetching resources.

  • org.apache.myfaces.USE_LAMBDA_METAFACTORY (default false) 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 called USE_LAMBDAS_OVER_REFLECTION in MyFaces 5.0.

  • org.apache.myfaces.VIEW_UNIQUE_IDS_CACHE_ENABLED, org.apache.myfaces.VIEWID_EXISTS_CACHE_ENABLED and the other VIEWID_* cache switches are on by default.

  • Whitespace compression, configured in faces-config.xml with <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:

CatalogueLazyModel
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());
    }
}
PrimeCatalogueBean
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;
    }
}
lazy-table.xhtml
<!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:

tabs.xhtml
<!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>

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:

cache.xhtml
<!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:

web.xml (PrimeFaces)
<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):

Table 12. Regular Expression Extractor settings
Field Value

Apply to / Field to check

Main sample only / Body

Name of created variable

viewState

Regular Expression

name="jakarta\.faces\.ViewState"[^>]*value="([^"]*)"

Template

$1$

Match No.

1

Default Value

VIEWSTATE_NOT_FOUND (so a missing field fails visibly instead of sending an empty state)

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 button action) and jakarta.faces.partial.event, with the resolved client ids that faces.js would 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 update element: 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:

Regular expression for the view state in a partial response
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 Production stage (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 than numberOfViewsInSession (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 render list) and compare the same plan before and after, on the same machine.

Mojarra and MyFaces

Table 13. Differences that matter for performance
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 ui:repeat

about 2,350 characters

about 620 characters

Ajax request payload, execute="@this"

only the Ajax parameters and state (284 bytes)

every input of the form (308 bytes for a two-input form)

Ajax post without Faces-Request header

answered with the full HTML page

answered with the partial response

Resource headers

Cache-Control, ETag, Last-Modified

Cache-Control, Last-Modified

State saving limits

numberOfLogicalViews, numberOfViewsInSession, numberOfActiveViewMaps

NUMBER_OF_VIEWS_IN_SESSION

EL caching, view pooling, lambda access, early flush

not available

CACHE_EL_EXPRESSIONS, VIEW_POOL_*, USE_LAMBDA_METAFACTORY, EARLY_FLUSH_ENABLED

Full state saving (PARTIAL_STATE_SAVING=false)

deprecation warning at startup

deprecation warning at startup

Literal @form as the execute value of a hand-made request

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), the PROJECT_STAGE parameter and FACELETS_REFRESH_PERIOD. MyFaces 2.0.8 and 2.1.2 introduced EL expression caching, and alwaysRecompile arrived in 2.1.12 (from the MyFaces documentation).

  • 2.2: stateless views (<f:view transient="true">) and jakarta.faces.SERIALIZE_SERVER_STATE (then in the javax namespace).

  • 2.3.x / 4.0.x: Mojarra added numberOfActiveViewMaps (2.3.18), refreshTransientBuildOnPSS (4.0.19) and restoreBuildTimeDecisions (4.0.23), according to its reference.

  • 4.0: the jakarta namespace and faces.js instead of jsf.js; the parameters that older material spells javax.faces.* are jakarta.faces.* from here on.

  • 4.1: full state saving is deprecated; Mojarra and MyFaces log a warning when PARTIAL_STATE_SAVING is set to false.

  • 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_METAFACTORY to USE_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 rendered expressions. 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" and render="@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=false from 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 CombinedResourceHandler to 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 ViewExpiredException error page returns 200 on many setups; assert on content.

  • A JMeter Ajax sampler without the Faces-Request header. Mojarra then renders the full page.

  • Putting user-specific content inside p:cache. The fragment is shared by everyone.