Orkut Application Platform

Latency Tips for Orkut

Arne Rooman-Kurrik and Lane LiaBraaten, Google Developer Programs
May 2008

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 article contains tactics for reducing latency in your app that will decrease the load on your servers and provide a better experience for your users.

Contents

  1. Orkut-specific techniques
    1. Prefetch data from orkut
    2. Preload data from your server
    3. Cache your static content
  2. Best practices for web development
    1. Control the caching on your content
    2. Reducing the number of fetches
    3. Some other best practices
  3. Let the container cache your dynamic content
  4. Use multiple content sections
    1. Use appData as a cache for content
    2. An example

Orkut-specific techniques

Prefetch data from orkut

The orkut team has implemented a "pre-fetcher" that will analyze your app and attempt to load the data you need at the same time it's rendering the container page, so when your app sends a DataRequest, the response is almost instantaneous. To get the most out of this feature, follow these guidelines:

  • Do request data that you will always or nearly always need, even if it's not needed immediately on loading your gadget.
  • Do batch up multiple request items into one DataRequest.
  • Don't request data you don't need.
  • Don't add newUpdatePersonAppDataRequest to your first DataRequest.
  • Don't call opensocial.requestCreateActivity before sending your first DataRequest.

Preload data from your server

If your application uses a makeRequest call to fetch data from a third party server, chances are that you've written something similar to:

function request() {
  var params = [];
  ...
  gadgets.io.makeRequest("http://www.example.com/content.html", response, params);
};
gadgets.util.registerOnLoadHandler(request);

While this code is syntactically correct, it isn't very efficient at loading data. Users of your application will need to:

  1. Wait for orkut to render your application IFrame.
  2. Wait for the IFrame to finish loading so the OnLoadHandler methods will execute
  3. Wait for the makeRequest call to return data from your server.

During this time, a slow application will show a loading animation in the best case, or nothing at all in the worst. To address this, orkut will offer additional syntax for preloading content from a makeRequest call while the gadget is being rendered. This feature is enabled through the addition of an extra <Preload> parameter in your ModulePrefs. If your makeRequest call looks like:

var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
gadgets.io.makeRequest("http://www.example.com", response, params);

You can cache the request by adding a <Preload> tag:

<ModulePrefs title="Demo Preloads" description="Demo Preloads">
  <Require feature="opensocial-0.7" />
  <Require feature="views" />
  <Preload href="http://www.example.com" />
</ModulePrefs>

When your application IFrame loads, you will see something similar to the following embedded in the source:

gadgets.io.preloaded_ = {"http://www.example.com":{"body":"...","rc":200}};

Where "..." is the content that exists at http://www.example.com. When your application executes the makeRequest call, this content will be returned instantly, without needing to hit your server again. Signed request calls can take advantage of preloads with a slight change to the preload syntax. If your signed request code looks like:

var params = {};
params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED;
gadgets.io.makeRequest("http://www.example.com", response, params);

The corresponding preload code you should use is:

<ModulePrefs title="Test Preloads" description="Test Preloads">
  <Require feature="opensocial-0.7" />
  <Require feature="views" />
  <Preload href="http://www.example.com" authz="signed" />
</ModulePrefs>

There are a few more optimizations you can use to make preloads work even better:

  1. Turn off sending the viewer in signed requests. If you don't need the VIEWER ID for your signed request, disable it by adding signViewer="false" to your <Preload> tag. This will allow orkut to cache your request for a lot more requests. This is a critical improvement for profile pages!
  2. Use multiple <Preload> tags if you have more than one request. You're not limited to one <Preload> tag, so preload whatever you can.
  3. Restrict preloads to the correct view. If you only use a certain request in a specific view, restrict the preload to that view by adding a views attribute to your <Preload> tag. For example, to restrict a preload to the canvas view, add views="canvas" to your <Preload> tag. You can also specify multiple comma separated views, like views="canvas,profile".

What are the benefits? Users no longer need to wait for your application to finish loading on orkut before executing a makeRequest call. Orkut will make the request and insert the response directly into the application as it renders your application.

Cache your static content

The Orkut sandbox will now rewrite the appropriate href or src attributes on HTML elements to take advantage of the caching proxy, meaning that all non-dynamic references to remote content will automatically get the benefit of caching.

If your source contains an image tag which looks like:

<img src="http://www.example.com/i_heart_apis.png" />

Orkut will render the image tag as:

<img src="http://www.orkut.gmodules.com/gadgets/proxy?url=http%3A%2F%2Fwww.example.com%2Fi_heart_apis.png" />

Any HTML fetched using makeRequest or otherwise passed through the proxy will also have its links rewritten in this manner in order to cache the returned content. But while URL rewriting is enabled by default in the Orkut sandbox, this mechanism must be explicitly enabled for applications running on www.orkut.com. To do this, place the following block in your app spec's ModulePrefs element, replacing the [REGULAR_EXPRESSION] blocks with your custom expressions:

<Optional feature="content-rewrite">
  <Param name="include-urls">[REGULAR_EXPRESSION]</Param>
  <Param name="exclude-urls">[REGULAR_EXPRESSION]</Param>
  <Param name="include-tags">[COMMA SEPARATED LIST OF HTML TAG NAMES]</Param>
</Optional>

The first parameter above is where you should place full or partial URLs (using standard regular expression syntax) to be automatically proxied. The second parameter accepts URLs that should be explicitly excluded from the automatic proxying. Finally, the third row contains element names that the preprocessor targets for rewriting. For example, to enable rewriting for all URLs and all tags, include the block below. Notice the .* in the first parameter -- this is a common regular expression for matching all characters, meaning that all URLs will effectively be included here:

<Optional feature="content-rewrite">
  <Param name="include-urls">.*</Param>
  <Param name="exclude-urls"></Param>
  <Param name="include-tags">img,script,embed,link,style</Param>
</Optional>

You can use a similar block to disable rewriting in the sandbox (for testing only!) by leaving the first and third parameters empty and specifying .* in 'exclude-urls'. Keep in mind that this should only be used to help develop your application. Once your application is polished and ready for deployment, you should re-enable rewriting so that your app can take advantage of the automatic proxying, which reduces the load on your server for image, CSS, and JavaScript resources that are directly included in your application source, without you needing to change any code at all.

Note: You can also use gadgets.io.getProxyUrl to proxy select resources if you choose to disable rewriting. If you have rewriting enabled, however, this is redundant.

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 &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 &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") && data.get("viewer").getData(),
        owner = data.get("owner") && data.get("owner").getData(),
        appData = data.get("appData") && 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>

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.