AboutBlogContact
Frontend EngineeringMarch 19, 2013 7 min read 158Updated: June 22, 2026

The Backbone.js and Knockout.js Era: Before React and Angular Took Over the World

AunimedaAunimeda
📋 Table of Contents

React was released at JSConf in May 2013. AngularJS 1.0 shipped in June 2012 but didn't gain mainstream traction until 2013. Before those frameworks dominated, teams building complex JavaScript applications had two serious options: Backbone.js (released October 2010) and Knockout.js (released July 2010).

We used both. They solved different problems, had different philosophies, and produced different codebases. Here's what daily development looked like in that window.


Backbone.js: Bring Your Own Structure

Jeremy Ashkenas (who also created CoffeeScript and Underscore.js) built Backbone on a single idea: JavaScript needed the minimal structure of MVC without a framework forcing opinions on you. Models, Collections, Views, and a Router - everything else you decided yourself.

A typical Backbone model:

// Backbone 0.9.x - Product model
var Product = Backbone.Model.extend({
  defaults: {
    name: '',
    price: 0,
    inStock: true,
    category: ''
  },
  
  urlRoot: '/api/products',
  
  validate: function(attrs) {
    if (!attrs.name || attrs.name.trim() === '') {
      return 'Name is required';
    }
    if (attrs.price < 0) {
      return 'Price cannot be negative';
    }
  },
  
  // Computed property - no framework magic, just a method
  formattedPrice: function() {
    return '$' + this.get('price').toFixed(2);
  }
});

// Creating, fetching, saving
var product = new Product({ id: 42 });

product.fetch({
  success: function(model, response) {
    console.log('Loaded:', model.get('name'));
  },
  error: function(model, response) {
    console.error('Fetch failed:', response.status);
  }
});

product.save({ price: 29.99 }, {
  success: function() { console.log('Saved'); },
  error: function() { console.error('Save failed'); }
});

A Backbone Collection wrapping an array of Products:

var ProductCollection = Backbone.Collection.extend({
  model: Product,
  url: '/api/products',
  
  // Custom filter method - no framework filter, just JavaScript
  byCategory: function(category) {
    return this.filter(function(product) {
      return product.get('category') === category;
    });
  },
  
  // Custom sort
  comparator: function(product) {
    return product.get('price');  // Sort by price ascending
  }
});

Backbone Views: The Hard Part

The View layer was where Backbone got painful. You were responsible for:

  • Rendering HTML
  • Binding events
  • Updating the DOM when the model changed
  • Cleaning up event listeners to prevent memory leaks
var ProductView = Backbone.View.extend({
  tagName: 'div',
  className: 'product-card',
  
  // Events hash - delegated to the view's root element
  events: {
    'click .btn-add-to-cart': 'onAddToCart',
    'click .btn-wishlist': 'onAddToWishlist',
    'change .quantity-input': 'onQuantityChange'
  },
  
  initialize: function() {
    // Listen to model changes - re-render when data changes
    this.listenTo(this.model, 'change', this.render);
    this.listenTo(this.model, 'destroy', this.remove);
  },
  
  render: function() {
    // Template - in 2012 this was typically Underscore's _.template
    var template = _.template(
      '<div class="product-image">' +
        '<img src="<%= imageUrl %>" alt="<%= name %>">' +
      '</div>' +
      '<div class="product-info">' +
        '<h3><%= name %></h3>' +
        '<p class="price"><%= formattedPrice() %></p>' +
        '<% if (inStock) { %>' +
          '<button class="btn-add-to-cart">Add to Cart</button>' +
        '<% } else { %>' +
          '<span class="out-of-stock">Out of Stock</span>' +
        '<% } %>' +
      '</div>'
    );
    
    this.$el.html(template(this.model.toJSON()));
    return this;  // Chaining convention
  },
  
  onAddToCart: function(e) {
    e.preventDefault();
    var quantity = parseInt(this.$('.quantity-input').val()) || 1;
    this.model.trigger('add-to-cart', this.model, quantity);
  },
  
  onAddToWishlist: function() {
    // Trigger event that parent view/router handles
    this.trigger('wishlist:add', this.model);
  },
  
  onQuantityChange: function(e) {
    var qty = parseInt(e.target.value);
    if (qty > 0) {
      this.model.set('selectedQuantity', qty);
    }
  },
  
  // CRITICAL: Remove event listeners to prevent memory leaks
  // Backbone's stopListening handles this if you use listenTo
  remove: function() {
    this.stopListening();
    Backbone.View.prototype.remove.call(this);
  }
});

The memory leak problem was real. A View that listened to model events via model.on('change', this.render, this) but didn't call model.off when removed from the DOM would keep the model retaining a reference to the View, and the View retained a reference to its DOM element - even after the element was removed from the page. listenTo (added in Backbone 0.9.9) tracked bindings and cleaned them up automatically. Before that, developers leaked memory routinely.


Knockout.js: Data-Binding Without MVC

Knockout took the opposite philosophy. Instead of providing structure, it provided two-way data binding via "observables":

// Knockout 2.x - same product example
function ProductViewModel(data) {
  var self = this;
  
  // Observables - plain JS values wrapped in KO's observable function
  self.name = ko.observable(data.name || '');
  self.price = ko.observable(data.price || 0);
  self.inStock = ko.observable(data.inStock !== false);
  self.selectedQuantity = ko.observable(1);
  
  // Computed observable - automatically recalculates when dependencies change
  self.formattedPrice = ko.computed(function() {
    return '$' + self.price().toFixed(2);
  });
  
  self.totalPrice = ko.computed(function() {
    return self.price() * self.selectedQuantity();
  });
  
  // Actions
  self.addToCart = function() {
    // Cart logic here
    cart.add(self, self.selectedQuantity());
  };
  
  self.addToWishlist = function() {
    wishlist.add(ko.toJS(self));  // ko.toJS strips observables to plain object
  };
}

// Binding to a specific DOM element
var vm = new ProductViewModel(productData);
ko.applyBindings(vm, document.getElementById('product-container'));
<!-- HTML data-bind attributes -->
<div id="product-container">
  <h3 data-bind="text: name"></h3>
  <p class="price" data-bind="text: formattedPrice"></p>
  
  <input 
    type="number" 
    data-bind="value: selectedQuantity, valueUpdate: 'input'"
    min="1"
  >
  <p data-bind="text: 'Total: $' + totalPrice().toFixed(2)"></p>
  
  <!-- ko if: inStock() -->
    <button data-bind="click: addToCart">Add to Cart</button>
  <!-- /ko -->
  <!-- ko ifnot: inStock() -->
    <span>Out of Stock</span>
  <!-- /ko -->
</div>

When selectedQuantity changed, totalPrice automatically recalculated, and the DOM automatically updated. No manual DOM manipulation. No event listeners to manage. The data-bind attribute tied the UI to the ViewModel.


The Key Differences

Feature Backbone.js Knockout.js
Philosophy Minimal structure, maximum flexibility Data-binding magic
DOM updates Manual, in render() Automatic via observables
Learning curve Low (it's just JavaScript) Medium (observables are a new concept)
Boilerplate High Low
Debugging Easy (standard JS debugging) Hard (observable dependency tracking)
Size 6.4KB 40KB
Best for Complex, custom UIs Form-heavy, CRUD UIs

What We Got Wrong

Backbone: We didn't enforce component boundaries. Views grew to 400 lines. Models accumulated business logic. The flexibility that made Backbone easy to start with made it hard to maintain at scale. Every project had a unique architecture.

Knockout: Observable chains were hard to debug. A computed observable that depended on 5 other observables that each depended on 3 more created dependency graphs nobody could hold in their head. Silent failures - an observable not updating because a dependency wasn't tracked - were the hardest bugs to find.

Both frameworks suffered from the absence of component thinking. React's component model - a piece of UI that owns its state and re-renders in response to state changes - was the right abstraction. Neither Backbone Views nor Knockout ViewModels were reusable the way React components are.


The Legacy

Angular 1.x arrived in 2012 with dirty-checking: a $digest cycle that re-evaluated all watched expressions on every event. It was slower than Knockout's observable tracking and harder to reason about, but it came with a complete framework opinion (controllers, services, directives, dependency injection) that teams with unclear architecture needed.

React arrived in 2013 with the virtual DOM and uni-directional data flow. It made Backbone's manual DOM management and Knockout's observable magic look like workarounds for the same underlying problem: mutating the DOM is hard to reason about. React's answer - don't mutate the DOM, describe what it should look like, let the framework figure out the diff - was the right answer.

By 2015 we were writing everything in React. By 2016 we were migrating old Backbone codebases to React. By 2018 we'd rewritten every significant Knockout project.

But Backbone taught us structure and event-driven architecture. Knockout taught us reactivity and declarative UI. The concepts survived even as the implementations were retired.


Aunimeda builds modern web frontends - from single-page applications to complex multi-locale sites.

Contact us to discuss your frontend project. See also: Web Development, Corporate Website Development

Read Also

React vs Angular 2: Why We Chose React for Our SaaS CRM in 2016aunimeda
Frontend Engineering

React vs Angular 2: Why We Chose React for Our SaaS CRM in 2016

After evaluating both for a complex CRM build, we chose React + Redux. The honest comparison: two-way binding vs unidirectional data flow, component philosophy, and what actually mattered at scale.

How to Set Up React with Webpack 1.x Without Create React App (2015)aunimeda
Frontend Engineering

How to Set Up React with Webpack 1.x Without Create React App (2015)

In 2015, Create React App didn't exist. Setting up React meant manually configuring Webpack 1.x, Babel 5, and Hot Module Replacement. Here's the exact working config we used in production - and why each piece was necessary.

Early Single Page Applications: Managing State Before Redux Existedaunimeda
Frontend Engineering

Early Single Page Applications: Managing State Before Redux Existed

Redux was released in 2015. Before it, we managed SPA state with custom event buses, URL hashing, localStorage, and prayer. Here's what state management actually looked like in 2012-2014.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Web Development

Get Consultation All articles