JSF is great at hiding plumbing, but it does not hide the bill. The first time you watch a heap chart on a busy JSF app, you can almost hear the memory pressure. Tabs pile up, user flows bounce between forms, and your poor session grows like a backpack filled with bowling balls. I have been moving a couple of apps to Facelets and JSF 2 and the conversation that never ends is the same one. Where is state kept and who pays for it? Today I want to break that down in simple terms, with stuff you can copy paste into your project and a few tricks that help you keep response times steady when traffic gets real.
How JSF keeps state and why it matters
JSF builds a component tree for every view. That tree is the memory of the page. Input values, component properties, validation messages, the works. After rendering, JSF needs that state back on the next postback. There are two big modes for this thing. Server state and client state.
With server state the view tree lives in the HttpSession. The browser gets a small token and the server remembers the rest. This keeps the page light, but your session grows for every open view the user touches. If a user opens five tabs with five different forms, that is five trees in memory. If you are on a small VM, that adds up. If you are in a cluster, it also means stickiness or session replication has to carry that weight.
With client state the component tree gets encoded and posted back as a hidden field. Your session can stay slim, but each page gets heavier and every request decodes and rehydrates the tree. Your bandwidth pays and your CPU does a little more work per request. The trade is simple. Memory on the server versus bytes over the wire.
JSF 2 added partial state saving which cuts down the amount of state by storing only deltas. You should keep it on unless you know exactly why you need it off. Here is the quick tour through config knobs you can set in web.xml.
<!-- Server versus client state -->
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value> <!-- or client -->
</context-param>
<!-- Partial state saving in JSF 2 -->
<context-param>
<param-name>javax.faces.PARTIAL_STATE_SAVING</param-name>
<param-value>true</param-value>
</context-param>
<!-- Debug serialization to catch non serializable stuff in session mode -->
<context-param>
<param-name>javax.faces.SERIALIZE_SERVER_STATE</param-name>
<param-value>true</param-value>
</context-param>
<!-- Mojarra tuning: how many views to keep per session -->
<context-param>
<param-name>com.sun.faces.NUMBER_OF_VIEWS_IN_SESSION</param-name>
<param-value>15</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.NUMBER_OF_LOGICAL_VIEWS</param-name>
<param-value>15</param-value>
</context-param>
<!-- Mojarra: compress client state when using client mode -->
<context-param>
<param-name>com.sun.faces.compressViewState</param-name>
<param-value>true</param-value>
</context-param>
<!-- MyFaces cousins -->
<context-param>
<param-name>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION</param-name>
<param-value>15</param-value>
</context-param>There is also stateless views. For pages that are read only or almost read only you can tell JSF to skip state saving for that view. That takes the weight off both sides.
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core">
<f:view transient="true">
<h:outputText value="Hello, this page does not save view state."/>
</f:view>
</ui:composition>Use that on dashboards, list pages, reports and any view where you are not showing a form that keeps long lived input. It cuts a surprising amount of churn.
Session scope, view scope and the weight of your backpack
The other side of the story is managed bean scopes. In JSF 2 we have @RequestScoped, @ViewScoped, @SessionScoped and @ApplicationScoped. CDI also brings @ConversationScoped if you are on EE 6 with Weld. Pick the wrong scope and you turn a tiny object into a ball and chain.
Quick guide with a performance hat on. Request is cheap and safe. View is perfect for wizards and long forms. Session is comfy but dangerous when you toss full lists, DTO graphs and caches in there. Application is global so keep it light and thread safe.
When you do use session, keep beans Serializable and mark heavy fields as transient. Do not store an EntityManager. Do not store a big ResultSet. Reload what you need on demand. And watch for UI component references inside beans. They will drag the entire tree into the session if you bind them directly.
@ManagedBean
@ViewScoped
public class OrderWizardBean implements Serializable {
private static final long serialVersionUID = 1L;
private Long orderId;
private List<Item> items;
// This field is heavy and does not need to live in session
private transient SomePdfRenderer renderer;
@PostConstruct
public void init() {
renderer = new SomePdfRenderer();
}
public void addItem(Item i) {
items.add(i);
}
public StreamedContent downloadPreview() {
return renderer.render(items);
}
}Two small tricks here. Transient fields shave memory and make serialization safer. And @ViewScoped means the bean lives as long as the user stays on that view. When the user leaves, the bean goes away and the backpack gets lighter.
If you are doing Post Redirect Get, look at the new flash scope in JSF 2 for short lived messages and small bits of state that survive a redirect.
<h:commandButton value="Save" action="#{bean.save}">
<f:setPropertyActionListener value="Saved!" target="#{flash.successMessage}"/>
</h:commandButton>
<h:panelGroup rendered="#{not empty flash.successMessage}">
<h:outputText value="#{flash.successMessage}"/>
</h:panelGroup>Big trees, big tables and the cost of rendering
Even with perfect state choices you can grind the server with a huge component tree. The main culprits are large tables rendered all at once, verbose composite components, and forms with dozens of inputs that sit in the session across many tabs.
Some very practical advice. Paginate server side. Do not bind a list of ten thousand rows into an h:dataTable and hope for the best. Feed it a window. Let the backend load slices. JSF will keep the state of the components in the tree, so if the table has one thousand child input components, that is one thousand bits of state on each view. You can also render read only tables and switch to an edit dialog that only spins up inputs for the chosen row.
Ajax in JSF 2 is sweet and saves bandwidth when you update small fragments, but the server still has to rebuild the tree, restore state and run the lifecycle. Keep Ajax regions small. Use execute and render to limit the work.
<h:form id="orderForm">
<h:inputText id="qty" value="#{bean.qty}"/>
<h:commandButton value="Recalc">
<f:ajax execute="qty" render="total"/>
</h:commandButton>
<h:outputText id="total" value="#{bean.total}"/>
</h:form>One more lever that gets ignored. Component binding. It looks handy to keep a reference to a component in a bean, but that reference holds the entire tree. Use binding only for special cases and never in a session bean. Your heap will thank you.
Server state or client state. Pick your poison with eyes open
I keep getting the same question on teams. Which mode should we use for state saving. The honest answer is that both are fine if you match them to the project shape. Here is how I choose in practice.
- Choose server state when your pages are light, you can keep an eye on session size, you have sticky sessions, and you want smaller page payloads for older browsers or slow links.
- Choose client state when your users open many tabs, your cluster does not like big sessions, or you want easy failover without replication. Use compression and partial state saving and watch page size.
- Mix it. You can keep client state for public pages and flip to server state for intranet apps that live behind a fat link. Or go stateless on read only pages and keep state only where forms live.
One warning for both paths. When you see ViewExpiredException in your logs, it is almost always a session timeout or the server dropping the view because you hit the cap on number of views per session. Raise the cap a bit or teach the app to refresh gracefully.
Practical checklist you can run this week
- Measure session size. Put a quick filter in the app that logs the byte size of the session once per request in test. This uses plain serialization as an estimate.
public class SessionSizeFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest r = (HttpServletRequest) req;
HttpSession s = r.getSession(false);
chain.doFilter(req, res);
if (s != null) {
int size = estimateSize(s);
Logger.getLogger(getClass().getName())
.info("Session bytes ~ " + size);
}
}
private int estimateSize(HttpSession s) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
Enumeration<String> names = s.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
Object val = s.getAttribute(name);
if (val instanceof Serializable) {
oos.writeObject(val);
}
}
oos.close();
return bos.toByteArray().length;
} catch (IOException e) {
return -1;
}
}
}- Pick a state saving mode per module. Do not let it be an accident. Document it in the codebase with the web.xml snippet and a short note.
- Turn on partial state saving unless a library breaks with it.
- Cap the number of views per session. Start with 15 and watch the logs. Raise only if you see many valid flows getting expired.
- Scan beans for scope bloat. Hunt for @SessionScoped beans that hold lists, full DTO graphs, or service references. Move to @ViewScoped when the flow is bound to a view. Keep fields transient if they are heavy and can be recreated.
- Remove component bindings from session beans. If you really need one, keep it in a view bean.
- Mark read only views as transient with f:view transient equals true. Your heap will drop and page to page hops will feel snappier.
- Paginate tables and avoid rendering huge trees. If you need to edit in bulk, split the view into smaller regions and use Ajax with tight execute and render targets.
- Test tab storms. Open ten tabs with the same form and click around like a real user. Watch heap with VisualVM or jconsole. This catches view cap issues early.
- Watch page size if you choose client state. Look at the hidden field named
javax.faces.ViewState. If it balloons, find the component that dumps a lot of state and trim it. - Run with serialization on in dev for server state. The param is
javax.faces.SERIALIZE_SERVER_STATE. It flushes out non serializable fields before prod does it for you at 2 am. - Cache smart. If you cache reference data, do it in an application scoped bean and keep values lean. Never drop a full Hibernate session or EntityManager in a bean scope.
- Plan for timeouts. Give users a friendly way back when a view expires. Detect it and send them to a safe page with the flash scope carrying a message.
For folks on Tomcat or GlassFish, tooling is right there. VisualVM ships with the JDK and gives you a straight look at heap, threads and profiles. JConsole can show memory trends. If you need a quick heap dump, jmap works. Small time spent measuring beats guessing every time.
Also, do not forget the boring wins. Turn off Facelets comments in prod. Less bytes is less work.
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>And keep templates tight. Composite components are lovely. Just do not nest five levels deep if all you needed was a tag file and a tiny bit of EL. Every layer adds components and state.
If you are rolling out PrimeFaces or RichFaces goodies, remember that widgets often keep extra state on both sides. Try the plain JSF components first for heavy traffic flows, then layer in the fancy bits where they bring real value. Your users care about speed more than rounded corners.
One last bit on clusters. Server state leans on sticky sessions. If your load balancer hops requests between nodes, either pin them to one node or go with client state and keep page size under control. If you must replicate sessions, set serializable flags, keep the number of views small, and test failover with a tab storm test while you bounce one node.
That is the true trick with JSF. It gives you high level building blocks, but the cost model is still very real. Memory, bandwidth and CPU are your trade coins. The more honest you are about where the app spends those coins, the fewer late night fire drills you will have.
Short version. Keep state where it hurts the least, scope beans to the smallest basket that works, and cut the tree when it gets too big.
Build pages that respect the user and the server and you will be fine, even when traffic spikes because someone from product just dropped a link on the company homepage.
Coda. Keep state honest and your app will keep its pace.