Making the SPA work

Nick Fisher @spadgos


spadgos.github.io/webexpo

Background

Coming up

Models: making them work for you

CSS: making it work at all

Client-side performance, or "cheating"

Managing models

The long running app

  • Challenges
    • Memory leaks
    • Performance
  • Opportunities
    • Faster loading
    • Better experience

Leaks

var cache = {};
function sendRequest(url) {
  if (!cache[url]) {
    cache[url] = $.get(url).fail(logError);
  }
  return cache[url];
}

Sharing is caring

Many views, one model

Identity map

  • One model = one instance, everywhere
  • Access by overriding constructor
Martin Fowler: Patterns of Enterprise Application Architecture

var map = {};
function User(id) {
  if (map[id]) {
    return map[id];
  }
  map[id] = this;
  this.initialize();
}
var userA = new User(123),
    userB = new User(123);
userA === userB;                // true
userA.set('name', 'Sparticus');
userB.get('name');              // 'Sparticus'
var userC = new User(123);
userC.get('name');              // 'Sparticus'

Wait a second

Isn't this a massive memory leak?

It totally is, so...


var map = {},
    counts = {};    // <-- usage counter

function User(id) {
  if (map[id]) {
    ++counts[id];   // <-- increase on access
    return map[id];
  }
  map[id] = this;
  counts[id] = 1;   // <-- set to 1 on first creation
  this.initialize();
}

User.prototype.release = function () {
  --counts[this.id];
}
This is a slight simplification...

In use


MyView = View.extend({
  initialize: function (options) {
    this.model = new Sound(options.soundId);
  },
  dispose: function () {
    this.model.release();
  }
});

Still need to be careful to release all that you create

A nice side-effect

Nested API Responses

api.soundcloud.com/tracks/97602814.json


{
  // ...snip...
  title: "02 daftside - within",
  uri: "https://api.soundcloud.com/tracks/97602814",
  user: {
    id: 48554944,
    kind: "user",
    permalink: "daftside-2",
    avatar_url: "https://i1.sndcdn.com/avatars...",
    username: "DARKSIDE (USA)"
  },
  user_id: 48554944
}
          

Managing models

Techniques like the identity map can help you make full use of the data you have...

...but be careful to manage that data over your app's lifetime.

CSS Management


body#pages.sounds #main-wrapper #main-wrapper-inner #soundpage #primary .social-sharing .share-plugin.last .twitter-share-button {
  width: 101px !important;
}
          

5 ids

5 classes

8 descendant selectors

!important

wat

The problems

Fragility

Speed

File size

Maintainability

Our solution

Application built from lots of small views

One CSS file per view

In the template, each element gets a namespaced class

Inspired by BEM from Yandex


.soundTitle__title {
  max-width: 100%;
  display: inline-block;
}

.soundTitle__tagContainer {
  line-height: 12px;
}

.soundTitle__playButton {
  margin-right: 5px;
  margin-top: -15px;
}
          

But...

"It makes my eyes bleed"Chris Eppstein, SASS/Compass

CSS ↝ JS

CSS ↝ JS

define("views/sound/title.css", [],
  function(require, exports, module) {
    var style = module.exports = document.createElement("style");
    style.appendChild(
      document.createTextNode(
        '.soundTitle__info{display...}'
      )
    );
  }
);

View.extend({
  css: require('views/sound/title.css'),
  render: function () {
    if (!this.css.parentNode) {
      document.body.appendChild(this.css);
    }
    // ...
  }
});
          

CSS as modules

Fragility

Speed

File size

Maintainability

My waveforms

Let me show you them

The situation

New Waveforms

New style: sharp, no anti-aliasing.

New playlist style

Flexible

Solution:

Canvas!

Problem:

Data!

Solution:

Images as data source

Problem:

Performance!

Dubstep fans

Rendering time: 1352ms

Gradual improvements

  • Most efficient detection algorithm
  • Caching scaled images
  • Typed Arrays
  • WebWorkers

Breakthrough realisation:

Who gives a crap anyway?

Still kept improving

...by giving up

  • Move calculation to the back end
  • Ported algorithm to Go
  • Front end receives JSON, everyone happy

End result

Before: 1352ms

After: 10ms

Take away

Think about how to cheat.

Take off the engineer hat.

Think about what's important, not what's "correct".

THE END

Nick Fisher / @spadgos


soundcloud.com
spadgos.github.io/webexpo

(Obligatory)

We're hiring!

soundcloud.com/jobs