June 14th, 2012

Building The Next SoundCloud

This article is also available in Serbo-Croatian: Pravljenje novog SoundCloud.

The front-end team at SoundCloud has been building upon our experiences with the HTML5 widget to make the recently-released Next SoundCloud beta as solid as possible. Part of any learning also includes sharing your experiences, so here we outline the front-end architecture of the new site.

Building a single-page application

One of the core features of Next SoundCloud is continuous playback, which allows users to start listening to a sound and continue exploring without ever breaking the experience. Since this really encourages lots of navigation around the site, we also wanted to make that as fast and smooth as possible. These two factors were enough in themselves for us to decide to build Next as a single-page Javascript application. Data is drawn from our public API, but all rendering and navigation happens in the browser for near-instant navigation without the need to make round-trip requests to the server.

As a basis for this style of application, we have used the massively popular Backbone.js. What attracted us to Backbone (apart from the fact that we’re already using it for our Mobile site and the Widget), is that it doesn’t prescribe too much about how it should be used. Backbone still provides a solid basis for working with views, data models and collections, but leaves lots of questions unanswered, and this is where its flexibility and strength really lies.

For rendering the views on the front end, we use the Handlebars templating system. We evaluated several other templating engines, but settled on Handlebars for a few reasons:

  • No logic is performed inside the templates, which enforces good separation of concerns.
  • It can be precompiled before deployment which results both in faster rendering and a smaller payload that needs to be sent to clients (the runtime library is only 3.3kb even before gzip).
  • It allows for custom helpers to be defined.

Modular code

One technique we used with the Widget which ended up being a great success was to write our code in modules and declare all dependencies explicitly.

When we write code, we write in CommonJS-style modules which are converted to AMD modules when they’re executed in the browser. There are some reasons we decided to have this conversion step, possibly best explained by seeing what each style looks like:

// CommonJS module ////////////////
var View = require('lib/view'),
    Sound = require('models/sound'),
    MyView;

MyView = module.exports = View.extend({
    // ...
});

// Equivalent AMD module //////////
define(['require', 'exports', 'module', 'lib/view', 'models/sound'],
  function () {
    var View = require('lib/view'),
        Sound = require('models/sound'),
        MyView;

    MyView = module.exports = View.extend({
        // ...
    });
  }
);
  • The extra define boilerplate is tedious to write
  • Duplication of module dependencies is also tedious and error-prone
  • Conversion from CommonJS to AMD is easily automated, so why not?

During local development, we convert to AMD modules on-the-fly and use RequireJS to load them individually. This makes development quite frictionless as we can just save and refresh to see the updates, however it’s not so great for production since this method creates hundreds of HTTP requests. Instead, the modules are concatenated into several packages, and we drop RequireJS for the super-lightweight module loader AlmondJS.

CSS and Templates as dependencies

Since we’re already including all of our code by defining explicit dependencies, we thought it made sense to also include the CSS and template for a view in the same way. Doing this for templates was rather straight-forward since Handlebars compiles the templates into a Javascript function anyway. For the CSS, it was a new paradigm:

Views define which CSS files are needed for it to display properly, just the same as you would define the javascript modules you need to execute. Only when the view is rendered do we insert its CSS to the page. Of course, there are some common global styles, but mostly, each view has its own small CSS file which just defines the styles for that view.

When writing the code, we write in plain vanilla CSS (without help of preprocessors such as SCSS or LESS), but since they are being included by Require/Almond they need to be converted into modules as well. This is done with a build step which wraps the styles into a function which returns a <style> DOM element. Here’s an example of how it looks in essence:

Input is plain CSS

.myView {
  padding: 5px;
  color: #f0f;
}
.myView__foo {
  border: 1px solid #0f0;
}

Result is an AMD module

define("views/myView.css", [...], function (...) {
  var style = module.exports = document.createElement('style');
  style.appendChild(
    document.createTextNode('.myView { padding: 5px; color ... }');
  );
})

Views as components

A central concept in developing Next is that of treating views as independent and reusable components. Each view can include other ‘sub’ views, which can themselves include subviews and so on. The effect of this is that some views are merely composites of other views and cover an entire page, whereas others can be as small as a button, or even a label in some cases.

Keeping these views independent is very important. Each view is responsible for its own setup, events, data, and clean up. Views are ‘banned’ from modifying the behaviour or appearance of their subviews, or even making assumptions about how or where this view itself is being included. By removing these external factors, it means that each view can be included in any context with absolute minimum fuss, and we can be sure that it will work as it is supposed to.

As an example, the ‘play’ button on Next is a view. To include one anywhere on the site, all that we need to do is create an instance of the button, and tell it the id of the sound it should play. Everything else is handled internally by the button itself.

To actually create these subviews, most of the time they are created inside the template of the parent view. This is done by use of a custom Handlebars helper. Here is a snippet from a template which uses the view helper:

<div class="listenNetwork__creator">
  {{view "views/user/user-badge" resource_id=user.id}}
</div>

As you can see, adding a subview is as simple as specifying the module name of the view and passing some minimal instantiation variables. What actually happens behind the scenes goes like this:

When a view is rendered, the template must return a string. When the view helper is invoked, it pushes the attributes passed to it, plus a reference to the requested view class, into a temporary object with an id, and outputs a placeholder element (we use <view data-id="xxxx">). The id is just a unique, incrementing number. After a template is rendered, the output would be a string which might look something like:

<div class="foo">
  <view data-id="123"></view>
</div>

Then we find the placeholders and replace those elements with the subview’s element which it automatically creates for itself. In essence, the code does this:

parentView.$('view').each(function () {
  var id = this.getAttribute('data-id'),
      attrs = theTemporaryObject[id],
      SubView = attrs.ViewClass,
      subView = new SubView(attrs);
  subView.render(); // repeat the process again
  $(this).replaceWith(subView.el);
});

Sharing Models between Views

So, we now have a system where there will be dozens of views on the screen at one time, many of which will be views of the same model. Take, for example, a “listen” page:

There would be a view for the play button, the title of the sound, the waveform, the time since the sound was uploaded (this dynamically updates itself, which is why it is a view), and so on. Each of these views are of the same sound model, but we wouldn’t want each to duplicate the data. Instead we need to find a way to share the model.

Also remember that each of these views has to handle the case where there is no data yet. Almost all views are instantiated only with the id of its model, so it’s quite possible that the data for that model hasn’t been loaded yet.

To solve this, we use a construct we call the instance store. This store is an object which is implicitly accessed and modified each time a constructor for a model is called. When a model is constructed for the first time, it injects itself into the store, using its id as a unique key. If the same model constructor is called with the same id, then the original instance is returned.

var s1 = new Sound({id: 123}),
    s2 = new Sound({id: 123});

s1 === s2; // true, these are the exact same object.

This works because of a surprisingly little-known feature of Javascript. If a constructor returns an object, then that is the value assigned. Therefore, if we return a reference to the instance created earlier, we get the desired behaviour. Behind the scenes, the constructor is basically doing this:

var store = {};

function Sound(attributes) {
    var id = attributes.id;

    // check if this model has already been created
    if (store[id]) {
        // if yes, return that
        return store[id];
    }
    // otherwise, store this instance
    store[id] = this;
}

This is not a particularly new pattern: it’s simply just the Factory Method Pattern wrapped up into the constructor. It could have been written as Sound.create({id: 123}), but since Javascript gives us this expressive ability, it makes sense to use it.

So, this feature means that it’s completely simple for views to share the same instance of a model without knowing anything about the other views, simply by calling the constructor with a single id. We can then use this shared instance as a localised ‘event bus’ to facilitate communication and synchronisation of the views. Usually this is in the form of listening to changes to the model’s data. If the views subscribe to the ‘change’ events which affect it, then they will be notified immediately upon change and the page can be kept up-to-date with very little effort required by the developer.

This also is how we solve the issue of there being no data on the model. On the first pass, several views might have a reference to a model which only contains an id and no other attributes. When the first view is rendered, it can detect that the view does not have enough information and so it would ask the model to fetch its data from the API. The model keeps track of this request, so that when the other views also ask it to fetch, we do nothing and avoid duplicate requests. When the data comes back from the server, the attributes of the model will be updated, causing ‘change’ events which then notify all the views.

Making full use of data

A common feature of many APIs is that when one particular resource is requested, other related resources are included in the response. For example, on SoundCloud, when you request information about a sound, included in the response is a representation of the user who created that sound.

/* api.soundcloud.com/tracks/49931 */
{
  "id": 49931,
  "title": "Hobnotropic",
  ...
  "user": {
    "id": 1433,
    "permalink": "matas",
    "username": "matas",
    "avatar_url": "http://i1.soundc..."
  }
}

Rather than let this extra data go to waste, each model is aware of which ‘sub-resources’ it can expect in its responses. These sub-resources are inserted into the instance store in case any views need to use the data. This means we can save a lot of extra trips to the API and display the views much faster.

So, for our example above, the Sound model would know that in its property “user” it has a representation of a User model. When that data is fetched, then two models are created and populated on the client side:

var sound = new Sound({id: 49931 });

sound
  .fetch()             // get the data
  .done(function () {  // and when it's done
    var user = new User({id: sound.user.id });
    user.get('username'); // 'matas' -- we already have the model data
  });

What’s important to remember is that because there’s only ever one instance of each model, even pre-existing instances are updated. Here’s the same example from above, but note when the User is created.

var sound = new Sound({id: 49931 }),
    user = new User({id: 1433 });

user.get('username'); // undefined -- we haven't fetched anything yet.
sound
  .fetch()
  .done(function () {
    user.get('username'); // 'matas' -- the previous instance is updated
  });

Letting go

Holding on to every instance of every model forever isn’t feasible, especially for the Next SoundCloud. Because of the nature of the site, it’s quite possible that a user might go for several hours without ever performing a page load. During this time, the memory consumption of the application would just continue to grow and grow. Therefore, at some point we need to flush some models out of the instance store. To decide when it is safe to do this, the instance store increments a usage counter each time an instance has been requested, and views can ‘release’ a model when it is no longer needed and the count is decremented.

Periodically, we check the store to see if there are any models with a count of zero, and they’re purged from the store, allowing the browser’s garbage collector to free up the memory. This usage count is encapsulated in the store object, but in essence it’s something like this:

var store = {},
    counts = {};

function Sound(attributes) {
  var id = attributes.id;
  if (store[id]) {
    counts[id]++;
    return store[id];
  }
  store[id] = this;
  counts[id] = 1;
}

Sound.prototype.release = function () {
  counts[this.id]--;
}

The reason for performing the cleanup on a timer, rather than whenever a usage count hits zero is so that the model stays in the store when you switch views. If you navigate to another page, there will be a single moment between cleaning up the existing views and setting up the new ones when every single model’s count is zero. The new page might actually contain views of one or more of these models, so it’d be quite wasteful to remove them instantly.

A long journey…

This has been a brief introduction into some of the methods and concepts we’re using to create Next SoundCloud, but it’s just the beginning. There are plenty more features which we have yet to build and therefore plenty more challenges to tackle. If you want join us along the way, remember that we’re always hiring!

Nick Fisher
  • http://www.twitter.com/simondlr Simon de la Rouviere

    Hi. On a related note, is there somewhere where we can check if soundcloud has received our feedback on the new version? Some parts are buggy, and some parts I feel has dropped in terms of UX. I have submitted some feedback, but would like to know if you guys are looking at it!

  • http://www.sridharsmusic.com Sridhar

    congrats, and well done team.

  • James Welch

    Hey there, Jami from the community team here. I’m personally monitoring all the feedback that comes through the form daily, summarising it, and passing it on to the Next development team. 

    Because of the very large volume of feedback, we’re not able to respond to everything, but rest assured, all bug reports are being fast tracked to our developers.

    Thanks for your help in making Next SoundCloud better!

  • http://www.thebangpop.com/ @nataliekbeats

    Great stuff guys! 

    Cheers!

  • http://fatkidonfire.com/ FatKidOnFire

    Loving Next SC guys, looks (and uses) beautiful. Once the bugs are sorted out it’s going to be an amazing step up – great work! 

  • devmach

    > Making full use of data

    >sound
    >  .fetch()             // get the data
    >  .done(function () {  // and when it’s done
    >    var user = new User({id: sound.user.id });
    >    user.get(‘username’); // ‘matas’ — we already have the model data
    >  });

    Where do you assign user models’ data from sound model/ api response ?

    Shouldn’t be something below more correct ?

      var user = new User( sound.user );

  • http://twitter.com/micheldegraaf Michel de Graaf

    Awesome work guys!  And  thanks for the writeup!

  • http://twitter.com/a_kovalev Alexander Kovalev

    Every model can define the mapping of response property to sub resource type.

    var Sound = Model.extend({
      …
      submodelMap: {    user: UserModel  }
      …
    });

    It means that when sound details are fetched from API the instance of UserModel is created and populated with the corresponding data. That’s why we can retrieve `username` property in the code snippet you’ve quoted

  • devmach

    Aha ! Now it’s clear. Thank you :)

  • http://www.twitter.com/simondlr Simon de la Rouviere

    Thanks James!

  • Jeff

    Nice stuff.  Good job!

  • Anonymous

    Great stuff guys! Just curious, will you open source the conversion from commonjs-to-AMD?

    Thanks!

  • http://thomasdavis.github.com/ Thomas Davis

    This blog post contains many break throughs for single page web application design that will undoubtedly become standards. I look forward to implementing and expanding on these concepts myself! Keep it up and sleep well!

  • http://www.facebook.com/profile.php?id=1381376151 Vitor Balocco

    Thank you so much for this awesome writeup. I’m working on a Backbone app right now which can certainly benefit from most of these ideas.

    You guys must be having a kick-ass time writing the frontend of Next SoundCloud!

  • Anonymous

    So you guys built a sort of ref-count garbage collector for the app? (Letting Go)

  • http://twitter.com/a_kovalev Alexander Kovalev

    yes, the technique of holding/releasing data resource object (actually it can be applied to any object)  can be considered as reference counting garbage collection

  • Rob

    Hey, good stuff, 1 question: when you populate model out of
    “user”: {
    “id”: 1433,
    “permalink”: “matas”,
    “username”: “matas”,
    “avatar_url”: “http://i1.soundc…”
    }how do you go about when you wan to edit and put this stuff back to the model, backbone does not deal with nested models out of the box. thx

  • http://jamie-wong.com/ Jamie Wong

    Any chance you’ll open source the build system? I’m particularly interested in the auto conversion of CommonJS -> RequireJS for development and CommonJS -> AlmondJS for prod.

  • http://twitter.com/a_kovalev Alexander Kovalev

    Probably. The thing is that the code responsible for this CommonJS-to-AMD conversion is distributed among development server and build scripts which doesn’t make it very easy to open source right now.

  • http://twitter.com/plentz Diego Plentz
  • http://twitter.com/a_kovalev Alexander Kovalev

    Once the submodel is created it has nothing to do with the parent one. All subsequent changes of this submodel are not reflected in the attributes of the initial (parent) model. We assume that if model contains any properties representing another entity the separate model for this subresource should be created.

    I’m not sure if it answers your question but the main point here is that we consider the properties which can be mapped to submodels as an excessive data. And we just use this excessive data in order to save a lot of extra trips to the API.

  • http://twitter.com/a_kovalev Alexander Kovalev

    ??? 
    Do you wanna say that it was a wrong decision to build next SoundCloud on top of our API?  :)

  • Thomas

    Read it with a lot of excitement and got new insights on Backbone and views. Looking forward for future posts about your development. Thanks for the write up it was a pleasure to read!

  • http://twitter.com/reconbot reconbot

    I’d love to see the code behind the sub model creation. How do you guys handle collections? Sharing models is easy but they either have to live in arrays or you have to extend/modify how collections work to have a model in more then one.

  • Florian

    Please bring back the view/number of how many downloads a track already has.

  • Rob

     yep, it makes sense to create new instance for each nested model and deal with it separately. thx for catchup

  • http://twitter.com/a_kovalev Alexander Kovalev

    As I’ve already said in one of the comments below this singletone-ish pattern can be applied to any object you want. All you need to define is a custom hashing function. This is the simplified code snippet illustrating what I’m trying to say

    var Comments = Collection.extend({}, {
      hashFn: function(commentModels, options) {
        return (options && options.sound_id) || null;
      }
    });

    var c1 = new Comments([], { sound_id: 123 });
    var c2 = new Comments([ comment1, comment2 ], { sound_id: 123 });

    console.log(c1 === c2, c1.length, c2.length) // -> true, 2, 2

  • Davis

    I think he’s trying to say something akin to Twitter built their front-end out in heavy JS, and now they are abandoning that for server side rendering…which is a completely moot point — it is about as relevant here as the price of tea in China.

    You’re doing a fine job.  Thanks for posting these articles — they are fun to read.

    Take a look at http://socketstream.com when you have a chance.  I think it would be very useful for you to incorporate real-time features into your app with that.  I’m building out a socketstream app right now with backbone and it is a lot of fun.

  • http://www.roborooter.com/ Francis

    I see the latest backbone.js lets you add models to multiple collections (it’s not clear that all events bubble up to all collections), but I don’t see when your hashFn is used. (Collections don’t know anything about that.).

  • Brenton Partridge

    Browserify doesn’t do exactly the same thing, but it does package CommonJS for the browser and provides an implementation of `require` – I’d recommend checking that out in the meantime.

  • http://foxinni.com/ Foxinni

    So incredibly solid. Miles ahead of the old SC. Well done.

  • Nick Fisher

    That’s something I’d definitely like to do. Right now, the build scripts contain a bit too many hardcoded shortcuts and assumptions about the application structure/deployment methods to be very useful to anyone else. Refactoring this is on my wishlist. In the meantime, check out Grunt. I’ve not used it, but have heard plenty of nice things about it. It doesn’t have a tool to do the conversion, but perhaps there’s a plugin available?

    https://github.com/cowboy/grunt

  • Nick Fisher

    That’s on the roadmap and should be done in the upcoming weeks. Next SoundCloud hasn’t yet implemented even close to all the features we’d like, but if you are particularly missing some (like this), do use the feedback form to let us know — it’ll help push that feature forward on the roadmap.

  • Dave Stibrany

    Is this something that is built in to Backbone, or something that you implemented? 

  • Anonymous

    Thanks for that!

  • Anonymous

    Understood no problem at all. I always disliked the style of AMD and found it too verbose, so you hit a chord when you mentioned what you guys did :-)

  • Nick Fisher

    The hash function is used to determine the uniqueness of a model. In most cases, it’s just `return attributes.id`, but not always. The function is executed during the construction of a model or collection, and if another object exists which had the same hash, then they are considered to be the same.

    The collections call `new Model(modelData)` when they prepare the data, and so (unbeknownst to them) they get a preexisting model (and also update that model’s data!).

    https://github.com/documentcloud/backbone/blob/master/backbone.js#L820-830

    How this works with models in multiple collections is also unclear to me, but is not really a concern because of the independent views. A list view itself would not need to know about the attributes of the models in the list (since a different view would be rendering that), and so listening to the bubbled events isn’t required.

  • http://twitter.com/a_kovalev Alexander Kovalev

    We already use WebSocket for real time notifications about user’s stream activity. With regard to SocketStream — we aware of this frameworks, it looks really very interesting but by the moment we started working on Next it didn’t exist and i’m not sure that we want to use WebSocket as a transport for API communication. As far as I know SocketStream is using WebSocket instead of traditional ajax requests. 

  • http://twitter.com/doomhz Dumitru Glavan

    BackboneJS FTW. We have almost the same development concepts for our SPA, the only difference is that we didn’t switch to AMD yet. We still prefer to keep our scripts concatenated in one big, compressed file and serve it in one request.
    Btw, nice work ;)

  • http://twitter.com/a_kovalev Alexander Kovalev

    In production we also concatenate all commonjs modules into 4 different files according to how often the source changes (to work best with caching)

  • http://tbranyen.com/ Tim Branyen

    It seems very odd to me that you would do a translation process from CommonJS style modules to AMD.  Especially because your reasoning is purely stylistic.  Your example of how, for lack of a better word, bloated AMD syntax is very much non-normative.  You picked a very extreme case of how AMD can represent a module.  Below is a much more realistic example that is even more compact than your CommonJS example and achieves the exact same result:

    // Equivalent AMD module //////////
    define(['lib/view', 'models/sound'], function (View, Sound) {
      return View.extend({
          // ...
      });
    );

  • http://twitter.com/a_kovalev Alexander Kovalev

    It’s not built in to Backbone.
    We extended Backnone.Model and Backbone.Collection as well as the way how they are instantiated and being fetched

  • http://twitter.com/a_kovalev Alexander Kovalev

    But even in your “much more realistic example” you have to preserve the order of required modules. When you need to require couple of modules it works pretty well but when it’s more it’s quite hard to maintain and read this double declaration of dependencies. CommonJS to AMD conversion is a very easily automated process saving from extra typing and which doesn’t leave a chance of making a mistake.

    And the second point here is that using of  CommonJS-style modules allows to reuse the given module in both server and client side. For example, it easily allows you to reuse route configuration file if you want to introduce server side prerendering or sending back to client data needed to render the view

  • Davis

    Cool.  I can understand the hesitancy to use websockets for your API — if you have to support non-websocket clients then it can be trouble, but I’ve found it to be really easy and efficient to use, and I’m building a single client.  SocketStream also handles session state for both HTTP and WebSockets.   Anyway, I mention SocketStream, b/c it also does a lot of the stuff you built in here with uglify.js and browserify.js, etc.

    Looking forward to more posts.

  • http://philfreo.com/ Phil Freo

    You guys should really open source some of this stuff — especially your Model implementation that handles submodels (ideally also the instance store portion)

  • http://tbranyen.com/ Tim Branyen

    These are valid points and I don’t want to discount your motives for your build decision.  Granted I could come back and say that re-ordering is a discipline and that you rarely re-order when working with modules unlike working with variables and that AMD works just fine on server-side environments (through something like RequireJS).

    However, I can completely agree that CommonJS is more conducive to Node.js (specifically) shared development.  In the post it’s mentioned:

    “During local development, we convert to AMD modules on-the-fly and use RequireJS to load them individually.”

    How exactly is this working? Do you have a continuous watch task that is running?

  • Ross Boucher

    Reminds me a little of this adventure: http://cappuccino.org/discuss/2010/04/01/solving-the-javascript-memory-management-problem/

    Though, in all seriousness, reference counting is about the best you can do here, until something like WeakMap becomes widely available.

  • http://twitter.com/lmjabreu Luis Abreu

    Lovely writeup, the CommonJS thingie is odd indeed but it’s up to you.

    I’m surprised no one mentioned how sluggishly the page loads, not because of the approach used ( love, hope people don’t invoke twitter’s awful attempt at it as an explanation ), but because of the asset loading.

    I got 640reqs, 23secs, 1.200kb for the frontpage, no feedback while the other pages load which is confusing.

    For the frontpage: the individual players seem to be VERY taxing on performance, perhaps because the bulk of the requests are user avatars, possible solution: generate a sprite per track, position as background image using bg offsets, use either a map between x position and user, rely on array order to auto calculate the position based on width, wtv.

    Optimizing those sprites would yeld huge size gains as well.

    ( had a similar problem once, needed to load thousands of avatars to compose a mosaic, ended up generating a sprite and fetching ‘alphas’ after initial page load, guess something similar could be done to provide updates to comments )

    Either way, something needs to be done regarding those avatars. ( CDN feels quite slow as well )

    The feedback thingie would be cool.

    Anyway: awesome =)

  • http://twitter.com/a_kovalev Alexander Kovalev

    No, we have development server written in Node.js.
    That’s how it looks in a very simplified form:

     app.get(/.js$/ function(request, response) { 
    deliverCommonJS(request, response); 
    });

    function deliverCommonJS(request, response) { 
    // convert CommonJS-style file to AMD 
    var reqPath = CommonJS.pathToRequire(request.url); 
    var body = CommonJS.convert(reqPath); 
    response.writeHead(200, { 
    ‘content-length’: Buffer.byteLength(body, ‘utf-8′), 
    ‘content-type’: type 
    });
    response.end(body);
    }

  • http://gregorynicholas.com/ Gregory Nicholas

    ever think to use r.js to precompile require.js ?

  • Émile Cantin

    Even shorter is the ‘Simplified CommonJS wrapper’ (http://requirejs.org/docs/api.html#cjsmodule) which look like this:
    // Equivalent AMD module //////////
    define(function (require) {
      View = require(‘lib/view’);
      Sound = require(‘models/sound’);
      return View.extend({      // …  });); 

    In coffeescript it’s even shorter (one line and an indentation level):
    # Equivalent AMD moduledefine (require) ->  View = require ‘lib/view’  Sound = require ‘models/sound’  class MyModule extends View      # …
    The best part is that this syntax is compatible with server-side Node.js if you use https://github.com/ajaxorg/node-amd-loader

  • Salman Abbas

    Good stuff! Soundcloud Rep++

  • http://twitter.com/kidkarolis Karolis Narkevičius

    Check out Cajon loader. It can load and build CommonJS/node and AMD modules.
    http://tagneto.blogspot.co.uk/2012/06/cajon-browser-module-loader-for.html

  • http://twitter.com/kidkarolis Karolis Narkevičius

    did you see http://tagneto.blogspot.co.uk/2012/06/cajon-browser-module-loader-for.html?

  • http://www.roborooter.com/ Francis

    Do you expose a javascript api? I’d love to build a set of bookmarklets that let me do fun things like skip back 30s, indicate where on the timeline a comment I’m hovering over lies, and things like that.

  • http://www.facebook.com/wiener Logan Aube

    How are you guys handling search engine indexing? I can see you’re not doing an initial render when you go to a specific user/track page, but are you giving out indexable content to specific bot useragents?

  • James Burke

     Some notes that can help the modular approach:

    1) The RequireJS r.js optimizer 2.0.2 has some new prefs that would allow you to generate an AMD built file straight from CommonJS modules (cjsTranslate), and do it with sourceURL (useSourceUrl) so that you can still get individual (turn off sourceURL for production builds/deployment though so you get minification benefits):

    https://github.com/jrburke/r.js/blob/master/build/example.build.js#L412

    You could use those to set up a node builder that has a watch on the dev directory to do the work as you save files.

    2) As Karolis mentioned below, the Cajon loader allows loading CommonJS-formatted modules on demand, and compiles them down to AMD.

    3) There is a sugared form of AMD that addresses some concerns about using AMD from the start, so that it looks more like a CommonJS module, just with a thin define() wrapper:

    http://requirejs.org/docs/whyamd.html#sugar

    4) Finally, be sure to do a build with the r.js optimizer before deployment. The benefit of a standard module format and AMD’s wrap format in particular is that you can easily get perf gains by doing a quick build pass to combine modules together.

  • http://twitter.com/marco_poLOL Marco Munizaga

    Nice, it is really refreshing to see projects that move javascript into a clean and organized implementation, rather than a rash “whatever works” implementation.

  • http://jamie-wong.com/ Jamie Wong

    Cool – didn’t know about the sugar syntax – thanks!

  • Andrós

    What about SEO or search engines in general? Search engines still suck in indexing JS site’s.

  • http://twitter.com/a_kovalev Alexander Kovalev

    Luis thank you for the feedback.
    We are aware of the problem caused by hundreds of request for avatar images and are working on solution to generate JSON representation of sound comments’ avatars. It should improve the current situation significantly.

  • Ashenafi

    Thank you for sharing your patterns and experience.

    Having each view is responsible for its own setup, events, data, and clean up is good.
    But how do you handle communication between different view. For example, when you click on a play button(one view), you change the play to pause button and also update the duration counter(another view). How do you notify the counter view of an event that occurred in the play view.

  • http://twitter.com/a_kovalev Alexander Kovalev

    New soundcloud app doesn’t expose any API but our widget does
    http://developers.soundcloud.com/docs/html5-widget

  • http://twitter.com/a_kovalev Alexander Kovalev

    That’s where this singletone-ish pattern becomes very helpful.
    In the case you described play button and duration counter views will share the same instance of Sound model which means that when the sound is for instance started/paused this change of sound state is reflected in all views based on the same sound instance.

  • http://twitter.com/a_kovalev Alexander Kovalev

    James, as far as I remember by the moment we started working on new version of SoundCloud RequireJS didn’t support `cjsTranslate` option. That’s why we decided to come up with our own home grown solution.
    Our build script is capable of defining all dependencies recursively, separate the whole application code into 4 different files according to how often the source changes (to work best with caching). 

    We also wanted to try out so called Lazy Evalutation of CommonJs Modules, a technique proposed by Tobie Langel (http://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/). Hence we modified AlmondJS a little bit to make it compatible with this.

    Anyway,thanks for RequireJS itself and for this detailed answer and recommendations on how requirejs should be used properly.

  • http://twitter.com/a_kovalev Alexander Kovalev

    At the moment we do nothing, but we’re already working on this.
    The cheapest and the fastest way to do it is to try to render the page using PhantomJS. Later on we will try to send prefetched API data as part of HTML page in order to increase the speed of initial rendering. I don’t want to go deeper into details since it’s a massive topic :)

  • http://www.facebook.com/profile.php?id=1381376151 Vitor Balocco

    How do you prevent the instance store from thinking two instances are the same if they are from two different models (say, a User model instance with id 13 and a Song model instance with id also 13), but with the same id?
    Do you have an instance store per model? or do you have a hash function which takes in account more than just the object id?

  • Ashenafi

    ya, that works in the case where these views share the same model. How do you manage the interaction when the two view don’t share the same model?

  • http://twitter.com/anthonyshort Anthony Short

    I’m assuming some type of event emitter/pubsub

  • http://twitter.com/a_kovalev Alexander Kovalev

    Exactly

  • http://twitter.com/anthonyshort Anthony Short

    The model store inside the constructor is an interesting way of doing thing. I also really like the sub-model map. 

    In an app I’m building I solved the shared model issue by having a global collection that is filled with data on load from the HTML. Every other collection creates a subset from this collection:

          storage = new Collection()
          myOtherCollection = storage.subset() 

    That way every collection shares the same model. Adding a model to a subset pushes it up to the parent which then pushes it down to other subsets. Fetching a collection pushes all models up.

    I really like some of the ways you have solved the same issues I’ve had. I’d love to see some of this code to see how you’ve implemented it. Especially the model store and the releasing of models from the store. 

    Thanks for the article, the community really needs more articles like this so we can share knowledge. Nice work!

  • http://twitter.com/a_kovalev Alexander Kovalev

    Each model/collection or any other object singleton pattern is applied to has a separate store. Hash function is given the same arguments as constructor so it’s quite easy to redefine it so that it returns not just the object id.

  • Anonymous

    Slightly random question: how did you achieve the movement of  as it relates to the scroll position? I guess what I’m wondering is how whatever event listener is checking for scroll() knows when to pass a parameter to the player view.

    Thanks! Really inspiring stuff here, as well as an inspiring design!

  • http://www.facebook.com/wiener Logan Aube

    Ahah yeah, it’s something I’ve been thinking about as well – thanks for the insight!

  • Dave Stibrany

    +1

  • Pingback: Aktuelles 15. Juni 2012

  • Nick Fisher

    I’m not totally sure which movement you’re talking about here. Scrolling the current player into view?

  • Nick Fisher

    Hey Anthony, that sounds like a really interesting approach. Are the subsets formed with some sort of filter method?

    When a model is “released” it merely reduces the usage counter. When we remove models from the store it’s as simple as finding those with usage count of zero and calling `delete store[id]`.

    Given the great response so far, I think we’ll definitely be writing more about our processes too!

  • Nick Fisher

    Yes, that’s a problem we’re yet to solve. As it stands right now, Googlebot would see an empty page when visiting Next — this is ok for now, since it’s still a private beta, but of course we will need to address it in the next few months. Currently, we’re experimenting with some level of server-side rendering, either by executing some of our code in NodeJS, or by running it through a headless browser and capturing the result.

    The short answer is that we haven’t forgotten about it, but we don’t know yet.

  • Nick Fisher

    Cool idea though!

  • http://openid.anonymity.com/a3cyni gwt-fan

    Nice writeup. I just learned about handlebarsjs. I wonder if you ever considered using GWT. From my experiences, lots of the optimizations you have done are already includeded out of the box (compressing the js, inlining css, even inlining image).

  • Nick Fisher

    Just read about it then. Given that we have automatic conversion to AMD modules, it doesn’t seem to offer much for us. Might be very useful in new projects wanting to stick to CommonJS modules though (which I highly recommend).

  • Nick Fisher

    Hi James, thanks for stopping by to read and comment, and thanks for all the good work on requirejs! I didn’t mean to sound too negative about AMD in the post — I think it’s definitely a THE way to structure and deliver javascript these days. The sugar syntax looks nice, however, if your application is large enough to warrant a build script (and I think a lot of apps are these days), then it makes sense to do that work before deployment.

    By having a custom conversion script, we were also able to do some fun things with our templates and CSS. Obviously, I’m now a little behind on the developments of Require (2.0 already? craziness!) so accept my apologies if I’ve misrepresented the library.

  • Nick Fisher

    GWT might be a good choice if you have Java developers on staff, but we don’t!

  • http://www.blossom.io Thomas Schranz

    Hey @jlfwong:disqus you can take a look at http://brunch.io which is the build system that we are using at https://www.blossom.io (coffeescript, backbonejs, stylus, …).

  • Guest

    It sounds like this ‘problem’ might solve itself soonish: http://swapped.tumblr.com/post/23133779276/google-bot-now-crawls-arbitrary-javascript-sites *fingerscrossed*

  • Guest

    It sounds like this ‘problem’ might solve itself soonish: http://swapped.tumblr.com/post…*fingerscrossed*

  • http://www.blossom.io Thomas Schranz

    It sounds like this ‘problem’ might solve itself soonish: http://swapped.tumblr.com/post…*fingerscrossed*

  • http://twitter.com/doomhz Dumitru Glavan

    Ah, nice.

    I’m a bit afraid of the static store implementation. Bonding multiple views to the same model can be dangerous. I ended up in some cases with views that altered or removed my model and it was broken everywhere.
    I also had views that shared the same DOM container and were rerendered when the router path changed. If by any chance someone forgot to destroy the old view when rendering the new one in the same DOM element, the link to the model was still there and the old view automatically rerendered himself on model change. It confusing for other team members to understand what’s happening and how to fix the bug.
    I ended up by creating a global Ajax caching plugin that cached the entire Ajax response by URL for a desired period of time. This way each view had it’s own model that could call fetch as many time as he wants.

    A great improvement was to implement a Sandbox system in our app. IT was basically a global object that knew about all the existent modules. All the modules received a link to this sandbox object and triggered events when something occurred inside the module. The sandbox will announce that to the rest of the modules about that event.
    This pub/sub system came pretty handy when working with Websockets. Whenever some data was changed in the application the websocket module will trigger a change event to the sandbox and that one will pass the data along to the rest of the modules. A Window module would trigger an unload or resize event to the rest of the modules instead of them binding directly to that events. An ErrorHandler module would catch all the errors events triggered by other modules and log them.

    Working with subviews was nice in the beginning. Lately it became a performance hell. Having a long HTML list as a parent view that had to render one by one subviews, appending them inside that list was very slow. I ended up working with one view linked to a collection that iterated through its objects and rendered them all at once.

    Thank you for listening :) and again – great job guys!

  • Nick Fisher

    AFAIK, google uses a javascript-executing crawler to get the preview images for the site, but still a ‘dumb’ bot for proper indexing.

  • Anonymous

    What I’m referring to is on the next.soundcloud.com page itself, the awesome player at the top has this widget that is a “timeline-que”. That viewer area shifts to different parts of the waveform based on where you scroll.

    What I’m curious about, specifically, is how whatever is listening for scroll() knows when to pass a param to the player BB view:
    - Is it based on height segmentation? That seems pretty static.
    - Is it based on a #div being “shown”?
    - And so on… I’m not sure

    I get how the rest of it would be accomplished, but I’ve never done anything with scroll() stuff in the past, so I’m looking for some insight to go off of.

    Thanks!

  • http://twitter.com/anthonyshort Anthony Short

    Yeah you pass a filter method to the subset method. It return a new collection that will only ever allow models matching that filter (and any parent filters). That way I can just have a global collection and many subsets but have every other collection automatically add new models whenever they are added to the top-most collection.

    Ah, a usage counter. I get it now. I wasn’t sure how you were tracking when to dispose of the model.

    Really looking forward to more posts like this.

  • James Burke

    Alexander and Nick: I appreciate you started your work a while back before the requirejs 2.0 release, just mentioning some current options if others read your post and want to emulate the approach. Thanks for sharing it — similar experiences are what drove the 2.0 changes, and the benefit of AMD is that you can do some custom work and still have a normalized target for output.

    On lazy evaluation: requirejs 2 and almond 0.1 (both released just recently) now do the following kind of lazy evaluation: a module’s factory function is not executed until that module is needed to satisfy a dependency for another module/top level require() call. So if you just deliver a bunch of defined modules with no top level require() they will just be registered with the loader but not executed.

    The modules are still just JS in the file and not converted to strings, so that kind of approach would need a custom tweak to almond or via another custom AMD shim.

  • http://twitter.com/a_kovalev Alexander Kovalev

    Thanks, James.
    At the moment we use UglifyJS in order to walk through AST of packaged js file and replace JS functions with strings.

  • http://twitter.com/anthonyshort Anthony Short

    I’ve been using the single-model approach as well and I haven’t ran into that issue. If a model gets updated, all views update without having to actually do anything. When a view is disposed off, it leaves the model. Collections and models handle the removal of a model unless a view is actually attempting to destroy it. That way when a model updates you don’t need to figure out how to sync every other similar model on the page.

    Of course, all of this depends on the app and the architecture you’re using.

  • http://obscurelyfamous.com Daniel Ha

    Internally, we called the new Disqus “Next” as well. :)

    Awesome writeup!

  • http://twitter.com/mattwondra Matt Wondra

    Great article with some really great ideas! 

    I was trying to implement the data store by overriding Backbone’s Model constructor, but was running into a return-value issue as noted in this (rejected) pull request: https://github.com/documentcloud/backbone/pull/1265 and related code snippet: https://github.com/ynniv/backbone/commit/4d03d09bcd19467aaa50c359d63135888a3ad2f2

    Namely, the helper functions wrap “parent object” constructors in an anonymous function, throwing away the return value and destroying the trick you mentioned. How did you get around this? Do you have a forked custom version of Backbone that respects the constructor return value? Or a wrapper class that gets around this? Something else? 

    Thanks for sharing your great work!

  • Kamran Mackey

    Soundcloud, Can I please have an invite for The Next Soundcloud?!?!?!?!??! I’m very sick and tired of waiting for a stupid invite!!!! email’s kamranm1200@gmail.com, Now send me an invite before I get damn fucking angry!!!!!!!!!!!!!!!

  • Kareem

    Excellent article. I’ve been meaning to implement something like your instance store for some time, and after reading your article have finally decided to do it. Thanks!

    However, I’ve run into an issue and I’m wondering if you experienced the same thing. If possible would you mind taking a look at this stack overflow question.

    http://stackoverflow.com/questions/11145159/implement-javascript-instance-store-by-returning-existing-instance-from-construc

    Thank you.

  • Pingback: Implement javascript instance store by returning existing instance from constructor | active questions php

  • Andrós

    Thanks for you reply Nick! Well, I wish you good luck with that. As an ‘early adaptor’ when it comes to webdev tech, I was really wandering if I was missing something. Anyway, it’s not really good practice to serve different content to search engines. (thinking: where have all the awesome ‘unobtrusive’ days been??) What I reached my developers (and it’s still very relevant): first build a app/site/cms so that it works without JS, after that apply JS/ajax-shit to make it awesome/neat/funky. It will be accessible for handicapped/disabled people, search engines… and therefore rock the shit out of your competition.

  • Pingback: SoundCloud前端技术团队分享开发经验 | 梦想小熊小爱

  • Pingback: Indigo » Link Roundup – Junho 2012

  • http://www.roborooter.com/ Francis

    I’ve created a small project that follows the singleton pattern for Models. (I’m not sure I’d want singleton views or collections but it wouldn’t be hard to generalize this.) It creates a Backbone.SingletonModel which passes all of Backbone’s model tests as well as it’s own.

    https://github.com/reconbot/backbone-singleton 

  • Chris Fricke

    At a previous company I worked for our team chose a very similar solution w/ backbone, thin (well ours was) server side, and a rich client side code base.  Because SEO was crucial to the business, we had to try to take our client side code and recreate that environment on the server side, ultimately using jsdom… unfortunately we ran into a big issue with in terms of memory consumption, open file handles (since each js file is now a file you have to open, and read in… obviously use your concatenated, minified file), and also just overall sluggishness of the rps.  We ultimately ended up moving a lot of our rendering to the server side for the routes (toggle-able via extensions (.json)) which reduced the complexity of the client side but of course brought more into the server side.  This allowed us to have any page first visited rendered normally in html (bots love that) and then subsequent calls switch to full ajax mode (if js available) and if not, go to server side rendering which is mainly for the bots, and that small percent of the world who for some reason doesn’t have JS enabled. 

    And btw, requirejs rocks.  

  • Pingback: Extending the MVC design pattern | Soniple

  • Anonymous

    Can you make the custom helper for Handlebars available?

  • Sam Kunesh

    when will this be publicly released?

  • http://www.facebook.com/philip.nuzhnyy Philip Nuzhnyy

    Great stuff. I especially liked the view helper. Here’s my take on it: https://github.com/callmephilip/backbone-tyler for. It definitely makes my life easier  

  • http://twitter.com/pulse00 Robert Gründler

    With your current URL structure, wouldn’t you get penalized by google when serving different content for the crawler and to your users? Afaik, the only “google-approved” way is to implement it using shebang urls, as described by google: https://developers.google.com/webmasters/ajax-crawling/

  • http://www.facebook.com/philip.nuzhnyy Philip Nuzhnyy

    Hey Michael. I put a little library together to enable this sort of declarative composite views. Hope this helps: https://github.com/callmephilip/backbone-tyler

  • Anonymous

    Thanks, mate!

  • http://twitter.com/colin_jack Colin Jack

    Great blog post, I’d also be interested in whether you’d open source the conversion longer term. Sounds very interesting, not least as it has the lazy interpretation built in.

  • Mockstep

    Looks Awesome!
    Sounds Awesome!
    Is Awesome!

  • nickpoorman

    +10

  • http://twitter.com/zimok zimok

    Ciao,

    thanks for this great technical post.

    I noticed that by looking at the webkit inspector, I can reach any of you javascript modules from the “Source” tab. I was trying to figure out how this is possible.

    Isn’t all your javascript compressed into one big file?
    How do the inspector is able to show me the source into separated modules?

    I’m using AMD and requirejs to optimize my modules into a javascript file and when I look through the inspector I can see the Source tab only show me the optimized file and no other modules.

    By the way, it’s very useful to look at your source, lot of great things learned :)

  • http://twitter.com/a_kovalev Alexander Kovalev

    You can inspect each module separately because we use Lazy Evalutation of CommonJs Modules in production, a technique proposed by Tobie Langel(http://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/). We modified AlmondJS a little bit to make it compatible with this so that each module is converted into a sting with a @sourceUrl directive appended to the end.

  • http://twitter.com/zimok zimok

    That sounds great.

    Any plan to open source your custom almondjs with lazy evaluation?

    Thanks

  • http://twitter.com/zimok zimok

    ps: Note that the link to the technique proposed by Tobie Langed is broken.
    It would be helpful to read about that.

  • http://twitter.com/a_kovalev Alexander Kovalev

    You can inspect each module separetely because in production we use Lazy evaluation of CommonJS modules, a technique proposed by Tobie Langel (http://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/). In order to implement it we modified AlmondJS a little bit so that each module is converted into a string with a special sourceURL directive appended to the end.

  • http://twitter.com/zimok zimok

    Lazy evaluation of commonjs modules (correct link) http://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/

  • Mulberg

    The way you handling model is great, but what about models that haven’t ID yet?

    For example, suppose we have some sort of messenger where user can add new message. He click some “Add message” button, and message from shows up. The new message model creates at frontend (it haven’t ID yet) and bind to the form. And after we click “Send message” it sends to the backend, where it saves into storage.

  • http://gregorynicholas.com/ Gregory Nicholas

    another important question is how you guys are thinking about dumping objects from memory as as user travels so far down their feed.. i can’t even load my likes from 2 weeks ago

  • Mulberg

    And what about saving model:

    1. How do you save hierarchical models, like Sound{ user: User }? You save User and Sound separately or save only Sound?

    2. If I call sound.get(‘user’), I’ll get User model or what?

    Maybe you can describe some more best-practices of managing models in complex application. This topic so rarely discussed.