How to reduce a large internal view state / what is in the internal view state?
I needed to reproduce this in a smaller more focused example to prove my hunch. Then further down you'll see how I've approached a solution that hopefully doesnt impact your desire to structure the data as you currently have it. Yet does more work in the viewstate in order to simplify the VF markup.
Research. It looks like it is the use of nested apex:repeat and apex:outputPanel's, they internally appear to use quite a lot of state. In the reproduction below, I found around half of my view state allocated to internal. And I didn't even take my example further by using apex:outputField (which I also suspect to be quite heavy). So with this in mind first, my reproduction controller and page are shown below. Followed by one possible way I considered to reduce the VF components on the page a little more but get the same output.
public with sharing class ViewStateTestController {
public List<Row> rows {get;set;}
private static final Integer scaler = 5;
public class Row
{
public Boolean isGrouping {get;set;}
public Boolean selected {get;set;}
public String name {get;set;}
public String data1 {get;set;}
public String data2 {get;set;}
public String data3 {get;set;}
public List<Row> children {get;set;}
}
public ViewStateTestController()
{
rows = new List<Row>();
for(Integer group1Idx=0; group1Idx<scaler; group1Idx++)
{
// Add group for level 2 data
Row group1Row = new Row();
group1Row.isGrouping = true;
group1Row.Name = 'Level 1 Group';
group1Row.children = new List<Row>();
rows.add(group1Row);
for(Integer group2Idx=0; group2Idx<scaler; group2Idx++)
{
// Add some level 2 data
for(Integer group2DataIdx=0; group2DataIdx<group2Idx+1; group2DataIdx++)
{
Row group2DataRow = new Row();
group2DataRow.isGrouping = false;
group2DataRow.Name = 'Level 2 Data';
group2DataRow.data1 = 'Some Level 2 Data 1';
group2DataRow.data2 = 'Some Level 2 Data 2';
group2DataRow.data3 = 'Some Level 2 Data 3';
group1Row.children.add(group2DataRow);
}
// Add group for level 3 data
Row group2Row = new Row();
group2Row.isGrouping = true;
group2Row.Name = 'Level 2 Group';
group2Row.children = new List<Row>();
group1Row.children.add(group2Row);
for(Integer group3Idx=0; group3Idx<scaler; group3Idx++)
{
// Add some level 3 data
for(Integer group3DataIdx=0; group3DataIdx<group3Idx+1; group3DataIdx++)
{
Row group3DataRow = new Row();
group3DataRow.isGrouping = false;
group3DataRow.Name = 'Level 3 Data';
group3DataRow.data1 = 'Some Level 3 Data 1';
group3DataRow.data2 = 'Some Level 3 Data 2';
group3DataRow.data3 = 'Some Level 3 Data 3';
group2Row.children.add(group3DataRow);
}
}
}
}
}
}
And the VF page...
<apex:page controller="ViewStateTestController" sidebar="false">
<style type="text/css">
table { border-collapse:collapse; }
table.myTable td, table.myTable th { border:1px solid black;padding:5px; }
table.myTable td.padding { border:0px; }
</style>
<apex:form >
<table class="myTable">
<tr>
<td><b>Select</b></td>
<td colspan="3"><b>Name</b></td>
<td><b>Data 1</b></td>
<td><b>Data 2</b></td>
<td><b>Data 3</b></td>
</tr>
<apex:repeat value="{!rows}" var="rowlevel1">
<tr>
<apex:outputPanel rendered="{!rowLevel1.isGrouping}">
<apex:outputPanel layout="none">
<td><input type="checkbox"/></td>
</apex:outputPanel>
<td colspan="6">{!rowlevel1.Name} ({!rowlevel1.children.size})</td>
</apex:outputPanel>
</tr>
<apex:repeat value="{!rowlevel1.children}" var="rowlevel2">
<tr>
<apex:outputPanel rendered="{!rowLevel2.isGrouping}">
<apex:outputPanel layout="none">
<td><input type="checkbox"/></td>
</apex:outputPanel>
<td class="padding"> </td>
<td colspan="5">{!rowlevel2.Name} ({!rowlevel2.children.size})</td>
</apex:outputPanel>
<apex:outputPanel rendered="{!NOT(rowlevel2.isGrouping)}">
<td><apex:inputCheckbox value="{!rowlevel2.selected}"/></td>
<td class="padding"> </td>
<td colspan="2">{!rowlevel2.name}</td>
<td>{!rowlevel2.data1}</td>
<td>{!rowlevel2.data2}</td>
<td>{!rowlevel2.data3}</td>
</apex:outputPanel>
</tr>
<apex:repeat value="{!rowlevel2.children}" var="rowlevel3">
<tr>
<td><apex:inputCheckbox value="{!rowlevel3.selected}"/></td>
<td class="padding"> </td>
<td class="padding"> </td>
<td colspan="1">{!rowLevel3.name}</td>
<td>{!rowlevel3.data1}</td>
<td>{!rowlevel3.data2}</td>
<td>{!rowlevel3.data3}</td>
</tr>
</apex:repeat>
</apex:repeat>
</apex:repeat>
</table>
</apex:form>
</apex:page>
Results in this...
Proposed Solution. So my solution revolves around flattening the nested lists into one and providing the bindings to control the layout in a single apex:repeat with simplified markup within. The pattern should allow you to retain your existing structure just be sure to produce this view of the data before rerendering the page. Of course if you want to adapt your internal viewstate structure this would make things easier.
I extended the above controller to add this...
public List<ViewRow> viewRows {get;set;}
public class ViewRow
{
public Row row {get;set;}
public String name {get;set;}
public Integer level {get;set;}
public Integer colspan {get;set;}
}
private void makeViewRows(List<Row> rows, Integer level)
{
for(Row row : rows)
{
ViewRow viewRow = new ViewRow();
viewRow.row = row;
viewRow.name = row.isGrouping ?
String.format('{0} ({1})', new String[] { row.Name, String.valueOf(row.children.size()) }) : row.Name;
viewRow.level = level;
viewRow.colSpan = row.isGrouping ?
(level == 1 ? 6 : 5) :
(level == 2 ? 2 : 1);
viewRows.add(viewRow);
if(row.isGrouping)
makeViewRows(row.children, level + 1);
}
}
Then add these lines to the bottom of the constructor to produce the new list.
// Produce a flat list to binding to
viewRows = new List<ViewRow>();
makeViewRows(rows, 1);
My Visualforce page now looks like this.
<apex:page controller="ViewStateTestController" sidebar="false">
<style type="text/css">
table { border-collapse:collapse; }
table.myTable td, table.myTable th { border:1px solid black;padding:5px; }
table.myTable td.padding { border:0px; }
</style>
<apex:form >
<table class="myTable">
<tr>
<td><b>Select</b></td>
<td colspan="3"><b>Name</b></td>
<td><b>Data 1</b></td>
<td><b>Data 2</b></td>
<td><b>Data 3</b></td>
</tr>
<apex:repeat value="{!viewrows}" var="viewrow">
<tr>
<td><apex:inputCheckbox value="{!viewrow.row.selected}"/></td>
<td style="display:{!IF(viewrow.level>=2, 'table-cell', 'none')}" class="padding"> </td>
<td style="display:{!IF(viewrow.level>=3, 'table-cell', 'none')}" class="padding"> </td>
<td colspan="{!viewrow.colspan}">{!viewrow.name}</td>
<td style="display:{!IF(viewrow.row.isGrouping, 'none', 'table-cell')}">{!viewrow.row.data1}</td>
<td style="display:{!IF(viewrow.row.isGrouping, 'none', 'table-cell')}">{!viewrow.row.data2}</td>
<td style="display:{!IF(viewrow.row.isGrouping, 'none', 'table-cell')}">{!viewrow.row.data3}</td>
</tr>
</apex:repeat>
</table>
</apex:form>
</apex:page>
Results in the following reduction on internal viewstate from 5.22kb (48%) to 2.85kb (34%).
With the apex:inputCheckbox component commented out, it reduces to 1.27kb (20%).
Summary: So it seems using the VF components in large tables can have a more noticeable impact on your internal view state. The general feeling I get is that VF is best at form entry, but struggles with lots of VF components (be they read or write) on the page. And large tables certainly stress this a lot more, especially in nested situations like this, where the output needs to vary from row to row. Some suggestions then...
- Try to avoid having nested VF component related repeats / panels / rendered logic in the table in the VF page. In this case it seems to help to predetermine some of the rendering aspects in the view state as I have done in my proposed solution above. BTW I did notice at least one outputPanel that just wrapped a checkbox, so while that might give a small gain, I think that could be safely removed from your current solution.
- Given this example is largely output driven, with mostly a single VF checkbox component per row it might be best to consider dropping VF components all together for the rows. And outputting a standard HTML checkbox. Then using JavaScript Remoting or a parameterised apex:actionFunction to get the selected rows back to the controller?
- Building on the above, for total control you might want to consider building the entire table HTML in your controller Apex logic yourself. This can be output with apex:outputText, setting escape="false". Take a look at this for a brief explanation.
- Consider if you need the apex:form tag, sometimes this is just carried over, copy paste style as general tags needed. But if you are not accepting any input via the traditional VF input components then you don't need it and hence you don't need viewstate.
- Finally of course consider ways to help the users reduce the number of records displayed via a paging solution perhaps.
Update: 22nd Jan, added point relating to use of apex:form tag.
Update: 21st Feb, added further insight as to relative weight of internal viewstate based on field type and use of fieldsets. Thanks Ralph!
"Ran into this in a couple more cases. One interesting conclusion is that the weight of apex:outputField varies by the field type, with lookups not surprisingly added a lot more view state relative to test fields. Also found that looping over field sets as for the columns also generated a lot of extra view state as opposed to hard-coded columns"
Updated: 25th July 2013
This answer from sfdcfox, represents another option (so long as you take into account the advice above as well). Basically he proposes using Dynamic Visualforce. For trees with variable depths this could be an option.
One possibility is that the internal view state is being taken up by the recordset that you have in your controller, i.e. the accounts that are being selected. It's hard to tell without seeing code, but if there are any fields that you are selecting but not using then you can reduce the view state size by not selecting them.
Also it sounds like you might have a lot of accounts being matched by your search procedure. Could you use a "limit" on the SOQL to restrict it to the top 10 or something? You could consider using a standardsetcontroller if you want to work with the full resultset but only have a workable group in view state memory at one time.
Edit - if you can provide a bit more detail on what you are trying to do with the accounts this might help. For example, I have a page where the user identifies records which are to have a complicated operation performed on them. In order to identify the records they only need to see a handful of fields (name etc.) So when I am returning rows for the page I return a list selecting only those fields.
The user indicates which records they want to amend and then in the controller I take the list of IDs and select the 100+ fields that I require from each row in order to do my operation. This would blow the view state if I returned it to the page but I don't need to - the work is all server side.
If you can reduce the information about the accounts passed to the page to the bare minimum required for the user to identify those for action, then you shouldn't have a problem with view state. Hope that helps