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