Cache Pre-Compiled Templates - BACKBONE VIEWS - Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

PART 1: BACKBONE VIEWS

Chapter 5: Cache Pre-Compiled Templates

So far the extracting of common implementations in to reusable objects has been focused on the reduction of code duplication. However, there are other reasons to create common objects and components. At times, performance considerations need to be taken in to account. For example, theBaseView view type has a prime candidate for this: pre-compiling the templates.

A Texas 2-Step

Underscore, like most JavaScript template engines, uses a 2-step process to generate the end result:

1. Compile the specified template in to a function

2. Execute the function, with an option set of data, to generate the HTML result

The BaseView that we’ve defined happens to compress both of these steps in to a single call - _.template(this.template, data) - but the two step process is still happening.

The problem is that most view definitions will not change the template at runtime. That is, once a template has been defined, the same template will be used over and over again no matter how many instances of that view are created and rendered. This is a performance burden on the view instances, re-doing work that has already been done before, to get the same results. In fact, the performance difference between using a pre-compiled template vs compiling everytime can be quite staggering. Pre-compiled templates can be nearly twice as fast, averaging a little over 1.5 times faster, depending on the browser and version used (see this JSPerf test and this blog post on pre-compiled templates for more information).

If this is the case and a view’s template doesn’t change at runtime, then it doesn’t make sense to re-compile the template every time. The solution, then, is to compile the template once and re-use that pre-compiled and cached template every time a new view instance is created.

Building A Template Cache

View instances should check for and/or build a template cache when they are instantiated. Template caches should also be re-used between view instances, and not re-compiled per instance. For that to happen, the easiest place to store the cached template function will be the the view type’s prototype.

1 BBPlug.BaseView = Backbone.View.extend({

2

3 constructor: function(){

4 Backbone.View.prototype.constructor.apply(this, arguments);

5

6 this.buildTemplateCache();

7 },

8

9 buildTemplateCache: function(){

10 var proto = Object.getPrototypeOf(this);

11

12 if (proto.templateCache) { return; }

13 proto.templateCache = _.template(this.template);

14 },

15

16 render: function(){

17 var data;

18 if (this.serializeData){

19 data = this.serializeData();

20 };

21

22 // use the pre-compiled, cached template function

23 var renderedHtml = this.templateCache(data);

24 this.$el.html(renderedHtml);

25

26 if (this.onRender){

27 this.onRender();

28 }

29 }

30 });

There are several items of interest in this implementation.

First, the use of the constructor function. JavaScript itself doesn’t typically provide a function like this. Rather, this is a function that Backbone provides as the object’s constructor. Its called any time an instance of the object type is created, and it is responsible for the over-all initialization of the type, including the call to the object’s initialize function that is typically seen in a Backbone object. When overriding the constructor function of a Backbone object, it’s important to call the prototype’s constructor function so that the initialize method and other necessary code will be called.

So why use the constructor function when the initialize function could be used instead? Isn’t this causing an undue burden on the developer, overriding it? The answer to that isn’t quite so simple. Yes, the developer writing this base view has to remember to call the prototype’s constructor. However, this is a far better situation than overriding the initialize function. Overriding that function would require every developer that extends from this base view to remember to call back to the prototype’s initialize function if they need to implement their own initialize.

If we override the constructor instead, only the base view that we are defining has to remember to call back to the prototype’s constructor function. Views that extend from this base view don’t have to call back to the prototype at all - for the constructor or for the initialize method. This means the views extending from BaseView have less to remember, reducing the over-all burden of implementation.

Secondly, the use of Object.getPrototypeOf in the buildTemplateCache function is a different way of getting the prototype. The constructor that we are accessing the prototype of Backbone.View directly, but the buildTemplateCache uses this function instead. In the constructor, we know that the prototype is Backbone.View so we can explicitly state that. Now it can be argued that we should just use Object.getPrototypeOf in the constructor function. But the use of it is necessary in the builtTemplateCache function because we don’t directly know the prototype of the object we are dealing with.

When the buildTemplateCache function is called, the context is set to the current instance of whatever view type we have defined. That is, any time you use this within the view’s methods, it will (most likely) be referencing the current view instance. If you set this.templateCache = … then it will always set the templateCache attribute on the view instance. This would defeat the purpose of a template cache, as each view instance would build it’s own cache. The call to Object.getPrototypeOf, then, gives us the prototype of the object instance. By using “proto.templateCache”, the cache is set on the prototype directly and all instances will be able to retrieve the precompiled template from the cache.

A getPrototypeOf Shim

There is one potential drawback to using Object.getPrototype - not every browser supports it. According to the Mozilla Developer Network, the current support for this method includes FireFix 3.5 and up, Chrome 5 and up, Internet Explorer 9 and up, and Safari 5 and up. Opera is not listed with support at all, and IE 8 and below do not support this. To get support for it, then, a shim has to be put in place. Fortunately, John Resig (the creator of jQuery) has one available via his blog:

1 if ( typeof Object.getPrototypeOf !== "function" ) {

2 if ( typeof "test".__proto__ === "object" ) {

3 Object.getPrototypeOf = function(object){

4 return object.__proto__;

5 };

6 } else {

7 Object.getPrototypeOf = function(object){

8 // May break if the constructor has been tampered with

9 return object.constructor.prototype;

10 };

11 }

12 }

This code needs to be included somewhere in the over-all project, and sent down to the browser before any call to Object.getPrototype can be called. Otherwise Opera and old versions of IE won’t be able to use this method call.

A check is made to see if the object’s prototype has a cached version of the template or not. If it is found, the function exits immediately. If it is not found, a compiled version of the template is created and stored. And lastly, the render function is updated to use the cached template instead of the raw template string.

With this in place, the view definitions that we have for blog posts, comments, or any other view that extends from BaseView, ModelView, or CollectionView, will receive the performance benefit of pre-compiled and cached templates. The big win, though, is that views extending from BaseViewdo not need to know anything about this optimization or handle it explicitly. They only need to specify a template in the view definition, as they have already been doing. The performance benefit was added to the base view and therefore immediately available to all other views.

For more information on prototypal inheritance and the this keyword in JavaScript, check out my screencasts at WatchMeCode.net. Episode #4 covers JavaScript’s context (this) keyword while Episode #5 covers prototypal inheritance.

Lessons Learned

Reducing boilerplate code is an important and common reason for creating abstractions and extracting common code. However, performance improvements, encapsulation and other reasons exist to create abstractions and re-usable components. Look for opportunities to simplify the overall framework and the use of it, and opportunities for performance improvements in the framework or plugin.

Performance: Do You Speak It?

Pre-compiled templates are a great example of a place that performance concerns create a need for more abstraction. Rather than having each view implementation be responsible for caching the template that it uses, having a base type implement the caching mechanism makes the cache and pre-compilation transparent.

Cross-Browser Compatibility And Shims

Of course, this isn’t just a plugin and add-on concern. JavaScript application development in general needs to be aware of cross-browser compatibility. Fortunately, there are several frameworks, guidelines and other resources for managing these issues. Using a framework like jQuery, for example, creates a higher likelihood that the functionality you need will be available in all of the browsers that you are supporting. Even still, there are times when additional shims and backward-compatibility implementations must be provided. Tools like Modernizr provide a lot of backward compatibility for modern features, but a quick search for specific methods such as Object.getPrototypeOf or Object.create will often surface individual implementations, reducing the need for something as large as Modernizr.