Improving Latency

Briefing

If you've ever looked at Firebug when loading a profile page, you're familiar with the flurry of activity involved in loading an OpenSocial app—but this doesn't mean your app can't be snappy to load. This field manual contains tactics for reducing latency in your app that will decrease the load on your servers and provide a better experience for your users.

Be sure to read the Latency Measurement guide to get an accurate measurement of your app's latency.

Best practices for web development

Many techniques that are used in normal web development will also benefit your OpenSocial app. Here are some of the most effective techniques.

Control the caching on your content

Most containers offer support for the Cache-Control HTTP header. You have server-side control over how your resources are cached, so be sure to set your headers appropriately for maximum benefit.

The Cache-Control header is best described in the HTTP/1.1 specification but there are some simpler descriptions available as well. If you're not sure about the cache headers your server is currently sending, you can try some publicly available tools to examine the cache headers on your files and see if they need to be tweaked.

Be aware that the Cache-Control header will be examined for all content coming from your server, including XML application specs, responses from makeRequest (both prefetched and not), and proxied images. Be sure to set caching headers for all of this content!

Notes on Apache

Apache defaults to using Last-Modified and ETag headers to control caching for static files, rather than the recommended Expires and Cache-Control: max-age headers. If you are using Apache, change your cache headers to Expires and Cache-Control: max-age!

Need to disable caching on your Apache server? Use the following in your .htaccess file to disable caching on .css, .js, and .xml files (change the FilesMatch line if you need to support more filetypes):

<FilesMatch "\.(css|js|xml)$">
Header unset ETag
FileETag None
Header set Cache-Control "no-cache"
</FilesMatch>

What are the benefits? Your server has much more control over how the container caches its content. You can set a low cache expiration for content that changes often, and a high cache timeout for content that does not change. Caching will become much more efficient once you set the appropriate headers.

Reducing the number of fetches

The HTTP/1.1 specification states:

*Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.*

For this reason, some internet browsers (like IE7) will only download two files from a given server at a time, shared amongst all HTML, XML, image, CSS, and JavaScript files. To reduce the number of connections that a user has to make back to your server, consolidate and inline as much code as possible.

If your JavaScript includes look like:

  <script src="http://www.example.com/javascript.core.js" type="text/javascript"></script>
  <script src="http://www.example.com/javascript.extra.js" type="text/javascript"></script>

then you should combine each file into one master JavaScript file:

<script src="http://www.example.com/javascript.all.js" type="text/javascript"></script>

Better yet, inline your code if at all possible:

<script type="text/javascript">
   function1() {
      ...
   };
   ...
</script>

This will save server connections for other assets. Remember that this approach can be used for CSS, as well.

To decrease the number of image files your application needs to load, you can use image spriting to combine all your image files into a single master "sprite" file. Check out A List Apart's CSS Spriting article for a good description of this technique.

Generally speaking, concatenating your files is a great performance improvement you can make. Because of the aggressive caching that containers perform, even using a relatively slow server-side script to automatically concatenate files will still wind up performing better than separate files (once the automatically concatenated file is cached). Aim for a single CSS and a single JS file in production.

What are the benefits? This approach keeps the number of server connections low, and reduces the total number of HTTP requests that each user of your application has to make.

Use Google's AJAX Libraries API for Popular JavaScript Frameworks

App developers very often use frameworks such as Prototype, jQuery, Dojo, or Script.aculo.us. In OpenSocial container environment, where applications are embedded in multiple iframes, these libraries often get loaded multiple times, not properly gzipped or minified. Google's latest AJAX libraries API addresses these issues by serving properly gzipped and minified version of these libraries from fast edge servers that are close to clients, providing caching once and for all, and significantly improving performance.

For example, to load Prototype 1.6.0.2 you would do the following:

<script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.2/prototype.js"></script>

For more info on how to take advantage of AJAX Libraries API to speed up your apps on Google's infrastructure, go to AJAX Libraries API.

Some other best practices:

  • Turn on gzip for any content you deliver. Good things come in small packages.
  • Minimize JS and CSS. Again, small is good.
  • Split CSS and image files across 2-4 servers. Browsers limit the number of concurrent connections to any one server.
  • Place JavaScript as late in the page as possible. Loading JavaScript blocks the downloading of other important components like images and CSS.

Tip: Try the YSlow Firefox plugin to analyze your app's performance.

Let the container cache your dynamic content

The gadgets.io.getProxyUrl function will return the location of the cached version of the URL you provide, including images, JavaScript, and CSS. So instead of using the URL of content hosted on your server, like this:

function showImage() {
  imgUrl = 'http://www.example.com/i_heart_apis_sm.png';
  html = ['_img src="', imgUrl, '"_'];
  document.getElementById('dom_handle').innerHTML = html.join('');
};

showImage();

you can use the URL of the cached content, like this:

function showImage() {
  imgUrl = 'http://www.example.com/i_heart_apis_sm.png';
  '''cachedUrl = gadgets.io.getProxyUrl(imgUrl);'''
  html = ['_img src="', '''cachedUrl''', '"_'];
  document.getElementById('dom_handle').innerHTML = html.join('');
};

showImage();

Use multiple content sections

Take advantage of multiple content sections in your gadget spec to render more tailored views for canvas and profile pages. This will help ensure that the container only loads the necessary components for each view. In particular, focus on making your profile view as lean as possible.

Use appData as a cache for content

It's much faster to request data from the container than it is to hit your own server. There are lots of ways you can cache your application data in the Persistence API and speed up page loads. The profile view is a great place to do this because it gets a lot of page views and there is less dynamic content.

Here's the slow way to load a profile page:

  1. User opens profile page.
  2. Your app uses makeRequest to get data from your server.
  3. Once the data is returned, your app renders the profile page.

Here's a much faster way:

  1. User opens profile page.
  2. Your app uses a DataRequest to get data from the container.
  3. Once the data is returned, your app renders the profile page.
  4. Now, your app uses makeRequest to get data from your server.
  5. Once the data is returned, your app updates the profile page.

An example

First, let's look at using multiple content sections. Here's the bare minimum:

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
  <ModulePrefs title="users &amp;lt;3 speed">
    <Require feature="opensocial-0.7" />
  </ModulePrefs>
  <Content type="html" view="profile">
    <![CDATA[
      Hello, profile!
    ]]>
  </Content>
  <Content type="html" view="canvas">
    <![CDATA[
      Hello, canvas!
    ]]>
  </Content>
</Module>

Now let's use the technique where we populate the profile view with HTML cached in appData:

  <Content type="html" view="profile">
    <![CDATA[
<script type="text/javascript">
  function request() {
    var req = opensocial.newDataRequest();
    req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.OWNER), "owner");
    req.add(req.newFetchPersonAppDataRequest(opensocial.DataRequest.PersonId.OWNER, "profile"), "usrdata");
    req.send(response);
  };

  function response(data) {
    console.log(data);
    var usrdata = data.get("usrdata").getData(),
        owner = data.get("owner").getData(),
        profileHtml = 'No data';
    if (usrdata[owner.getId()]) {
      profileHtml = usrdata[owner.getId()].profile || 'Empty data';
    }
    document.write(profileHtml);
};
 
  gadgets.util.registerOnLoadHandler(request);
</script>
    ]]>
  </Content>

Finally, implement some functionality for the canvas view. When the user takes an action that will update the data shown in their profile, update the 'profile' field in appData. This app lets the user set a quote to be displayed on their profile. When the 'save' link is clicked, the quote and the HTML to display in the profile view are updated in appData. Here's the full application spec:

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
  <ModulePrefs title="users &amp;lt;3 speed">
    <Require feature="opensocial-0.7" />
  </ModulePrefs>
  <Content type="html" view="profile">
    <![CDATA[
<script type="text/javascript">
  function request() {
    var req = opensocial.newDataRequest();
    req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.OWNER), "owner");
    req.add(req.newFetchPersonAppDataRequest(opensocial.DataRequest.PersonId.OWNER, "profile"), "usrdata");
    req.send(response);
  };

  function response(data) {
    console.log(data);
    var usrdata = data.get("usrdata").getData(),
        owner = data.get("owner").getData(),
        profileHtml = 'No data';
    if (usrdata[owner.getId()]) {
      profileHtml = usrdata[owner.getId()].profile || 'Empty data';
    }
    document.write(profileHtml);
};
 
  gadgets.util.registerOnLoadHandler(request);
</script>
    ]]>
  </Content>
  <Content type="html" view="canvas">
    <![CDATA[
<script type="text/javascript">
  function request() {
    var req = opensocial.newDataRequest();
    req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.OWNER), "owner");
    req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.VIEWER), "viewer");
    req.add(req.newFetchPersonAppDataRequest(opensocial.DataRequest.PersonId.OWNER, "quote"), "appData");
    req.send(response);
  };

  function response(data) {
    var viewer = data.get("viewer") &amp;&amp; data.get("viewer").getData(),
        owner = data.get("owner") &amp;&amp; data.get("owner").getData(),
        appData = data.get("appData") &amp;&amp; data.get("appData").getData(),
        quote = '',
        text = '';
    if ((viewer.getId() || -1) == (owner.getId() || -2)) {
      if (appData[owner.getId()]) {
        quote = appData[owner.getId()];
      }
      text = ['Edit your quote: ',
              '<input id="quote_input" type="text"/> ',
              '<a href="javascript:void(0);" onclick="save();" value="',
              quote,
              '">save</a>'].join('');
      document.getElementById('main').innerHTML = text;
    }
  };
                                                              
  function save() {
    var quote = document.getElementById('quote_input').value,
        profileHtml = '';
    profileHtml = ['Latest quote: ', quote].join('');
    req = opensocial.newDataRequest();
    req.add(req.newUpdatePersonAppDataRequest(
        opensocial.DataRequest.PersonId.VIEWER, "quote", quote), "updatequote");
    req.add(req.newUpdatePersonAppDataRequest(
        opensocial.DataRequest.PersonId.VIEWER, "profile", profileHtml), "updateprofile");
    req.send(response2);
  };
 
  function response2(data) {
    if (!data.hadError()) {
      document.getElementById("status").innerHTML = "Saved quote at " + new Date();
    } else {
      document.getElementById("status").innerHTML = "There was a problem updating your profile";
    }

    /*
     * Now that the page is loaded you can use makeRequest to
     * see if you have fresher data on your server.
    */
  };

  function status(text) {
    var dom =
    dom.innerHTML = text;
  };
 
  gadgets.util.registerOnLoadHandler(request);
</script>
<div id="main"></div>
<div id="status"></div>
    ]]>
  </Content>
</Module>