Advice on speeding up Visualforce pages?
In this specific case, consider client-side rendering instead of Visualforce rendering. I wrote up an example of each, with performance considerations:
Pure Visualforce
First, I'm rendering a list of 10,000 items with pure Visualforce inside a data table.
Controller:
public with sharing class repeatvf {
public repeatvf() {
startDateTime = JSON.serialize(System.now());
}
public class wrapper {
public string href { get; set; }
public string value { get; set; }
public wrapper(string h, string v) {
href = h;
value = v;
}
}
static wrapper[] generatewrappers() {
wrapper[] wrappers = new wrapper[0];
for(integer i = 0; i < 10000; i++) {
wrappers.add(new wrapper('http://www.google.com/search?q='+i,'Search Google for '+i));
}
return wrappers;
}
public wrapper[] getwrappers() {
return generatewrappers();
}
public string startDateTime { get; set; }
public string endDateTime { get { return JSON.serialize(System.now()); } }
}
Page Code:
<apex:page controller="repeatvf" readOnly="true">
<script>
var startDateTime = JSON.parse('{!startDateTime}');
</script>
<div id="output">
<apex:dataTable value="{!wrappers}" var="wrapper">
<apex:column >
<apex:outputLink value="{!wrapper.href}">{!wrapper.value}</apex:outputLink>
</apex:column>
</apex:dataTable>
</div>
<script>
var endDateTime = JSON.parse('{!endDateTime}');
</script>
<script>
function onload() {
var div = document.getElementById('totalTime'), output = document.getElementById("output");
output.style.display = 'none';
div.appendChild(document.createTextNode('Total Generation Time: '+(Date.parse(endDateTime) - Date.parse(startDateTime))));
}
window.addEventListener('DOMContentLoaded', onload, true);
</script>
<div id="totalTime">
</div>
</apex:page>
In my browser, this code consistently runs between values of 1,800 and 2,500 during the time of this trial.
Low High
1834 2687
Remoting
Here is identical code, using remoting instead of pure Visualforce:
Controller:
public with sharing class renderjs {
public renderjs() {
startDateTime = JSON.serialize(System.now());
}
public class wrapper {
public string href { get; set; }
public string value { get; set; }
public wrapper(string h, string v) {
href = h;
value = v;
}
}
static wrapper[] generatewrappers() {
wrapper[] wrappers = new wrapper[0];
for(integer i = 0; i < 10000; i++) {
wrappers.add(new wrapper('http://www.google.com/search?q='+i,'Search Google for '+i));
}
return wrappers;
}
public wrapper[] getwrappers() {
return generatewrappers();
}
@RemoteAction
public static wrapper[] wrappers() {
return generatewrappers();
}
public string startDateTime { get; set; }
public string endDateTime { get { return JSON.serialize(System.now()); } }
}
Page:
<apex:page controller="renderjs">
<script>
var jsStartTime = new Date(), vfRemoteStartTime;
function render(data, event) {
var vfRemoteEndTime = new Date(), jsRenderTime, jsEndTime, div, table, tbody, tr, td, a, ctr, jsTime, vfTime, vfRemoteTime;
div = document.getElementById('outputArea');
table = document.createElement('table');
tbody = document.createElement('tbody');
for(ctr = 0; ctr < data.length; ctr += 1) {
tr = document.createElement('tr');
td = document.createElement('td');
a = document.createElement('a');
a.href = data[ctr].href;
a.appendChild(document.createTextNode(data[ctr].value));
td.appendChild(a);
tr.appendChild(td);
tbody.appendChild(tr);
}
table.appendChild(tbody);
div.appendChild(table);
jsEndTime = new Date();
div.style.display = 'none';
div = document.getElementById('resultArea');
vfTime = Date.parse(vfEndDateTime) - Date.parse(vfStartDateTime);
jsTime = jsEndTime - jsStartTime;
vfRemoteTime = vfRemoteEndTime - vfRemoteStartTime;
jsRenderTime = new Date() - vfRemoteEndTime;
div.appendChild(document.createTextNode('VF Time: '+vfTime+', VF Remote Time: '+vfRemoteTime+', JS Time: '+jsTime+', JS Render Time: '+jsRenderTime));
}
function onload() {
vfRemoteStartTime = new Date();
renderjs.wrappers(render);
}
window.addEventListener('DOMContentLoaded', onload, true);
</script>
<div id="outputArea">
</div>
<div id="resultArea">
</div>
<script>
var vfStartDateTime = JSON.parse('{!startDateTime}'), vfEndDateTime = JSON.parse('{!endDateTime}');
</script>
</apex:page>
In this example, I get to see the effects of rendering locally, including better time stamps. I have four values I can view: The initial loading time, the time spent remoting, the time elapsed in the page from start to end (the "JS" time), and the time spent rendering ("JS Render Time").
Note that I could get VF rendering time using logs, but I'm just interested in a quick demonstration. Note that I end up with a total rendering time of 1,200 to 1,500, at least a third of a second faster, and in many cases up to a second faster.
This page gives me the following values:
VF Time VF Remote Time JS Time JS Render Time
Low High Low High Low High Low High
19 25 1085 1277 1258 1451 91 97
Observations
Assuming that VF Time + VF Remote Time in the second set of code is the same approximate non-rendering time as the first page, I can clearly see that I have a rendering time of over 700 ms. Conversely, my browser is rendering the same data in less than 100 ms.
So, we can see from these examples, that the following cases are true:
The increased time until the page is available stems from two factors: bandwidth and Visualforce. First, we're actually transferring far less data than we would be with pure HTML, because we only transfer the data, not the formatting. Secondly, "expressions" require time to evaluate that are much faster in JavaScript than in Visualforce. Had I used conditional rendering, it would have had a more profound effect.
The user has immediate (<0.03 seconds) that the page is indeed loading, instead of the 1.8 to 2.5 second wait without remoting. That said, we could also gain speed boosts by using rerender-on-load (e.g. the main page simply loads, then has a JS function that calls a reRender). It still wouldn't be faster than pure remoting, however.
The overall time until the page is usable is reduced by up to a second, up to a 60% decrease of loading time. The user doesn't have as long to wait.
That being said, this model won't always work, and shouldn't be advocated as the end-all solution. However, whenever you're rendering a ton of data that won't need to be edited, consider remoting whenever possible to reduce loading time.
I'm going to answer my own question and say that if you want to express your rendering logic in Visualforce and you want to leverage things like SObject fields then the only way to achieve speed is to keep the number of fields shown small. Otherwise you can end up with the controller Apex executing in say 200ms but the Visualforce taking say 2 seconds.
Or get creative with asynchronous data loading, preferably of data that isn't showing.
Or don't use Visualforce.
Did you know you can see a performance timeline in the Developer Console using Debug -> Switch Perspective -> 'Analysis' and then clicking on the Timeline?
Four simple suggestions from my experience with rendering large lists in VF:
- A larger list means longer query time, longer Visualforce generation time, longer transmission time to the browser and longer rendering time. So keep those lists short.
- Showing data from objects and fields (e.g.
{!acc.Name}
) is up to 3x slower than storing the Account Name in a different variable and use{!accountName}
. - Using
<apex:outputField>
is up to 3x slower than<apex:outputText>
because it will evaluate Field Level Security. Don't use it if you don't need it. - You can prevent a whole lot of serialization and data transferring if you make controller variables
transient
. You should understand the implications though. Read the documentation for more details.
Using the above rules can make your VF page render significantly faster, I have real-world examples of a 10-fold speed increase!