codescience-resource-reatured-image-generic

For well-known performance reasons, client-side caching of web resources like stylesheets, images, and scripts is important. This becomes pretty important for those who are creating mobile/tablet Visualforce pages for Salesforce1. Luckily HTML5 Application Cache is well supported on Visualforce + Salesforce1 stack; please refer to this official document for more details. Getting started with Cache Manifest is easy, but problems come when cached resources are changed, and we need to force the browser to refresh them. Let’s start with a sample cache manifest for a Visualforce page, which might look similar to this:

<apex:page contentType=”text/cache-manifest” applyHtmlTag=”false” standardStylesheets=”false” showHeader=”false”>CACHE MANIFEST NETWORK: * CACHE: # all static resources to be cached {!URLFOR($Resource.html5Frameworks,’sf1-bootstrap/css/bootstrap.min.css’)} {!URLFOR($Resource.html5Frameworks,’sf1-bootstrap/js/bootstrap.min.js’)} {!URLFOR($Resource.html5Frameworks,’angular.min.js’)} {!URLFOR($Resource.eventsApp, ‘styles/base.css’)} {!URLFOR($Resource.eventsApp, ‘partials/view1.html’)} {!URLFOR($Resource.eventsApp, ‘partials/view2.html’)} {!URLFOR($Resource.eventsApp, ‘app.js’)} </apex:page>

This above app tries to cache resources from regular HTML5 frameworks like CSS and scripts from Angular and Bootstrap. Apart from that, the Angular app stuff is packed in a zipped resource named “eventsApp“. We want to cache scripts, CSS, and partial templates of this resource as well. Out of above cached resources, Angular and Bootstrap resources will rarely change, unless we upgrade versions. But the eventsApp resources will require updates in the following scenarios:

  1. Whenever developer changes any of the source file in eventsApp i.e. script/css or partials.
  2. A new stable version of app is uploaded as managed package. This should invalidate all cached resources on customer’s devices with new one in package.
Forcing the browser to update cached resources is easy. Any single bit change in Cache Manifest forces the browser to update cached resources. This change is not necessarily a code change in Manifest, it could be a comment change as well. So, we have a few ways to achieve a cache update in the above scenarios:
  1. Add version numbers to resources and update it on any change in the app.
    {!URLFOR($Resource.eventsApp, ‘v1/styles/base.css’)}
    {!URLFOR($Resource.eventsApp, ‘v1/partials/view1.html’)}
    {!URLFOR($Resource.eventsApp, ‘v1/partials/view2.html’)}
    {!URLFOR($Resource.eventsApp, ‘v1/app.js’)}
  2. Add version number as comment in Cache Manifest, which looks better than the above approach and takes less effort, too.
    CACHE:
    # Last updated for EventsApp version : 1.1
    # all static resources to be cached
    {!URLFOR($Resource.html5Frameworks,’sf1-bootstrap/css/bootstrap.min.css’)}
    {!URLFOR($Resource.html5Frameworks,’sf1-bootstrap/js/bootstrap.min.js’)}
    {!URLFOR($Resource.html5Frameworks,’angular.min.js’)}
    {!URLFOR($Resource.eventsApp, ‘styles/base.css’)}
    {!URLFOR($Resource.eventsApp, ‘partials/view1.html’)}
    {!URLFOR($Resource.eventsApp, ‘partials/view2.html’)}
    {!URLFOR($Resource.eventsApp, ‘app.js’)}
  3. Associate an Apex controller with the Cache Manifest page, and let the controller generate a dynamic new comment, in case any static resource is changed.
    <apex:page contentType=”text/cache-manifest” applyHtmlTag=”false”
    controller=”ThirdPartyLibsCacheManifestController”
    standardStylesheets=”false” showHeader=”false”>CACHE MANIFEST
    # Last Changed At : {!lastChangedTimeInMillis}
    NETWORK:
    *
    CACHE:
    # all static resources to be cached
    {!URLFOR($Resource.html5Frameworks,’sf1-bootstrap/css/bootstrap.min.css’)}
    {!URLFOR($Resource.html5Frameworks,’angular.min.js’)}
    {!URLFOR($Resource.eventsApp, ‘styles/base.css’)}
    {!URLFOR($Resource.eventsApp, ‘partials/view1.html’)}
    {!URLFOR($Resource.eventsApp, ‘partials/view2.html’)}
    {!URLFOR($Resource.eventsApp, ‘app.js’)}
    </apex:page>And here is how controller looks:
    public class ThirdPartyLibsCacheManifestController {
    public Long lastChangedTimeInMillis {get; private set;}
    public ThirdPartyLibsCacheManifestController() {
    // Your logic to figure out Namespace prefix, best way could be to query a global apex class
    // and fetch namespace prefix from it.
    String nameSpacePrefix = ‘[…]’;
    StaticResource latestStaticResource = [SELECT LastModifiedDate
    FROM StaticResource
    Where NamespacePrefix like :nameSpacePrefix
    // add more filters as required here
    ORDER BY LastModifiedDate DESC Limit 1];
    /*
    Get last mod timestamps for relevant classes and pages which are in cache
    This will make sure that the container page and its class changes leads to a cache auto update as well
    */
    ApexPage latestPage = [SELECT LastModifiedDate FROM ApexPage
    Where NamespacePrefix like :nameSpacePrefix
    // add more filters as required here
    ORDER BY LastModifiedDate DESC Limit 1];
    ApexClass latestClass = [SELECT LastModifiedDate FROM ApexClass
    Where NamespacePrefix like :nameSpacePrefix
    // add more filters as required here
    ORDER BY LastModifiedDate DESC Limit 1];
    ApexComponent latestComponent = [SELECT LastModifiedDate FROM ApexComponent
    Where NamespacePrefix like :nameSpacePrefix
    // add more filters as required here
    ORDER BY LastModifiedDate DESC Limit 1];
    // Collect all timestamps
    List<Datetime> allTimeStamps = new List<DateTime>{
    latestPage.LastModifiedDate,
    latestStaticResource.LastModifiedDate,
    latestClass.LastModifiedDate,
    latestComponent.LastModifiedDate
    };
    allTimeStamps.sort();
    // get the latest one
    Datetime latest = allTimeStamps.get( allTimeStamps.size() – 1 );
    this.lastChangedTimeInMillis = latest.getTime();
    }
    }

The last approach is automatic and developer friendly for both coding and packaging reasons. So I would highly recommend using this approach.Please note a few key points about  “ThirdPartyLibsCacheManifestController”:

  1. Apart from static resources, it is checking timestamps of classes, page, and components as well. Please remove the part which doesn’t makes sense in your app, i.e. remove the ApexClass soql call, if your app is mostly using “Remote Objects” only, with no Apex extensions or controllers.
  2. This line inthe  manifest page is key for this fixture to work.
    [xml]# Last Changed At : {!lastChangedTimeInMillis}[/xml]

Hope this helps in simplifying your app’s Cache Manifest!