Every postback in Jakarta Faces (formerly known as JSF) builds the view twice from the Facelets file, first during restore view and then during render response. The jakarta.faces.ViewState hidden field (formerly named javax.faces.ViewState) carries what one request hands over to the next, in order to ensure that the restored view is exactly the same as the view which was submitted. Once you see those two things, the old question whether JSTL in Facelets makes sense mostly answers itself.
This article walks through the hidden field and what is actually in it, decoded for several common pages, and when rowStatePreserved is worth its cost. It then covers the difference between build time and render time, <c:forEach> versus <ui:repeat>, and what changed recently around JSTL tags whose outcome differs on a postback. It ends with what dynamic components and composite components cost in view state and build time, what compressing the client side view state is worth and when to choose client side state saving at all, followed by the issues in the view state of Mojarra which were fixed along the way. All behavior and all numbers below are measured on Tomcat 11 with Mojarra 4.1.21 and MyFaces 4.1.5-SNAPSHOT at 59c652b.
The jakarta.faces.ViewState hidden field
Every <h:form> renders a hidden field like this.
<input type="hidden"
name="jakarta.faces.ViewState"
id="j_id1:jakarta.faces.ViewState:0"
value="-5680257931536152847:-2882457509654920210" />
The name is fixed by the specification: it is the constant ResponseStateManager.VIEW_STATE_PARAM. The id is fixed as well. It is composed of the client ID of the UIViewRoot, the separator character, the name, the separator character again, and a number which is unique within the view. That number simply counts the forms. So with two forms you get :0 and :1 in Mojarra, or :1 and :2 in MyFaces, whose view root ID is j_id__v_0 instead of j_id1. Both forms carry the same value, because there is only one view.
In case you wish to change the generated ID of the view root: <f:view id="foo"> has no effect. The <f:view> tag has no id attribute, and both implementations silently ignore it. Call UIViewRoot#setId() from a <f:event> instead.
<f:metadata>
<f:event type="postAddToView" listener="#{view.setId('foo')}" />
</f:metadata>
The hidden field ID then becomes foo:jakarta.faces.ViewState:0 in Mojarra and foo:jakarta.faces.ViewState:1 in MyFaces, also on a postback. The listener runs during every build of the view, so on a postback the new ID is already there before the render response phase.
What the value contains, depends on the jakarta.faces.STATE_SAVING_METHOD context parameter.
- With
server, the default, the state is stored in the HTTP session and the value is only a key to look it up. In Mojarra those are two numbers separated by a colon, in MyFaces a short Base64 string. - With
client, the value is the state itself. Mojarra serializes it, compresses it with GZIP, encrypts it and encodes it in Base64. MyFaces does the same, but without compression, unlessorg.apache.myfaces.COMPRESS_STATE_IN_CLIENTis set totrue. For a small page with two forms that was about 320 characters in Mojarra and about 620 in MyFaces, in Production stage.
The specification strongly recommends making the value difficult to predict, as a guard against cross site attacks. With server side state saving, the default, both implementations honor it: the value is a random key into the HTTP session, a value issued to one session is rejected in another one, and so the hidden field also acts as a CSRF token for postbacks. A plain GET request has no form, and therefore no view state. For pages which may not be opened via a forged link, declare them as protected views in faces-config.xml. An <h:link> to such a page then carries a jakarta.faces.Token request parameter, and a request without it fails with a ProtectedViewException.
<protected-views>
<url-pattern>/protected.xhtml</url-pattern>
</protected-views>
A view with <f:view transient="true"> is stateless. Its forms still get the hidden field, but the value is literally stateless. Nothing is saved, and the specification does not require @ViewScoped beans to work in such a view.
The view state is not the component tree
A common misunderstanding is that the view state holds the component tree. Current AI models make this mistake too, which is one reason for claude-faces-expert and for this blog article. The view state does not hold the component tree, at least not anymore since partial state saving was introduced in JSF 2.0 (December 2009!).
On every request, also on a postback, the component tree is built from scratch from the Facelets file. The view state holds only the delta: what is different from the tree as the Facelets file produces it. That is for example a programmatically changed attribute, the local value of an input, or a component which was added programmatically after the build. This is called partial state saving, and it has been the default since JSF 2.0. The alternative, full state saving via jakarta.faces.PARTIAL_STATE_SAVING set to false, is deprecated since Faces 4.1 and removed in Faces 5.0, along with jakarta.faces.FULL_STATE_SAVING_VIEW_IDS. Backing beans are not in the view state either: a @ViewScoped bean is stored in the HTTP session.
So the restore of a postback works in two steps. First the tree is built again from the Facelets file. Then the saved delta is applied over it. For this to work, the second build must produce the same tree as the build which rendered the previous response. Remember that one, because everything about JSTL below comes down to it.
What is actually in it
When using client side state saving, you can see that delta for yourself by decoding the hidden field. With both encryption and compression turned off, the value is nothing more than Base64 of a serialized Java object. You can turn off the encryption in MyFaces via the org.apache.myfaces.USE_ENCRYPTION context parameter, and in Mojarra via com.sun.faces.disableClientStateEncryption, but only in Development stage. With server side state saving there is nothing to decode, as the hidden field then holds only a key into the HTTP session.
Below are the decoded contents after one postback of eight small pages, and once after a second postback.
These are simplified for humans to read. In reality the state is a tree of serialized Java objects: mostly nested Object[] arrays, with maps, lists, strings and enum constants in their slots, and an occasional dedicated class such as SavedState. Below, empty slots are left out, slots which an implementation stores by position are named after its source code, a property is shown as name = value instead of as a pair of an enum constant and its value, and a list of a single element is shown as that element. Both also save which view the state belongs to: MyFaces the view ID, and Mojarra a hash of it.
Each label gives the size of the view state after that postback as it is in Production stage with the default settings: the serialized Java objects, before any compression, encryption and Base64 encoding. That size does not depend on the state saving method. With server side state saving the same state is kept in the HTTP session, by default as Java objects, and only as these serialized bytes when jakarta.faces.SERIALIZE_SERVER_STATE is set to true. With <distributable/> in web.xml, the server additionally serializes the whole HTTP session, including those objects, whenever it replicates the session to another server. With client side state saving the hidden field carries it, encrypted, and in Mojarra also compressed.
A form with only a command button, and nothing changes.
<h:form id="form">
<h:commandButton id="submit" value="Submit" />
</h:form>
Mojarra (12 bytes)
view ID hash = 1673905523
MyFaces (73 bytes)
view ID = "/page.xhtml"
Even when nothing is saved, the view state is not empty: it still holds the container the state travels in. In Mojarra, that is a Java serialization stream of 12 bytes: a header of 4 bytes, the hash of the view ID in 6 bytes, a null for the tree structure which partial state saving does not use, and a null for the component states. When using client side state saving, GZIP makes that 32 bytes in Mojarra, because its header and trailer cost more than it saves on so little data. The encryption pads that to 48 bytes and adds a random initialization vector of 16 bytes and an HMAC-SHA256 signature of 32 bytes. Those 96 bytes become 128 characters in Base64.
MyFaces serializes an array with the view ID, 73 bytes on this page. When using client side state saving, it does not compress that, and encrypts it into 80 bytes plus a signature of 32 bytes, which become 152 characters.
Both save the view ID as a safety net. On the postback, they compare it with the view ID of the request, and when they differ, the request fails with a ViewExpiredException. Both do the same with server side state saving, where Mojarra keeps the complete view ID beside the state. In the client side state a hash is enough, as it only has to detect a mismatch. So the view state of one page is not applied to the tree of another page. The check is a fail-fast safety net, not a security boundary.
In practice the check rarely fires, because <h:form> renders the URL of the current view as its action, so a Faces form always posts back to the page which rendered it.
In Development stage only, Mojarra additionally saves a com.sun.faces.RenderedTags entry in the view state. It holds for every component the ID of the Facelets tag which built it. With that, the build which restores the view can detect and report where it unexpectedly produced another tree than the one which was rendered, which is exactly the problem described in "A build time decision which changes on postback" further below. Mojarra then logs a warning which helpfully names the affected components, instead of silently dropping their submitted values (#5964). It does not exist in Production stage, so it is left out of all dumps below.
Two inputs, both valid.
The postback submits 42 in the first one and x in the second one.
<h:inputText id="first" value="#{bean.first}" />
<h:inputText id="second" value="#{bean.second}" required="true" />
<h:commandButton id="submit" value="Submit" />
Mojarra (12 bytes)
view ID hash = 1673905523
MyFaces (69 bytes)
view ID = "/page.xhtml"
The validation passed, so Update Model Values pushed both values into the model and cleared the local values of the inputs. Nothing of the inputs is left to save, and the view state is the same as for the form with only a command button.
Two inputs, the second one with validation error.
The same page, but the postback submits 42 in the first one and nothing in the second one.
Mojarra (491 bytes)
view ID hash = 1673905523
form:first
value = "42"
localValueSet = true
form:second
valid = false
MyFaces (521 bytes)
view ID = "/page.xhtml"
form:first
value = "42"
localValueSet = true
form:second
valid = false
The validation failed, so the model was not updated, and the first input still holds its submitted value as local value. That is saved, together with the invalid state of the second input.
Why save that at all? On a full postback the browser submits every input again, so the next request decodes them anyway. The saved state however matters for a subsequent postback request which renders the inputs without executing them, such as an ajax request with execute="@this" and render="@form". That request renders the first input with its saved local value, and the second input still as invalid.
That is what you want when the user, after the validation error, picks a country in a dropdown which re-renders the form via ajax. The form then still shows what the user typed, and not the value of the model, which was never updated.
An attribute changed in Java, from an action method.
<h:outputText id="text" value="Hello" />
<h:commandButton id="submit" value="Submit" action="#{bean.changeStyleClass}" />
public void changeStyleClass() {
((HtmlOutputText) FacesContext.getCurrentInstance().getViewRoot().findComponent("form:text")).setStyleClass("changed");
}
Mojarra (293 bytes)
view ID hash = 1673905523
form:text
styleClass = "changed"
MyFaces (736 bytes)
view ID = "/page.xhtml"
form:text
styleClass = "changed"
attributesMap = { "oam.ATTRIBUTES_MARKED" = 2 }
Only the changed attribute is saved. The value of Hello comes from the Facelets file, so it is not in there. On the next postback, the build creates the component again from the Facelets file, without the style class. The restore then applies the saved style class over it. So the component keeps the changed style class on every following postback, also when the action is not invoked again.
MyFaces saves more here than Mojarra. The oam.ATTRIBUTES_MARKED entry is a bitmask of which common HTML attributes are set, with 2 standing for styleClass. Its renderers read that bitmask to decide which attributes to write, so it has to ride along in the state, where it is stored as a nested map and costs more than the value it guards. Mojarra keeps the same kind of bookkeeping, a list named attributesThatAreSet, but it rebuilds that list on restore, so its entry above holds only the value.
A component added after the view was built, from an action method.
This is the case covered in "Dynamic components" further below.
<h:form id="form">
<h:panelGroup id="target" />
<h:commandButton id="submit" value="Submit" action="#{bean.addComponent}" />
</h:form>
public void addComponent() {
UIComponent target = FacesContext.getCurrentInstance().getViewRoot().findComponent("form:target");
HtmlOutputText text = new HtmlOutputText();
text.setId("added" + target.getChildren().size());
text.setValue("Added");
target.getChildren().add(text);
}
The ID gets a number, because every click of the button adds one more component. With a fixed ID, the second click would add a second component with the same ID: Mojarra then fails with "JSF1007: Duplicate component ID form:added found in view", and MyFaces renders two elements with the same ID. After one click, the state holds:
Mojarra (732 bytes)
view ID hash = 1673905523
form:added0
class = jakarta.faces.component.html.HtmlOutputText
value = "Added"
initialStateMarked = false
rendererType = "jakarta.faces.Text"
id = "added0"
com.sun.faces.DynamicActions
ADD, parent = "form:target", clientId = "form:added0", id = "added0", index = 0
MyFaces (1592 bytes)
view ID = "/page.xhtml"
form:added0
parent = "form:target"
index = 0
structure = jakarta.faces.component.html.HtmlOutputText, id = "added0"
state
value = "Added"
attributesMap = { "oam.COMPONENT_ADDED_AFTER_BUILD_VIEW" = ADDED }
id = "added0"
clientId = "form:added0"
rendererType = "jakarta.faces.Text"
capabilities = 0
j_id__v_0
attributesMap = { "oam.CLIENTIDS_ADDED" = [ "form:added0" ] }
Both save the complete state of the added component, including what a Facelets file would otherwise provide, such as its ID and its renderer type. They also save where to add it again on the next postback: Mojarra in a separate list of actions, MyFaces with the component itself and in the attributes of the view root. On the next postback, the build does not create the added component, as the Facelets file does not have it. The restore then creates it from its saved state and adds it again at its saved place. So it stays in the view on every following postback, also when the action is not invoked again. In Mojarra, initialStateMarked = false marks the entry as a full state instead of a delta. It is never true; a delta leaves it empty.
After a second click, the state holds one more of everything:
Mojarra (891 bytes)
view ID hash = 1673905523
form:added0
class = jakarta.faces.component.html.HtmlOutputText
value = "Added"
initialStateMarked = false
rendererType = "jakarta.faces.Text"
id = "added0"
form:added1
class = jakarta.faces.component.html.HtmlOutputText
value = "Added"
initialStateMarked = false
rendererType = "jakarta.faces.Text"
id = "added1"
com.sun.faces.DynamicActions
ADD, parent = "form:target", clientId = "form:added0", id = "added0", index = 0
ADD, parent = "form:target", clientId = "form:added1", id = "added1", index = 1
MyFaces (1832 bytes)
view ID = "/page.xhtml"
form:added0
parent = "form:target"
index = 0
structure = jakarta.faces.component.html.HtmlOutputText, id = "added0"
state
value = "Added"
attributesMap = { "oam.COMPONENT_ADDED_AFTER_BUILD_VIEW" = ADDED }
id = "added0"
clientId = "form:added0"
rendererType = "jakarta.faces.Text"
capabilities = 0
form:added1
parent = "form:target"
index = 1
structure = jakarta.faces.component.html.HtmlOutputText, id = "added1"
state
value = "Added"
attributesMap = { "oam.COMPONENT_ADDED_AFTER_BUILD_VIEW" = ADDED }
id = "added1"
clientId = "form:added1"
rendererType = "jakarta.faces.Text"
capabilities = 0
j_id__v_0
attributesMap = { "oam.CLIENTIDS_ADDED" = [ "form:added0", "form:added1" ] }
Every added component brings its complete state, and its place in the tree.
A table with two inputs in each row, and no validation error.
Below the table is a required input, which the postback fills in.
<h:dataTable id="table" value="#{bean.items}" var="item">
<h:column><h:inputText id="name" value="#{item.name}" /></h:column>
<h:column><h:inputText id="value" value="#{item.value}" /></h:column>
</h:dataTable>
<h:inputText id="second" value="#{bean.second}" required="true" />
<h:commandButton id="submit" value="Submit" />
@Named
@RequestScoped
public class Bean {
private List<Item> items = List.of(new Item("name1", "value1"), new Item("name2", "value2"));
public List<Item> getItems() {
return items;
}
// Getter and setter of second, and Item with a name and a value property.
}
Mojarra (12 bytes)
view ID hash = 1673905523
MyFaces (69 bytes)
view ID = "/page.xhtml"
Just like with the two valid inputs, the model was updated and the local values were cleared, so nothing of the table is left to save.
A table with two inputs in each row, and a validation error below it.
The same page, but the postback leaves the required input below the table empty.
Mojarra (684 bytes)
view ID hash = 1673905523
form:table
childState
form:table
0
-
value = "name1"
localValueSet = true
-
value = "value1"
localValueSet = true
1
-
value = "name2"
localValueSet = true
-
value = "value2"
localValueSet = true
form:second
valid = false
MyFaces (773 bytes)
view ID = "/page.xhtml"
form:table
rowStates
form:table:0
-
localValue = "name1"
localValueSet = true
valid = true
-
localValue = "value1"
localValueSet = true
valid = true
form:table:1
-
localValue = "name2"
localValueSet = true
valid = true
-
localValue = "value2"
localValueSet = true
valid = true
form:second
valid = false
The <h:dataTable> has only one physical component per input, which it processes once per row. Per row, both implementations save a list with the state of each input of that row, in the order of the component tree. The restore matches that list to the inputs by position, not by client ID, so no form:table:0:name appears in it. A third input in the row adds a third entry per row. MyFaces also saves the fields which are at their default, Mojarra does not.
On a full postback this makes no difference: the inputs are part of the form, so the browser submits their values again, and both implementations redisplay what the user typed. It does make a difference on an ajax request which re-renders the table without executing it, for example one which executes only the required input. The UIData.encodeBegin() javadoc says that the saved per-row state is discarded "unless it is needed to rerender the current page with errors". Both keep it then, so the table still shows the typed values. Without errors, both discard it and show the values of the model.
A value from a @ViewScoped bean.
<h:form id="form">
<h:outputText id="text" value="#{bean.value}" />
<h:commandButton id="submit" value="Submit" />
</h:form>
@Named
@ViewScoped
public class Bean implements Serializable {
private String value = "a value which lives in the view scoped bean";
// Getter and setter of value.
}
Mojarra (177 bytes)
view ID hash = 1673905523 j_id1 = "4f5e93fd-72cb-4496-b72c-f7cc39cd1513"
MyFaces (430 bytes)
view ID = "/page.xhtml" j_id__v_0 = "-1659844830"
The bean itself is not in there. The view root saves only the ID under which the beans of this view are kept in the HTTP session.
With server side state saving, all of this is kept in the HTTP session instead, per view, while the hidden field holds only the key.
When to use rowStatePreserved
An iterating component such as <h:dataTable> or <ui:repeat> has only one physical component for each component in its body, and processes it once per row. To still let every row have its own input, it keeps per row the four fields which UIData.setRowIndex() lists: value, localValueSet, valid and submittedValue. That is all it takes for the values the user typed to survive a validation error, as the dumps above show. You do not need rowStatePreserved for that.
Every other property belongs to the single component which all rows share. When Java code changes such a property while the table is positioned on one row, the change shows up in all rows. The UIData.setRowStatePreserved() javadoc names the example of a style class. Take a button in each row which marks the input of its own row.
<h:dataTable id="table" value="#{bean.items}" var="item" rowStatePreserved="true">
<h:column>
<h:inputText id="name" value="#{item.name}" />
<h:commandButton id="mark" value="Mark" actionListener="#{bean.mark}" />
</h:column>
</h:dataTable>
public void mark(ActionEvent event) {
UIComponent column = event.getComponent().getParent();
((HtmlInputText) column.getChildren().get(0)).setStyleClass("marked");
}
Without the attribute, the style class lands on the one input which all rows share, so every row is marked. With rowStatePreserved="true", the table saves and restores the full state of every component in it, inputs or not, every time it moves to another row, so only the clicked row is marked. The attribute exists on <h:dataTable> since JSF 2.1 (jakartaee/faces#153), and on <ui:repeat> since Faces 4.1 (jakartaee/faces#1263).
The view state after a click on the button of the first row shows where the style class went. Without the attribute, it is saved for the one input which all rows share.
Mojarra (308 bytes)
view ID hash = 1673905523
form:table:name
styleClass = "marked"
MyFaces (737 bytes)
view ID = "/page.xhtml"
form:table:name
styleClass = "marked"
attributesMap = { "oam.ATTRIBUTES_MARKED" = 2 }
With rowStatePreserved="true", it is saved in the state of the first row only, for the input of that row.
Mojarra (400 bytes)
view ID hash = 1673905523
form:table
rowDeltaStates
form:table:0
form:table:0:name
styleClass = "marked"
MyFaces (828 bytes)
view ID = "/page.xhtml"
form:table
rowDeltaStates
form:table:0
form:table:0:name
styleClass = "marked"
attributesMap = { "oam.ATTRIBUTES_MARKED" = 2 }
The second row has no entry, because nothing changed there. A click on its button adds an entry for form:table:1:name next to it.
On the table with two inputs in each row of "What is actually in it", with the attribute, a postback without validation error leaves this state:
<h:dataTable id="table" value="#{bean.items}" var="item" rowStatePreserved="true">
<h:column><h:inputText id="name" value="#{item.name}" /></h:column>
<h:column><h:inputText id="value" value="#{item.value}" /></h:column>
</h:dataTable>
Mojarra (12 bytes)
view ID hash = 1673905523
MyFaces (79 bytes)
view ID = "/page.xhtml"
Both save nothing, just like the same table without the attribute.
The table saves a row as the difference between the state of the components and their initial state. Process Validations leaves a local value in the input, so the difference of each row is stored. Update Model Values clears that local value again, which brings the input back to its initial state. The difference is then empty, so the table removes the stored difference of that row.
With a validation error below the table, the local values are not cleared, so both save the rows, plus the invalid state of the required input. In Mojarra that is 908 bytes, against 684 for the same table without the attribute.
Mojarra (908 bytes)
view ID hash = 1673905523
form:table
rowDeltaStates
form:table:0
form:table:0:name
value = "name1"
localValueSet = true
form:table:0:value
value = "value1"
localValueSet = true
form:table:1
form:table:1:name
value = "name2"
localValueSet = true
form:table:1:value
value = "value2"
localValueSet = true
form:second
valid = false
But first ask yourself whether the difference can come from the data of the row instead. Mark the item rather than its component, and let EL pick the style class.
<h:dataTable value="#{bean.items}" var="item">
<h:column>
<h:inputText value="#{item.name}" styleClass="#{item.marked ? 'marked' : null}" />
<h:commandButton value="Mark" action="#{bean.mark(item)}" />
</h:column>
</h:dataTable>
@Named
@ViewScoped
public class Bean implements Serializable {
private List<Item> items; // Loaded in @PostConstruct.
public void mark(Item item) {
item.setMarked(true);
}
// Getter of items, and Item with a name and a marked property.
}
The bean is @ViewScoped, so that the items, and with them the mark, survive the postbacks of the view. Both implementations then mark only the clicked row, just like with rowStatePreserved="true".
That needs no attribute, and it costs nothing in the view state. rowStatePreserved is only for state which the components themselves hold and which differs per row, for example state which a third party component keeps internally, or component properties which are changed in Java and cannot be derived from the row data. It is not free: the table saves and restores every component in it on every row change. For the small table above, the view state of a postback with a validation error grew from 684 to 908 bytes in Mojarra, and from 773 to 935 bytes in MyFaces.
Build time versus render time
During a build, Facelets evaluates only a few EL expressions: the attributes of the JSTL core tags such as <c:if> and <c:forEach>, the src of <ui:include>, the template of <ui:composition> and <ui:decorate>, the attributes of <f:view> such as encoding and locale, the basename of <f:loadBundle>, and the id and binding attributes of components. All other component attributes, such as value and rendered, and also the value of <ui:repeat>, are evaluated later, when the components are actually processed or rendered. You can see it by letting a request scoped bean record every getter call together with the current phase. In the page below, every getter of #{bean} adds the name of the tag and attribute which called it, such as c:if test, to that trace.
<h:form>
<c:if test="#{bean.test}">
<c:forEach items="#{bean.forEachItems}" var="item">
<span>c:forEach #{item}</span>
</c:forEach>
</c:if>
<ui:include src="#{bean.src}" />
<h:outputText id="#{bean.id}" value="#{bean.value}" rendered="#{bean.rendered}" />
<ui:repeat value="#{bean.repeatItems}" var="item">
<span>ui:repeat #{item}</span>
</ui:repeat>
<h:commandButton value="Submit" />
</h:form>
This is what a postback of this page reported in Mojarra.
RESTORE_VIEW 1: c:if test RESTORE_VIEW 1: c:forEach items RESTORE_VIEW 1: ui:include src RESTORE_VIEW 1: h:outputText id APPLY_REQUEST_VALUES 2: h:outputText rendered APPLY_REQUEST_VALUES 2: ui:repeat value PROCESS_VALIDATIONS 3: h:outputText rendered UPDATE_MODEL_VALUES 4: h:outputText rendered RENDER_RESPONSE 6: c:if test RENDER_RESPONSE 6: c:forEach items RENDER_RESPONSE 6: ui:include src RENDER_RESPONSE 6: h:outputText rendered RENDER_RESPONSE 6: h:outputText value RENDER_RESPONSE 6: ui:repeat value
The first four lines are the build which restores the view. The rendered attribute is then evaluated in every phase which processes the component. Right before rendering, the JSTL tags and the <ui:include> are evaluated once more. That is the second build, which is described further below. A plain GET request has only one build, during the Render Response phase. MyFaces reports the same, except that the build which restores the view does not evaluate the test of the <c:if> and the src of the <ui:include>.
That is all there is to the JSTL question. Use JSTL to build the view: to decide which components end up in the tree at all, and how many. Use the rendered attribute to decide whether an existing component is rendered. Both can conditionally show a piece of the page, but they do it at a different moment, with different data available.
c:forEach versus ui:repeat
The <c:forEach> runs during the build and creates its body once per item, as physically separate components. The <ui:repeat> is one component which renders its single body once per item. The difference shows clearly with dynamic columns in a data table.
<h:dataTable value="#{bean.rows}" var="row">
<c:forEach items="#{bean.columns}" var="column">
<h:column>
<f:facet name="header">#{column}</f:facet>
#{row[column]}
</h:column>
</c:forEach>
</h:dataTable>
This gives one <h:column> per column, which is exactly what <h:dataTable> needs. Replace the <c:forEach> by a <ui:repeat> and the table has no columns at all: its only child is then a UIRepeat, and that is not a column.
The reverse goes wrong as well. A <c:if> inside a composite component is evaluated while the composite is built. When that composite sits in a <ui:repeat>, it is built only once, at a moment where the var of the <ui:repeat> does not exist yet.
<ui:repeat value="#{bean.numbers}" var="number">
<my:threshold value="#{number}" />
</ui:repeat>
A <c:if test="#{cc.attrs.value gt 1}"> inside that composite never passes, not even for the numbers above 1. A rendered="#{cc.attrs.value gt 1}" on a component inside it does, because that one is evaluated per row while rendering. Wrap the composite in a <c:forEach> instead, and the composite is built once per number, so its <c:if> sees the value of its own round.
A build time decision which changes on postback
Back to the rule that the restoring build must produce the same tree as the build which rendered the previous response. What if a JSTL tag decides differently on the postback? Take this form, opened via page.xhtml?show=true.
<h:form id="form">
<c:if test="#{param.show eq 'true'}">
<h:inputText id="text" value="#{bean.text}" />
</c:if>
<h:commandButton id="submit" value="Submit" action="#{bean.submit}" />
</h:form>
The input is rendered. But the form posts back to page.xhtml without that request parameter. So the build which restores the view evaluates the <c:if> to false and does not create the input at all. Nothing decodes the submitted value, and it silently does not reach the bean. No exception, and in Production stage no message either. In Development stage, Mojarra logs a warning which names the components. The same happens with a <c:forEach> whose range or items are not the same anymore on the postback, and with a <ui:include src> which resolves to another file. The classic advice was therefore to make sure that whatever such a decision depends on is still available on the postback, for example by keeping it in a @ViewScoped bean.
The root of the problem is one requirement: restoring the saved state of a view must produce that same view again, holding the same components at the same positions with the same state. In short, restore(save(view)) == view, always. That looks obvious, but partial state saving never guaranteed it. The view state holds only a delta, and the tree which that delta is applied to comes from a new build. When a JSTL tag decides differently in that build, the delta is applied to another tree than the one it was saved from, and the postback is processed by a tree which the user never saw. The restoring build can only produce the saved view when it reaches the same decisions as the build which rendered it.
This has now been solved at the root. The upcoming Faces 5.0 specification states that requirement in the javadoc of StateManagementStrategy.restoreView() (jakartaee/faces#2235). To meet it, the build which restores a view reproduces the decisions which the previous render reached, instead of evaluating them again. The build which precedes rendering still evaluates them against the current model, so the response does follow the model, only one phase later.
- MyFaces has done this since 2.2.0, via a
FaceletStatesaved with the view, and for<c:if>and<ui:include>already since 2.0.10 and 2.1.4 (MYFACES-3329). Nothing needs to be configured. - Mojarra 5.0 does it by default.
- Mojarra 4.0.23 and 4.1.14 and newer do it when you set the context parameter below (#5965).
- Older Mojarra versions do not do it. There you need a
@ViewScopedbean to keep track of whatever the build time decisions depend on.
<context-param>
<param-name>com.sun.faces.restoreBuildTimeDecisions</param-name>
<param-value>true</param-value>
</context-param>
The table below shows the outcome of three cases: the <c:if> above, a <c:forEach begin="1" end="#{param.count}">, and a <c:forEach items> over items of a request scoped bean which are gone on the postback.
| Case | Mojarra 4.1.21 | MyFaces 4.1.5 | |
|---|---|---|---|
| without the parameter | with the parameter | ||
<c:if> | dropped | arrives | arrives |
<c:forEach> over a range | dropped | arrives | arrives |
<c:forEach> over items which are gone | dropped | dropped, with a warning | arrives |
The last row needs an explanation. Mojarra saves the range and the number of rows of a <c:forEach>, but not the items themselves. Each row reads its item live from the model, so a row edits what the model holds now. When the model does not hold the items anymore, Mojarra still reproduces the rows, but a submitted value lands in a stand-in, and the server log gets a warning which says so. MyFaces does save the items themselves with the view state, and during the restoring build it binds each row to its saved item. In this test the inputs were bound to a map in the bean with the item as key, like #{bean.values[item]}, so the item was only needed as a key, and that was enough to get the values in.
This shows in the view state of a page with a <c:if>, a <c:forEach> over a range of literals and a <c:forEach> over the items of a @ViewScoped bean, opened via page.xhtml?show=true, after a postback in which nothing changed.
<h:form id="form">
<c:if test="#{param.show eq 'true'}">
<h:inputText id="text" value="#{bean.text}" />
</c:if>
<c:forEach begin="1" end="2" var="i">
<h:outputText id="range_#{i}" value="#{i}" />
</c:forEach>
<c:forEach items="#{bean.items}" var="item">
<h:outputText id="item_#{item.id}" value="#{item.name}" />
</c:forEach>
<h:commandButton id="submit" value="Submit" />
</h:form>
Mojarra without the parameter (177 bytes)
view ID hash = 1673905523 j_id1 = "e44eb009-8d4f-4752-bd2b-1082423daa49"
Mojarra with the parameter (586 bytes)
view ID hash = 1673905523
j_id1 = "e44eb009-8d4f-4752-bd2b-1082423daa49"
com.sun.faces.BuildTimeDecisions
1895078229_70f4990e = true
1895078229_70f499c9 = "range_1"
1895078229_70f499c9_1 = "range_2"
1895078229_70f49996 = "item_a"
1895078229_70f49996_1 = "item_b"
1895078229_70f499fc = { 0, 2147483646, 1, 2 }
1895078229_id = -1809659161
MyFaces (1466 bytes)
view ID = "/page.xhtml"
oam.FACELET_STATE_INSTANCE
7_1895078229_70f4993d = true
8_1895078229_70f499da
IterationState
counter = 2
valueList
- "0", 1, 1
- "1", 2, 2
9_1895078229_70f499e3
IterationState
counter = 4
valueList
- "2", Item { id = "a", name = "original a" }, 0
- "3", Item { id = "b", name = "original b" }, 1
j_id__v_0
attributesMap = "oam.FACELET_STATE_INSTANCE"
"-1849721011"
Without the parameter, Mojarra saves no decision at all, only the ID of the view scope. With the parameter, it saves the decisions under com.sun.faces.BuildTimeDecisions, each keyed by the ID of the tag in the Facelets file. The <c:if> saves its outcome. The <c:forEach> over a range of literals saves nothing, as it produces the same rows on every build.
The <c:forEach> over items saves the begin, end and step of its range, and the number of rows. The id attribute is evaluated during the build too, so each row also saves the ID which its component got. The items themselves are not saved.
The last entry is a checksum of the source of the Facelets file, keyed by the hash of its path. Mojarra saves one for every Facelets file which the build applied, but only when the view has a decision at all. When a Facelets file was edited between the render and the postback, the build stops replaying decisions at that file, and evaluates every decision from there on. So after an edit, a tag does not get the decision of another tag.
MyFaces saves a FaceletState in the attribute map of the view root. It holds the outcome of the <c:if>, and for each <c:forEach> an IterationState: per row the base of the IDs of its components, its item and its index, and in counter the next free base. So MyFaces saves the range of literals too, and the Item instances themselves.
That approach of MyFaces has a flip side. It shows when the input is bound to a property of the item itself.
<c:forEach items="#{bean.items}" var="item">
<h:inputText id="name_#{item.id}" value="#{item.name}" />
</c:forEach>
The table below shows this with the items in a @ViewScoped bean, so the same instances on every request, and with the items in a @RequestScoped bean which creates them anew on every request, as when they are loaded from a database. Nothing changes between the requests here, so no build time decision differs. The response printed the names from the bean.
| Items in | Mojarra 4.1.21 | MyFaces 4.1.5 | ||
|---|---|---|---|---|
| server | client | server | client | |
@ViewScoped bean | arrives | arrives | arrives | lost |
@RequestScoped bean | arrives | arrives | lost | lost |
Mojarra writes into the item of the bean in all cases. MyFaces writes into the item which it saved with the view state. With client side state saving that is always a deserialized copy, and with server side state saving it is a copy as soon as the bean holds new instances. The submitted value then lands in an object which nobody refers to anymore, without any message.
This is a deliberate choice of MyFaces, made in MyFaces 2.2.0 by MYFACES-3811, "Fix c:forEach behavior once for all". Faces 4 does not specify whether a row of a <c:forEach> reads its item from the items or from the saved state, so both implementations are technically correct there. Faces 5.0 will specify it: a row reads its item from the items, as Mojarra does (jakartaee/faces#2235). Mojarra does so also for the rows it reproduces with com.sun.faces.restoreBuildTimeDecisions, since 4.0.23 and 4.1.14 (#5965). MyFaces does so since 5.0 (MYFACES-4775). In MyFaces 4.x, the context parameter below makes it write into the item of the bean as well, with both state saving methods.
<context-param>
<param-name>org.apache.myfaces.STRICT_JSF_2_FACELETS_COMPATIBILITY</param-name>
<param-value>true</param-value>
</context-param>
That parameter however also changes how <c:set> and <ui:param> variables are visible across inclusions, so test the rest of the application with it. Which of both is right, is still an open question for the specification (jakartaee/faces#2248).
So for the items of a <c:forEach> the old advice still applies: keep them in a @ViewScoped bean, or recompute them in the @PostConstruct of a @RequestScoped bean. Inputs on properties of the items of a <c:forEach> are fine as long as the items are preserved: kept in a @ViewScoped bean, so that the postback writes into the same instances. In MyFaces 4.x that holds only with server side state saving. A <ui:repeat> or <h:dataTable> does not need that, as it evaluates its value during every phase. Everything else a build time decision depends on does not need to be kept alive anymore.
The cost of the second build
As the trace above showed, a postback builds the view a second time right before rendering, so that the response follows what the request did to the model. For a long time that meant the whole Facelets file was applied again as soon as it contained a single JSTL tag anywhere. Since Mojarra 4.0.23 and 4.1.14 it records every decision of a build, and before rendering it only evaluates those again. When they all still hold, the second build would produce exactly the same tree, so it is skipped (#5960). MyFaces also refreshes only the dynamic parts of the tree.
The table below shows both builds of a postback, measured with a ViewDeclarationLanguageWrapper which times restoreView() and buildView(), and the whole postback as seen by the client, from sending the request until the response was fully read. The page has about 800 components and runs in Production stage on the same machine as the client. Each number is the middle one of three runs, and each run took the median of 1500 postbacks after 300 warm-up postbacks. The variants are the page without any JSTL tag, with one <c:if> four levels deep, and with a small <c:forEach> over a List or over a Map.
| No JSTL | One <c:if> | <c:forEach>over a List | <c:forEach>over a Map | |
|---|---|---|---|---|
| Mojarra 4.1.21 | 1 µs, 2.09 ms | 4 µs, 2.04 ms | 4 µs, 2.00 ms | 552 µs, 2.58 ms |
Mojarra 4.1.21 withrestoreBuildTimeDecisions | 1 µs, 2.05 ms | 7 µs, 1.99 ms | 4 µs, 2.00 ms | 544 µs, 2.57 ms |
| Mojarra 4.1.13 | 1 µs, 2.30 ms | 605 µs, 2.98 ms | 608 µs, 2.97 ms | 595 µs, 2.86 ms |
| MyFaces 4.1.5 | <1 µs, 2.22 ms | 421 µs, 2.57 ms | 424 µs, 2.62 ms | 416 µs, 2.47 ms |
| Each cell shows the second build, the one right before rendering, and then the whole postback. | ||||
For comparison: the first build, the one which restores the view, took between 640 and 760 µs on all pages and in all implementations. So the second build used to cost almost as much as the first one, and in Mojarra it is now practically free as long as nothing changed. That shows in the whole postback too: with one <c:if> it went from 2.98 ms in Mojarra 4.1.13 to 2.04 ms in Mojarra 4.1.21. Setting com.sun.faces.restoreBuildTimeDecisions to true costs a few µs more on the second build, which barely shows in the whole postback.
A <c:forEach> over a Map or another collection without a positional index, such as a Set, still forces the full second build, and so does a <c:catch>, whose outcome is only knowable by running its body. When you iterate over a map at build time, iterate over a List copy of its entrySet() instead. The same page did its second build in 3 µs that way, against 552 µs over the Map itself.
Dynamic components
Components which you create in Java and add to the tree yourself are subject to the same rules, and here the moment matters even more. There are two ways to do it.
The first way is to add them while the view is being built, from a postAddToView event listener on the parent.
<h:panelGroup id="target" layout="block">
<f:event type="postAddToView" listener="#{bean.addComponents}" />
</h:panelGroup>
public void addComponents(ComponentSystemEvent event) {
UIComponent target = event.getComponent();
for (int i = 0; i < 100; i++) {
HtmlOutputText text = new HtmlOutputText();
text.setId("text" + i);
text.setValue("Dynamic " + i);
target.getChildren().add(text);
}
}
The listener runs during every build, so every build produces the same components again, just like it does for the components in the Facelets file. Nothing about them needs to be saved in the view state.
Let such a listener method take the event, as above, or else write its expression with parentheses. The listener attribute of <f:event> and <f:ajax> may point to a method taking the event or to a method taking no arguments. Both implementations try the method taking the event first. When it does not exist, Mojarra tries the method without arguments next, and so does MyFaces for <f:event>. So a method without arguments costs a thrown exception and a second invocation on every call. On a page with 100 postAddToView listeners, each call took about 10 µs in both implementations with a method without arguments, against about 1 µs with a method taking the event. With parentheses, as in listener="#{bean.addComponents()}", the expression provides its own arguments, so the method without arguments is found at the first try. That also took about 1 µs per call. For <f:ajax>, MyFaces does not try the method without arguments at all: listener="#{bean.method}" then fails with a MethodNotFoundException, and listener="#{bean.method()}" works.
The second way is to add them after the view was built, for example from an action method which looks up the parent with findComponent() and calls getChildren().add() on it.
<h:form id="form">
<h:panelGroup id="target" layout="block" />
<h:commandButton id="add" value="Add" action="#{bean.addComponents}" />
</h:form>
public void addComponents() {
UIComponent target = FacesContext.getCurrentInstance().getViewRoot().findComponent("form:target");
if (target.getChildCount() == 0) {
for (int i = 0; i < 100; i++) {
HtmlOutputText text = new HtmlOutputText();
text.setId("text" + i);
text.setValue("Dynamic " + i);
target.getChildren().add(text);
}
}
}
A binding to a bean property which manipulates its children is the most common unintended way to end up here. It is the same thing, only the parent is obtained via the bean property instead of via findComponent().
<h:panelGroup id="target" layout="block" binding="#{bean.target}" />
private UIComponent target;
public void addComponents() {
if (target.getChildCount() == 0) {
for (int i = 0; i < 100; i++) {
HtmlOutputText text = new HtmlOutputText();
text.setId("text" + i);
text.setValue("Dynamic " + i);
target.getChildren().add(text);
}
}
}
// Getter and setter of target.
There are hardly any valid real-world use cases for binding a component to a bean property. When it is done anyway, the bean behind such a binding must be @RequestScoped. The specification says that the system cannot behave correctly when it is in any wider scope: a component belongs to the view of a single request, and runs in a single thread.
A component also refers to its parent and its children, so a @ViewScoped or @SessionScoped bean which holds one, holds the entire component tree of that request as well, for as long as the bean lives. The next request then builds its view with a component of the previous one. Since Mojarra 4.0.19 and 4.1.10, the Development stage logs a JSF1104 warning when a component binding resolves to a component which survived across requests (#5790).
The Facelets file knows nothing about such components, so the next build does not produce them. The implementation therefore records the manipulation in the view state, including the complete state of each added component, and replays it on every following postback.
Replaying those manipulations is where Mojarra had most of its partial state saving bugs. The ones fixed recently are listed in "Improving the view state of Mojarra" at the end.
The table below shows both with 100 components of <h:outputText>, against the same page without any dynamic components. Each number is again the middle one of three runs, now with the median of 1000 postbacks per run.
| 100 dynamic components | Mojarra 4.1.21 | MyFaces 4.1.5 | ||||
|---|---|---|---|---|---|---|
| server | client | view state | server | client | view state | |
| none | 66 µs, 0.72 ms | 82 µs, 0.71 ms | 12 bytes | 72 µs, 0.81 ms | 96 µs, 0.83 ms | 63 bytes |
| added while building | 118 µs, 0.85 ms | 129 µs, 0.85 ms | 12 bytes | 119 µs, 0.91 ms | 150 µs, 0.97 ms | 64 bytes |
| added after building | 237 µs, 1.08 ms | 467 µs, 1.92 ms | 15076 bytes | 201 µs, 1.05 ms | 581 µs, 2.14 ms | 21573 bytes |
| Each server and client cell shows the first build, the one which restores the view, and then the whole postback. | ||||||
Adding them while building costs only the build of those 100 components, and the view state stays the same. Adding them after building makes everything slower, and in Mojarra with client side state saving the whole postback becomes more than two and a half times slower. The view state grows to about 15 KB in Mojarra and 22 KB in MyFaces, roughly 150 and 215 bytes per component. With server side state saving that is kept in the HTTP session, per view, and with client side state saving it travels in the hidden field, back and forth on every postback. Setting com.sun.faces.restoreBuildTimeDecisions to true made no measurable difference to Mojarra, neither in this table nor in the next one.
In short: do not add the components after building. It gives nothing in return for the extra CPU and memory it costs on every postback, and with client side state saving also for the extra bandwidth. Better yet, declare the components in the Facelets file. A plain rendered attribute or a JSTL tag like <c:forEach> or <c:if> can do the job in most cases. Only when the components really have to be decided in Java, use a postAddToView listener.
Compressing the client side view state
With client side state saving, the hidden field differs between Mojarra and MyFaces largely by compression. Mojarra compresses the client side view state with gzip by default, MyFaces does not. With the 100 components added after building above, the hidden field took 3328 characters in Mojarra and 28824 in MyFaces. That makes the hidden field of Mojarra much smaller, at the cost of compressing it on every response and decompressing it on every postback. With server side state saving, the hidden field holds only a key, so there is nothing to compress there. You can turn that off.
<context-param>
<param-name>com.sun.faces.compressViewState</param-name>
<param-value>false</param-value>
</context-param>
With the 100 components added after building, the whole postback took 2.03 ms against 2.01 ms with compression, measured as the middle one of three runs for both, which is within the variation between the runs. But the hidden field grew from 3328 to 20184 characters, which every request then has to send back and forth. So turning it off gains nothing measurable.
The other way round, MyFaces can compress its client side view state as well.
<context-param>
<param-name>org.apache.myfaces.COMPRESS_STATE_IN_CLIENT</param-name>
<param-value>true</param-value>
</context-param>
With the 100 components added after building, its hidden field then shrank from 28824 to 4056 characters, while the whole postback went from 2.26 ms to 2.71 ms, measured in one run for both. That brings MyFaces within about a fifth of the 3328 characters of Mojarra. So most of the difference in the hidden field is compression, and the rest is the heavier state of MyFaces, which the view state sizes in the table above already showed.
A gzip filter on the response, such as the one of the server or the CompressedResponseFilter of OmniFaces, does not replace it. Mojarra encrypts the client side view state by default, and it compresses before it encrypts. Encrypted data does not compress, so gzip only wins back the overhead of the Base64 encoding. The uncompressed hidden field of 20184 characters still took about 15 KB after gzip, three quarters of its length, and the whole response about 16 KB, against about 3.4 KB with the compression of Mojarra.
Moreover, gzip compresses only the response. The browser sends the form data of a postback uncompressed, so the hidden field goes back in full on every postback. A gzip filter is still useful for the rest of the page, but keep the compression of the view state on.
Server versus client side state saving
Server side state saving is the default. Client side state saving moves the view state from the HTTP session into the page. That buys two things, and costs a few others.
The first thing it buys: a postback survives the end of the HTTP session. With server side state saving, a postback after the session expired failed with a ViewExpiredException in both implementations. With client side state saving, both accepted it, as everything needed to restore the view is in the request. The @ViewScoped bean however lives in the HTTP session, not in the view state. The postback after session expiry silently got a new @ViewScoped bean, in both implementations. Whatever the old bean held, is gone without any message. So this only helps pages which do not depend on a view or session scoped bean.
The second thing: there is no limit on the number of open views. With server side state saving, each HTTP session keeps only the most recent views. When the same page was opened in 15 other tabs, a postback in the first tab failed with a ViewExpiredException in Mojarra. In MyFaces that happened after 20 other tabs. You can raise those limits via com.sun.faces.numberOfLogicalViews in Mojarra and org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION in MyFaces, at the cost of more memory per session. With client side state saving, the postback in the first tab was always accepted. Mojarra however keeps the @ViewScoped beans of at most 25 views per session, set by com.sun.faces.numberOfActiveViewMaps. After 25 other tabs, the first tab got a new bean, again without any message.
The costs. The view state travels in the hidden field on every postback and every ajax request, in both directions. It is serialized and encrypted on every response, and decrypted and deserialized on every postback. "Dynamic components" and "Compressing the client side view state" above show how large it can get.
It also needs a fixed key. Both implementations generate a random key on startup by default. A postback with a view state from before a restart failed with a ViewExpiredException in both. The same happens in a cluster, when the postback lands on another server than the one which rendered the page. Mojarra takes a fixed AES key from JNDI, MyFaces takes one for the encryption and one for the signature from context parameters.
<!-- Mojarra -->
<env-entry>
<env-entry-name>faces/ClientSideSecretKey</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>Base64 of 16 random bytes</env-entry-value>
</env-entry>
<!-- MyFaces -->
<context-param>
<param-name>org.apache.myfaces.SECRET</param-name>
<param-value>Base64 of 16 random bytes</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.MAC_SECRET</param-name>
<param-value>Base64 of 32 random bytes</param-value>
</context-param>
With those, the postback after a restart was accepted in both, once again with a new @ViewScoped bean. You can generate the values with openssl rand -base64 16 and openssl rand -base64 32. As web.xml usually lives in version control, you can also define them in the server configuration instead, in Tomcat for example as <Environment> and <Parameter> in context.xml.
The most common reason to choose client side state saving is a login or contact form which should not fail after the session expired. For that, a stateless view is simpler, as shown in "The jakarta.faces.ViewState hidden field" above. With <f:view transient="true">, the postback without session was accepted in both implementations, with both state saving methods. There is nothing to expire, nothing to encrypt and nothing to send back and forth.
The lack of a limit on open views makes client side state saving a good fit for a site which invites being opened in a lot of tabs and windows, such as a discussion forum or a Q&A site. That requires backing beans which are all @RequestScoped, so that the HTTP session holds nothing which session expiry or the limit on view scoped beans could take away. In Mojarra, also set com.sun.faces.restoreBuildTimeDecisions to true. A request scoped bean loads its data again on the postback, and when a new post arrived in between, a JSTL tag can decide differently than on the submitted page. A stateless view does not help there, as without a view state there are no saved decisions.
So in general, keep server side state saving, and make the pages which must survive session expiry stateless. Choose client side state saving when the view state is small, the backing beans are all request scoped, and every server has the same fixed key.
Composite components
A composite component is also built during every build, and its costs are worth knowing as well. A composite is not just a piece of markup which is pasted in place. It becomes a real component in the tree, a UINamingContainer with its own attribute map, its own Facelets file with the <cc:interface> and the <cc:implementation>, and an EL context in which #{cc} resolves. A tag file, on the other hand, is pasted in place during the build, and contributes no component of its own. The table below compares 100 cells written out as plain components, as a tag file and as a composite component, each producing the same <h:panelGroup> with an <h:outputText>.
| 100 cells | Mojarra 4.1.21 | MyFaces 4.1.5 | ||
|---|---|---|---|---|
| server | client | server | client | |
| plain components | 205 µs, 0.94 ms | 224 µs, 0.93 ms | 212 µs, 1.03 ms | 233 µs, 1.05 ms |
| tag file | 300 µs, 1.06 ms | 303 µs, 1.04 ms | 371 µs, 1.23 ms | 394 µs, 1.26 ms |
| composite component | 540 µs, 1.53 ms | 540 µs, 1.51 ms | 634 µs, 1.70 ms | 643 µs, 1.73 ms |
| Each cell shows the first build, the one which restores the view, and then the whole postback. | ||||
The view state was the same in all three, so composites do not cost view state as long as nothing changes their state after the build. When something does change, a composite saves the same as a tag file. This shows with a tag file and a composite component which both hold one required input, submitted empty.
<!-- /WEB-INF/tags/field.xhtml -->
<ui:composition>
<h:inputText id="#{id}" value="#{value}" required="true" />
</ui:composition>
<!-- /resources/components/field.xhtml -->
<ui:component>
<cc:interface>
<cc:attribute name="value" />
</cc:interface>
<cc:implementation>
<h:inputText id="input" value="#{cc.attrs.value}" required="true" />
</cc:implementation>
</ui:component>
<h:form id="form">
<tagfile:field id="second" value="#{bean.second}" />
<h:commandButton id="submit" value="Submit" />
</h:form>
<h:form id="form">
<composite:field id="second" value="#{bean.second}" />
<h:commandButton id="submit" value="Submit" />
</h:form>
Mojarra, tag file (322 bytes)
view ID hash = 1673905523
form:second
valid = false
Mojarra, composite component (328 bytes)
view ID hash = 1673905523
form:second:input
valid = false
MyFaces, tag file (363 bytes)
view ID = "/page.xhtml"
form:second
valid = false
MyFaces, composite component (366 bytes)
view ID = "/page.xhtml"
form:second:input
valid = false
Only the client ID of the input differs. The composite component itself saves nothing, as nothing changed its state after the build. With com.sun.faces.restoreBuildTimeDecisions set to true, Mojarra saves one more entry for the tag file, plus the checksums of the page and of the tag file, and its view state grows from 316 to 527 bytes.
com.sun.faces.BuildTimeDecisions
-118455453_6452920b = "second"
1966526476_id = -298669498
-1683132945_id = -441778525
That is the id="#{id}" of the input in the tag file. In a tag file, an attribute passed by the caller is an EL variable, so the id is an EL expression which the build evaluates, the same as the id of a row of a <c:forEach>. The composite has a literal id in its implementation, and the client ID of the composite takes care of the rest. So a tag file costs no view state, but when it passes an id through, it does cost a build time decision, and with that a checksum of every Facelets file of the page.
Composites do cost build time. With two components per composite, the first build took about two and a half times as long as with plain components. That shrinks as the composite holds more: about twice as long with five components, and about 1.7 times with ten. The whole postback stayed about 1.6 times as slow at all three sizes, where each component in the composite reads an attribute of the composite. And remember that the build happens on every request, and twice on a postback when the second build cannot be skipped. So use a composite component when you actually need what it offers over a tag file: being one component with its own client ID which you can reference in <f:ajax render>, declared attributes in <cc:interface>, or a backing component. For a reusable piece of markup only, a tag file is the cheaper choice.
Summary
For correct behavior and best performance:
- Let EL on the row data decide what differs per row in an
<h:dataTable>or<ui:repeat>. UserowStatePreserved="true"only for component state which differs per row and cannot come from the row data. - Use JSTL to decide which components end up in the tree, and how many. Use the
renderedattribute to decide whether an existing component is rendered. - Use
<c:forEach>when a parent needs physically separate children, such as the columns of an<h:dataTable>. Use<ui:repeat>to render one body once per item. - In Mojarra 4.0.23, 4.1.14 and newer, set
com.sun.faces.restoreBuildTimeDecisionstotrue, so that a JSTL decision which differs on the postback no longer silently drops submitted values. Mojarra 5.0 does this by default, as does MyFaces since version 2.2.0. - Keep the items of a
<c:forEach>in a@ViewScopedbean, or recompute them in the@PostConstructof a@RequestScopedbean. Put inputs on properties of those items only when they are kept in a@ViewScopedbean, and in MyFaces 4.x only with server side state saving. - Let a
<c:forEach>iterate over aList, not over aMapor aSet, so that Mojarra can skip the second build of a postback. - Do not add components after the view was built. Better yet, declare them in the Facelets file: with JSTL tags such as
<c:forEach>and<c:if>, it can build most dynamic views. Only when the components really have to be decided in Java, add them from apostAddToViewlistener. - Let the
listenermethod of<f:event>and<f:ajax>take the event. When the method has no use for the event, write the expression with parentheses, as inlistener="#{bean.method()}". - Do not bind a component to a bean property with
binding. Usebindingonly on a page variable, when a component needs to reference another component in the same view. - Keep server side state saving, and make a page which must survive session expiry stateless with
<f:view transient="true">. With client side state saving, set a fixed key, and keep in mind that a@ViewScopedbean is still lost when the session expires. Choose client side state saving for a site which is opened in many tabs, when its backing beans are all request scoped. - With client side state saving in Mojarra, keep the default view state compression on. A gzip filter on the response does not replace it.
- Prefer a tag file over a composite component, unless you need what only a composite offers.
Improving the view state of Mojarra
Decoding the view state of Mojarra for this article, and asking for every entry why it is there, also turned up quite some issues in Mojarra itself. They have been fixed along the way. All fixes below are in Mojarra 4.0.30 and 4.1.21, unless noted otherwise.
A smaller view state:
- The response encoding is no longer saved in the view state of every view, only when
<f:view encoding>is an EL expression. That is fixed since 4.0.28 and 4.1.19 (#6069). - No empty
com.sun.faces.DynamicActionslist in every view (#6096). - No
attributesThatAreSetlist in the saved state of a changed HTML component attribute (#6097), and also not in the full state of a dynamically added component which is restored from the state of a previous request (#6117). - The position of a component added after the build is saved once instead of three times (#6098).
- No empty maps and no default values, which removed 108 characters from the empty client side view state (#6112).
- No wrapper object per row in the saved row states of
rowStatePreserved="true", the client ID of a dynamically added component written once instead of twice, and no state of<ui:repeat>after a postback in which nothing changed (#6122). - No method expression of an
actionListener,valueChangeListenerorvalidatorattribute in every view state, also not on a GET request. The build creates them again on every request anyway. A form with one of each saved 1435 bytes, and now 12 bytes, the same as an empty view state (#6161). - No unused slot for the tree structure, in Mojarra 5.0 only, as it changes the format of the view state (#6113).
A better performance:
- A compressed view state is read through a buffer. Without it, Java serialization inflated it one byte at a time, so reading a compressed client side view state took about five times as long as reading the same state uncompressed: 959 against 180 µs for the 100 components added after building in "Dynamic components". With the buffer it takes 214 µs. The view state is also compressed at level 3 instead of 6, which makes writing it faster, for a hidden field which is about 3% longer (#6141).
- A composite component is built with less work per composite. The build which restores a page with 100 composite components went from 587 to 517 µs, which is 12% faster (#6146).
<f:event>and<f:ajax>try thelistenermethod taking the event first, asactionListenerandvalueChangeListeneralready did. They used to try the method without arguments first, so a listener method taking the event, such as thepostAddToViewlistener in "Dynamic components", paid a thrown exception on every call. The build which restores that page went from 157 to 116 µs. The same fix makes a listener which is passed through a composite component attribute work with both methods, and avalueChangeListenerwithout arguments on a bean passed into an include (#6157). Which method is invoked when the bean has both is not specified yet (jakartaee/faces#2274).- With
com.sun.faces.restoreBuildTimeDecisions, theitemsof a<c:forEach>are resolved once instead of twice during Restore View, and twice instead of three times right before rendering (#6148).
A correct restore:
- A view state is now valid only for the view it was saved for. Posted to another view, it used to be restored into that view, including its dynamically added components. It now expires the view, as it already did in MyFaces (#6147).
- Unencrypted client side state, as used for the dumps in this article, could be written but not read back. That is fixed since 4.0.28 and 4.1.19 (#6067). Client side state is no longer decoded twice on every postback, also since 4.0.28 and 4.1.19 (#6068).
- A
bindingcould fail with a duplicate component ID while saving the state. That is fixed since 4.0.26 and 4.1.17 (#6001). - Since Mojarra 4.0.19 and 4.1.10, a dynamic component which was removed and added again with the same ID was dropped on the next postback (#6060), and saving the state of dynamically built components could fail with a duplicate component ID (#6075). Both are fixed since 4.0.28 and 4.1.19.
- A dynamically added child stays at its index when a sibling before it is added or removed in a later request (#6100, #6102).
- A panel created by the view and replaced by a dynamically added panel whose child has the same ID, lost that child under full state saving (#6101).
- A table nested in another one no longer keeps the per-row values of its inputs on a re-render without errors (#6110).
- An attribute removed after
clearInitialState()no longer comes back after a postback (#6118). - With
com.sun.faces.restoreBuildTimeDecisions, a<c:forEach>saved its line and column to recognize its decision, so another<c:forEach>which landed at the same place after an edit of the Facelets file got its decision. A checksum per Facelets file now stops the replay at the edited file (#6119). - A
ListorMapwhich was put whole into the state helper, such as avalueof a table set from Java code, is restored as it was, instead of being merged into the current one (#6125). - With
rowStatePreserved="true", a row without saved state of its own kept the local value, the validators and the listeners of the previous row (#6128, #6133). - Two restores of one saved state, for example after the back button or in a second tab, shared the per-row values of a table with server side state saving. The fix also removes the stale saved row state of
rowStatePreserved="true"shown in "When to use rowStatePreserved" (#6129). - A dynamic remove of a component which the next build does not create was dropped from the view state, so when that component was built again later, the result depended on whether a postback came in between (#6130).
- A grouped
<h:selectOneRadio>in a nested<ui:repeat>lost the selection of all groups but the last, because validating a group visited the whole form and so overwrote the saved state of the rows (#6131). - More generally, a tree visit or
invokeOnComponent()from inside a row of a nested<ui:repeat>or<h:dataTable>, for example from a validator which checks the other inputs of the form, overwrote the saved state of other rows (#6138). - In Development stage only, the warning about components which the build that restores the view produced differently (#5964) also covers the components which a
postAddToViewlistener builds. Before, a listener which decided differently on that build dropped submitted values without any warning (#6144). - Since Mojarra 4.0.19 and 4.1.10, a postback could render the children of a
<c:forEach>before the components and the text which precede it in the same parent. That happened when something else in the view, such as a<c:forEach>over aMap, forced the full second build. The unchanged<c:forEach>kept its children from the first build, at their old place (#6166).
How Claude Code helped
The research for this article was done together with Claude Code. It wrote the tool which decodes the view state into the trees shown above, and the test pages and scripts which measured both implementations over plain HTTP. It also read the sources of Mojarra and MyFaces to explain every entry. None of those explanations went into this article on its word alone. Every behavior above was run, not only read from the source. That matters, because AI models have their own ideas about what the view state contains, as said at the beginning.
Most of the fixes listed in the previous section did not come from decoding the view state. They came from the unit tests around each fix. For every fix, Claude Code was asked to probe the edge cases next to it, also the ones which the fix was not about. Those tests kept turning up older bugs, some of them hidden for years.
- A round trip test of the smaller row state of
rowStatePreserved="true"showed the value of row 0 in row 1. That happened also without the fix under test, and turned out to be a bug in restoring a full state (#6128). The review of that fix found the same bug for the validators and the listeners (#6133). MyFaces had the bug of the local value too, and fixed it in 5.0 (MYFACES-4777). - The review of the smaller row state noticed that the per-row values of a table were restored by reference, so two restores of one saved state shared them (#6129). Its fix also removed the stale saved row state of
rowStatePreserved="true", a bug from 2010. MyFaces had both bugs too, and fixed them in 4.1.5 (MYFACES-4778). - Restoring the full state of a dynamically added component onto the same component built again (#6117) showed that an attribute which was removed after
clearInitialState()came back after a postback (#6118). - Saving the position of a dynamically added component only once (#6098) turned up three more bugs in the same code (#6100, #6101, #6102).
Claude Code also went through the view state issues of Mojarra which had been closed as stale without a reply. Two of them, reported in 2019, still reproduced on a real page, and are now fixed: a dynamic remove which got lost (#6130, reported as #4549), and a radio button group in a nested <ui:repeat>, caused by code from 2016 (#6131, reported as #4648).
One issue came from a measurement. In the "added while building" row of "Dynamic components", the build which restores the view took about 35 µs longer in Mojarra than in MyFaces. Claude Code timed the postAddToView listener on its own, and found that Mojarra tried the method without arguments first, so it threw an exception on every call. Probing the other listener attributes next to it then showed that a listener which is passed through a composite component attribute failed with a PropertyNotFoundException when the method tried first did not exist, and that a valueChangeListener without arguments failed on a bean passed into an include (#6157). Which method a listener attribute invokes when the bean has both is not specified yet. That is now jakartaee/faces#2274.
