Third & GroveThird & Grove
Feb 28, 2018 - Wes Jones

Integrating Optimizely in Drupal 7 without Degrading Site Performance

Optimizely is a great tool for running A/B tests of different messages or alternate designs and layouts. But we reviewed one of our Drupal 7 sites that integrates Optimizely with the contributed module, we made a rather startling discovery: Yikes! The Optimizely module functions are taking up over 50% of the CPU cycles!

The data brings up a larger question. How can we integrate Optimizely in a Drupal 7 site without degrading performance or user experience? Here are the best practices we came up with.

The Optimizely snippet

The first step is to add Optimizely javascript to the pages involved in the test. According to this page, you should add this script synchronously in the <head> section of the page. This will add some time to the page execution—enough that it will be noticeable to end users.

You can add it asynchronously, or move it to the footer section of the page, but it’s not your best option. According to the article:

"If the snippet is added anywhere lower, it will technically still work. But if the page has already loaded content that the visitor sees before the Optimizely snippet loads, the original version of the page may load before being transformed into the variation by Optimizely. This is known as "page flashing." In most cases, the code executes too quickly for this to be visible. But to avoid any potential problems we strongly encourage you to add the snippet as early in the execution path as possible."

 

Since we don't want to add this extra overhead to every page on the site, our best option is to load Optimizely javascript synchronously in the <head> section, but only on pages running a test.

 

That way, you don’t have to worry about degraded performance or visual loading issues on the rest of the site, and the pages with tests running will have the best experience.

So how do you implement it?

Adding Optimizely to a specific page with Drupal API

Knowledgebase recommends the Optimizely module. Full respect to the developers who contributed this module. It has a convenient UI for inputting all paths to add the snippet,and it's still very useful for small-to-medium sites. But why does it consume so much memory on large sites?

It's a good idea to understand how these functions work—and how we can write more efficient PHP to accomplish the same task.

Take a look into the code, and you’ll find the culprit: Several expensive API calls are made in a hook_init(): drupal_match_path() and drupal_lookup_path().

Here's a simplified version of that module's hook_init():

/**
 * Implements hook_init().
 */
function optimizely_init() {
  $current_path = current_path();
  $current_path_alias = drupal_lookup_path('alias', $current_path);
 
  $big_long_list_of_paths = get_big_long_list_of_paths();
  foreach ($big_long_list_of_paths as $path_to_check) {
    if ( stristr($current_path, $path_to_check) ||
      stristr(drupal_lookup_path('alias', $current_path), $project_path) ||
      stristr(drupal_lookup_path('source', $current_path), $project_path) ) {
      $add_snippet = TRUE;
    }
  }
 
  if (drupal_match_path($current_path, $project_paths) ||
    drupal_match_path($current_path_alias, $project_paths)) {
    $add_snippet = TRUE;
    break;
  }
 
  if ($add_snippet) {
    drupal_add_js('optimizely snippet');
  }
}

 

Calls to drupal_lookup_path() will query the database. If you look at the code above, it's calling drupal_lookup_path() not only on the current path, but twice for each path in the big long list. If you have 50 paths to check, that's at least 101 queries on every page load. That’s a lot of queries!

 

drupal_match_path() will take every path in the list and do a preg_replace() and a Mpreg_match(). Again, if we have a big long list of paths to check, that adds up.

So, if there are potential pitfalls with implementing Optimizely in this way, how can we do better with Drupal API? Here are a few pointers:

Avoid hook_init() for calls to drupal_add_js() - use hook_page_build() or template_preprocess_page()

It’s not a good idea to add javascript using drupal_add_js() in hook_init(). hook_init() runs very early in the page execution and often, even on cached pages. You should call it in a more specific scope to the page content, like hook_page_build() or template_preprocess_page().

Avoid multiple calls to drupal_get_path_alias()

drupal_get_path_alias() is the more common API function and internally just calls drupal_lookup_path('alias'), so the same observations apply. Call it once to get the current path's alias, but don't call it on multiple paths, especially when you know it could be calling 20+ items.

If you're using Page Manager, check page_manager_get_current_page()

The value returned by page_manager_get_current_page() is statically cached, so calling this in multiple hooks won't introduce more database queries to the page execution.

The following is an example of how to implement it:

/**
 * Implements template_preprocess_page().
 */
function module_a_preprocess_page(&$variables) {
  $page = page_manager_get_current_page();
  if (!$page || $page['name'] != 'page-my-specific-page-name') {
    return;
  }
 
  // Any code after this will only run on my-specific-page-name.
  drupal_add_js('https://cdn.optimizely.com/js/12345.js', array('type' => 'external'));
}

 

Conclusion

 

After diving into the code implementing Optimizely, we learned a whole lot about PHP performance and writing efficient code! This isn't a comprehensive discussion by any means, but if you're implementing Optimizely on a Drupal 7 site with Panels, here are the main takeaways:

  • Don't use the Optimizely module unless your site is small
  • Avoid calling drupal_add_js() within hook_init()
  • Don’t loop through a list of paths and calling drupal_get_path_alias()
  • Check for a specific Panels page with page_manager_get_current_page()

Following these suggestions will help your website perform better, and shift the performance overhead for Optimizely tests where it should be—on the test page itself.

Happy testing!