I work for a design company that has implemented this on multiple sites. We've been looking at the performance hit on the server for having every cached page call back to the server, and it's not something we like. It's certainly better than running totally uncached pages, but it could be better.

I should explain our use case so our usage makes sense. We tend to run page_cache on, then ajaxblocks replaces one single block, maybe two blocks on the page with something specific to the user. In our case, it's often 'weather in your area' or 'your closest retailer'. They don't change between pages, and are prime candidates for caching per user. The code I've worked up for you to review won't work if it's cache_per_page or no cache blocks, but for cache_per_user and cache_global blocks, it should be a solid performance increase.

I spent a few hours and scraped together a small fork for you to review. It uses json2.js, jquery.cookie.js, and jquery.jsoncookies.js. These likely should be library level requirements, rather than included directly in this module, but for demo purposes it works. The idea is pretty simple:

  1. Hit the server via ajaxblocks standards.
  2. Keep a list of all blocks eligible to be cached cookie: ajaxBlocks
  3. Write each cachable block's data object to a cookie using jquery.JSONcookie()
    Note that each data object is being stored seperately due to cookie byte size limitations. It's possible that the full data object would not fit in browser cookies as the JSONcookie encoding increases byte size considerably.
  4. On the next page load, ajaxblocks.js checks if the ajaxBlocks cookie exists. If it does, it loads each block's data out of the cookies, and sends along to be replaced in the html, without hitting the server again.

It's a skeleton right now, as I know there's fringe cases and logic to consider for things I've not thought of. But, I thought you might appreciate a look, and I might be given some time to work with you to get this into the a future release of ajax blocks. So, what does the community think?

Most of the changes went into the js file, so I'm putting it up here for easy review, and a full zip of the project.

Note: I added jquery.cookie.js into the project at the very end. The logic should be adjusted to remove the extra functions if they are not used. However I left them, in case you want to somehow get around adding jquery plugins instead.

/**
 * @file
 * Loads content of blocks via AJAX just after page loading, updates Drupal.settings, reattaches behaviors.
 */

Drupal.settings.ajaxblocks_cookie = 1;

(function ($) {

Drupal.ajaxblocksSendRequest = function (request, delay) {
  if (delay) {
    setTimeout(function () {Drupal.ajaxblocksSendRequest(request, 0);}, delay);
    return;
  }
  var ajaxBlocksCookie = null;
  var ajaxCookieContent = null;
  
  console.log('Drupal.ajaxblocksSendRequest');
  // Check if the response is already stored in a cookie
  var ajaxCookie = Drupal.ajaxblocksReadCookie('ajaxBlocks');
  console.log(ajaxCookie);
  
  if( Drupal.settings.ajaxblocks_cookie && ajaxCookie != null ){
    console.log('Used cookie data');
    // Use the stored object
    var ids = ajaxCookie.split(',');
    console.log(ids);
    jQuery.each(ids, function(index, value){
      var ajaxCookieData = jQuery.JSONCookie('ajaxBlocks-' + value);
      console.log(value);
      console.log(ajaxCookieData);
      Drupal.ajaxblocksSetBlockContent(value, ajaxCookieData);
    });
  } else {
    // Go back to the server
    $.ajax({
      url: ((typeof Drupal.settings.ajaxblocks_path !== 'undefined') ? Drupal.settings.ajaxblocks_path : (Drupal.settings.basePath + "ajaxblocks")),
      type: "GET",
      dataType: "json",
      data: request + '&nocache=1',
      cache: false,
      success: function (data) {
        // Store the results of the response
        //Drupal.ajaxblocksCreateCookie( 'ajaxBlocks', data, 1 );
        //console.log(data);
        //console.log(data.toSource(), data.toString().length);
          //console.debug(data.toSource());
        //
      
        // Replaces the placeholder divs by the actual block contents returned by the AJAX call,
        // executes the extra JavaScript code and attach behaviours if the apply to the blocks.
        Drupal.freezeHeight();
        var ajaxCookie = [];
        for (var id in data) {
          Drupal.ajaxblocksSetBlockContent(id, data[id]);
          if(  Drupal.settings.ajaxblocks_cookie ) {
            ajaxCookie.push(id);
            console.log(data[id]);
            jQuery.JSONCookie('ajaxBlocks-' + id, data[id], {path: '/'});
            console.log('Created cookie: ' + 'ajaxBlocks-' + id);
          } 
        }
        if(  Drupal.settings.ajaxblocks_cookie ) {
          Drupal.ajaxblocksCreateCookie( 'ajaxBlocks', ajaxCookie.toString(), 1 );
        }
        Drupal.unfreezeHeight();
        console.log('Retrieved from server.');
      }
    });
  }
}

Drupal.ajaxblocksSetBlockContent = function (id, data) {
  console.log('ajaxblocksSetBlockContent');
  console.log(data);
  if (data['delay']) {
    setTimeout(function () {data['delay'] = 0; Drupal.ajaxblocksSetBlockContent(id, data);}, data['delay']);
    return;
  }
  var wrapper = $('#block-' + id + '-ajax-content');
  if (!wrapper) return;
  var context = wrapper.parent();
  Drupal.detachBehaviors(context);
  if (!context) return;
  $('#block-' + id).addClass('ajaxblocks-loaded');
  context.html(data['content']);
  if (data['ajaxblocks_settings']) $.extend(true, Drupal.settings, data['ajaxblocks_settings']);
  Drupal.attachBehaviors(context);
}

Drupal.ajaxblocksCreateCookie = function(name,value,days) {
  if (days) {
    var date = new Date();
    date.setTime(date.getTime()+(days*24*60*60*1000));
    var expires = "; expires="+date.toGMTString();
  }
  else var expires = "";
  document.cookie = name+"="+value+expires+"; path=/";
}


Drupal.ajaxblocksReadCookie = function(name) {
  var nameEQ = name + "=";
  var ca = document.cookie.split(';');
  for(var i=0;i < ca.length;i++) {
    var c = ca[i];
    while (c.charAt(0)==' ') c = c.substring(1,c.length);
    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  }
  return null;
}

$(document).ready(function () {
  if (typeof Drupal.settings.ajaxblocks !== 'undefined') {
    // Drupal.settings.ajaxblocks = "blocks=custom-nearest_location&path=node/4"
    // Drupal.settings.ajaxblocks_delay = 3000
    Drupal.ajaxblocksSendRequest(Drupal.settings.ajaxblocks, Drupal.settings.ajaxblocks_delay);
  }
});

$(window).load(function () {
  if (typeof Drupal.settings.ajaxblocks_late !== 'undefined') {
    Drupal.ajaxblocksSendRequest(Drupal.settings.ajaxblocks_late, Drupal.settings.ajaxblocks_late_delay);
  }
});

})(jQuery);

Comments

maximpodorov’s picture

Excuse me for the late response. :)
It's very interesting functionality. Let's start to integrate it in the module. But I suggest to use session storage (http://en.wikipedia.org/wiki/Web_storage) for the modern browsers.

lance.gliser’s picture

Aye.. Good point. That's far stronger, and seems well supported. I'll start movement on that soon I think now that the idea's approved.

Next question we should discuss. How do we determine when something is client cachable? Block cache per user is easy. Block cache global shouldn't be ajaxed (I think?). Block cache per page... Likely not needed through ajax blocks cache.

Where we get into nastyness, is block no cache. We have no way to know if the content should be cached or not. What interface would you like to see for telling us if we should try this client caching? Maybe a radio set in the ajax fieldset similar to 'Allow this block to be cached by the client':
- For use on all pages
- For each page on which it appears

I wonder, depending on the number of pages possible on a site, could we even possibly offer the second option?

Any other concerns? Maximum cache age controls?

lance.gliser’s picture

I'm working on this now. I should have something to show soon in the next two days.

lance.gliser’s picture

Status: Active » Needs review
StatusFileSize
new38.14 KB
new13.9 KB

Alright, got a working version with all the use cases I could think of in it.

I aimed at new browsers as you suggested, and didn't force support for old ones into the module. Support for browsers without native JSON support can be added through the settings page's instructions. Cookie support is disabled by default as well.

Individual blocks can be cached, and their maximum lengths vary as well.

I've attached a patch, and a full directory for you to experiment with.

I made one somewhat unrelated change in the .install. Your previous schema noted itself as 6102. I've updated the module to instead run 7103 to get the right Drupal major version.

Tell me what you think!

dcoulombe’s picture

Hello there,

Inside some of my blocks, I can send an AJAX call that create a cookie and modify the content of my block without reloading the page. The updates are then not saved in the local storage until the next opening (i need to open two more pages to see my modification).

Here a simple JS patch that solve this issue:

$(document).ready(function() {
  window.onbeforeunload = function(e) {
    var cache;
    
    for (var id in Drupal.settings.ajaxblocks_blocks) {
      cache = Drupal.ajaxblocksGetCache('ajaxblock-' + id);
      cache['content'] = $('#block-' + id.replace(/\_/g, '-') + ' div.content').html();
      Drupal.ajaxblocksSetCache('ajaxblock-' + id, cache);
    }
  }
});
Anonymous’s picture

I have tested this patch and it is throwing an error when saving block configuration:

Call to undefined function ajaxblocks_validate_settings() includes/form.inc on line 1443

It seems you have hooked to a submit handler that doesn't (yet) exist. After commenting out line 182 the patch appears to work correctly.

lance.gliser’s picture

Status: Needs review » Needs work
StatusFileSize
new38.59 KB

I recently had a D6 site that needed this client cache functionality. I don't have a proper patch, and the files in this zip consider themselves a local copy, independant of the project, but here's a working D6 port for consideration as well.

lance.gliser’s picture

Title: Cache ajax'd blocks » Client Caching
Version: 7.x-1.3 » 7.x-1.x-dev
Assigned: Unassigned » lance.gliser
Status: Needs work » Needs review
StatusFileSize
new13 KB

Took some time to rework the patch provided #4. I have not addressed the case from #5, because I am unsure if that belongs in this patch, vs a custom site specific modification. I'd like a maintainer to weight on that point.

I've tested upgrade, and a fresh installation.

Couple notes:

- I changed around the javascript variables. With the additions of this patch Drupal.settings was getting bloated with ajaxblocks_* settings. I moved them into a self containing object instead.
- I have addressed the bug pointed out in #6.
- This patch is written against the current dev, instead the previous patches older version.

maximpodorov’s picture

Very promising. I think it can be accepted with small modifications. I'd prefer per block settings for client caching. And is there any reason to use milliseconds instead of seconds as expiration time units?

lance.gliser’s picture

"I'd prefer per block settings for client caching"
That should be what's in this patch? There is an admin page added in the patch, but the only setting is for enabling cookie based caching as well as local storage.

"And is there any reason to use milliseconds instead of seconds as expiration time units?"
None. Honestly, I'd think you'd be able to set time in hours, rather than seconds in most use cases. Seems almost like a preference thing as to how far we ramp it up. If a maintainer wants this moved to a calculated increment, I would suggest we do minutes or hours.

Anything else needed to move this patch forward?

lance.gliser’s picture

Issue summary: View changes

Cleaned formatting issues, and explained why there are two methods for setting cookies.

lance.gliser’s picture

Assigned: lance.gliser » Unassigned