128

I'm building an account search page that lets users select accounts and then pass them to other pages to perform various actions on them. Unfortunately users are reporting hitting the view state limit. Which leads me to a broader question: what is in in the internal view state and how can I reduce it?

I've already made everything transient I can, and when I look at the view state inspector my controller only uses a modest 15kb, but the "Internal" component of the view state is upwards of 90kb. Does anyone have any tips for how to reduce the "Internal" component of the view state?

Some additional details. The page is not using any components. The page has only one form on it. The controller does reference a couple other classes. The controller does not store any state that can be regenerated in code on the postback.

EDIT: Uploaded a gist with a thoroughly edited version of the code. The code will likely not compile initially but should give some idea of the design.

EDIT2: Here's a screenshot of what the page looks like from a related post on the UX stackexchange Account Filter Page

EDIT3: Here's a screenshot with a large result set loaded. If I group these results I'll exceed the view state limit. Result set size is 500 records and 1 grouping when the limit is reached. gigantic internal view state

10
  • I think for anything beyond the ideas salesforce.com/us/developer/docs/pages/Content/… you may need to provide a code sample.
    – Mike Chale
    Nov 19, 2012 at 1:07
  • 2
    Yes sample code of your page would be good. In the meantime, the <apex:inputXXXX> components do generate a lot of internal view state, especially if you put them in an <apex:repeat> or table. Nov 19, 2012 at 2:06
  • 6
    Do you have multiple sections on your page which get sent to and from the backend for different reasons? You can now have multiple <apex:form>s which reduces the amount sent back and forth as it can use multiple view states from what I understand. Alternatively, could you leverage javascript remoting instead?
    – Matt Lacey
    Nov 19, 2012 at 6:04
  • Good point by LaceySnr.Javascript remoting will surely resolve view state issue. Nov 19, 2012 at 6:09
  • 3
    WRT <apex:forms/>, I think there's a little confusion. Multiple form tags used to be a problem as that resulted in multiple copies of the viewstate. Single view state went GA in Summer 12 and fixed this so that multiple forms use a single copy of the view state. Bottom line is one form or many forms should result in the same size viewstate. Nov 19, 2012 at 10:33

2 Answers 2

120
+50

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">&nbsp;&nbsp;</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">&nbsp;&nbsp;</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">&nbsp;&nbsp;</td>
                    <td class="padding">&nbsp;&nbsp;</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...

enter image description here

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">&nbsp;&nbsp;</td>         
            <td style="display:{!IF(viewrow.level>=3, 'table-cell', 'none')}" class="padding">&nbsp;&nbsp;</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%).

enter image description here

With the apex:inputCheckbox component commented out, it reduces to 1.27kb (20%).

enter image description here

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.

5
  • 7
    Andrew, this is some amazing research, and sheds a lot of light into the mysterious "Internal" view state and I'm sure will help a lot of people who run into this down the line. Thank you!! Nov 25, 2012 at 13:48
  • also just to note: style="display:{!IF(viewrow.level>=2, 'table-cell', 'none')}" throws an error in saving HTML in the IDE. Pretty much try with any APEX output inside of style-tags in HTML. Thanks for the solution! Trying to implement some of this stuff right now. Jan 22, 2013 at 17:43
  • Thanks for the update Jordan, I didn't notice that when I wrote it, maybe different IDE version? Hope it helps you out. You also prompted me to add a further point around apex:form tag above. Jan 22, 2013 at 18:36
  • 2
    @AndrewFawcett fyi, 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 Feb 21, 2013 at 0:46
  • the form tag... Jun 26, 2015 at 1:50
6

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

7
  • The trouble is the whole point of the page is to allow them to do mass action on large sets of accounts, limiting the result set also limits the utility of the page. Nov 20, 2012 at 18:19
  • I think the best pattern here would be to use the page to identify the accounts concerned, all you need is a list of IDs, and then you do the heavy lifting in the controller side where you have much higher governor limits on the amount of memory and rows.
    – Doug B
    Nov 21, 2012 at 9:04
  • That's what I'm doing, the only thing that goes to the front end that is non-transient is the id, which is pretty light ... based on some talks with support I'm guessing it has to do with some nested repeats I have (the results can be grouped into two levels). Will post some additional details so people can get some context. Just need to trim it down to make it digestible on this forum. Nov 21, 2012 at 21:08
  • Another thing is, the record set falls into the part of the view state I can see. The question has to do with why the "Internal" portion of the view state is so big. Nov 21, 2012 at 21:23
  • In instantiating your class of Results, each of them has a reference to an Account. I think that is where the internal element of the view state may be coming from. Could you experiment with that commented out?
    – Doug B
    Nov 22, 2012 at 9:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .