Gitter marionette deck

Post on 11-Aug-2015

173 views 1 download

transcript

Who are these dudes?

suprememoocow

mydigitalself

Andrew Newdigate

Mike Bartlett

Gitter is where developers come to talk

120 000 registered users

24 000 public communities

Active every minute of every day

502 releases in 1.5 years

Zawinksi’s Law

Every program attempts to expand until it can read mail.

Those programs which cannot so expand are replaced by ones which can.

Act 1

Performance

Global Average

4.5Mbps

Akamai

Two Great ToolsOSX Network Link Conditioner

Chrome DevTools Network Throttle

Scene 1

Mistake: Assuming jQuery is fast enough

Chuck Norris’ keyboard

Finding performance problems

Don’t optimise too early

Focus on CollectionViews, CompositeViews

Improving the performance 1000% on a view that gets rendered once in the application isn’t going to make the slightest bit of difference.

The Chrome DevTools Timeline is Awesome

Example of using Timelines$.tooltip is really slow

Solution: change the tooltip behaviour to only initialise the tooltip on the first mouseover event fired on the element

1ms per tooltip to 0.005ms per tooltip

Easy performance win: attachElContentOverride this method in your collection/composite child views

// Abbreviated version of the attachElContent MixIn we use on Gitter attachElContent: function(html) { if (typeof html === 'string') { this.el.innerHTML = html; return this; } this.$el.html(html); return this; }

.innerHTML vs $.html

Scene 2

Mistake: Not pre-rendering content

Pre-rendering is good practicePage indexing / SEO advantages to doing it

Perceived speed of page load is much faster

Avoid multiple reflows as the application loads

Less jankiness

Pre-rendering is messy

At the moment, done through a series of hacks:

• Server-side handlebars helpers

• Client-side Marionette extensions

Would be awesome to move this out into a semi-sane, open-source library (or build it into Marionette!)

Fully pre-rendered Partially pre-rendered

Isomorphic LayoutViewsIn LayoutView’s before:show event

• If the region is empty, initialise ChildView as per normal:

• If the region already contains content, mount the ChildView on the existing element: view.showChildView(regionName, new ChildView({ el: existingElement, template: false, ... options ... }));

view.showChildView(regionName, new ChildView({ ... options ... }));

Isomorphic CollectionViews

childViewOptions: function (model) { if (!model.id) return; var el = this.$el.find('> [data-id="' + model.id + '"]')[0]; if (!el) return; return { el: el, template: false };},

<ul> <li data-id="1">One</li> <li data-id="2">Two</li> <li data-id="3">Three</li></ul>

collection.reset([ { id: “1”, name: “One” }, { id: “2”, name: “Two” }, { id: “3”, name: “Three” }]);

Scene 3

Mistake: Too much Jason

“640 people ought to be enough for any room”

Thanks!

People Roster Data

~300 characters

In a 5000 user room, that’s 1.4MB of JSON

Retina and non-retina avatar URLs

Unused fields, duplicate data, etc

{ "id": "5298e2d5ed5ab0b3bf04c980", "username": "suprememoocow", "displayName": "Andrew Newdigate", "url": "/suprememoocow", "avatarUrlSmall": "https://avatars1.githubusercontent.com/suprememoocow?v=3&s=60", "avatarUrlMedium": "https://avatars1.githubusercontent.com/suprememoocow?v=3&s=128", "gv": "3", "v": 30}

How we represent them now

77 characters

In a 5000 user room, that’s still 375KB.

Limit the list to the first 20 people

{ "id": "5298e2d5ed5ab0b3bf04c980", "username": "suprememoocow", "gv": "3", "v": 30}

Scene 4

Mistake: Using .on too much

.on is a code smellUsing jquery events

Backbone events

Also, beware of long running setTimeouts

this.ui.actionButton.on('click', function() { window.alert('Yo'); });

this.model.on('change', function() { window.alert('Yo'); });

Obvious solutionUse modelEvents, collectionEvents and events modelEvents: { 'change': 'onChange'},events: { 'click @ui.badge': 'onBadgeClicked'},collectionEvents: { 'add reset sync reset': 'showHideHeader'},

Use listenTo for listening to Backbone.Eventsthis.listenTo(model, 'change', function() { })

When you still need .onRemember to cleanup after yourself

onClick: function() { this.$someElement.on('mouseenter', ...); this.longRunningTimer = setTimeout(function() {}, 60000);},

onDestroy: function() { this.$someElement.off(); clearTimeout(this.longRunningTimer);},

DevTools Heap SnapshotsTake periodic snapshots and use the comparison view to find new allocations

Act 2

Software design mistakes

Scene 1

Mistake: Coupling view components together

MV* 101

This is how we’re taught to structure MV* applications at school.

Sometimes it’s easier to ignore the advice

We need to tell another view to do something.

We’re in a rush, so we’ll just wire the dependency in and fix it later. var MyView = Mn.ItemView.extend({ ... onActionClicked: function() { this.options.anotherView.doSomething(); }, })var myView = new MyView({ anotherView: anotherView });

Pretty soon we’ve got a tightly coupled mess

This makes change hardJust try to:

• Move a view within the view hierarchy

• Remove a view in a certain environment (unauthenticated view, mobile, etc)

Let’s change things around a bit…

Marionette solutions

Use a shared model and update the model

wreqr, or better yet, Backbone.Radio

Imaginary Radio Behaviourvar MyView = Mn.ItemView.extend({ behaviors: { Radio: { name: 'ui', comply: { 'chat:focus': 'focusChat' … }, focusChat: function() { // .... }});

var AnotherView = Mn.ItemView.extend({ behaviors: { Radio: { name: 'ui' }, }, onActionClicked: function() { this.radio.command('chat:focus'); }});

*correct spelling

Scene 2

Mistake: Messing with another view’s DOM

Quick and dirty

A component needs to respond to an action and change another component’s DOM…

Easiest solution: just use jquery

onClick: function() { $('#action-button').hide(); }

c/c++ pointer arithmetic

In c/c++, it’s possible to use pointer arithmetic to directly modify the contents of a location in memory.

I’m sure you will all agree: this is a VERY BAD IDEA!

bptr = (byte*) &data;bptr = bptr + 5;iptr = (int*) bptr;(*iptr) = 0xcafebabe;

Now imagine…

Your DOM is a global memory shared by all the Javascript code running in your app

Each view in your app manages a distinct piece of the global memory

Mutating another view’s DOM is a bit like using pointer arithmetic to change it’s memory behind it’s back

Don’t do it!

But why?

Refactoring becomes a nightmare

You’re creating hidden connections between views in your application.

Scene 3

Mistake: Different module formats on the client and server

Then

Client: AMD modules with RequireJS

Tests: run in a phantomjs

Server: commonjs modules with nodejs

Tests run in nodejs with mocha

Now

Client: commonjs modules with webpack

Server: commonjs modules with nodejs

Shared code is kept in shared

Shared code can be tested quickly using the nodejs and mocha, without having to start a phantomjs browser

require.ensure();

// In your backbone router.... markdown: function() { require.ensure(['views/markdown/markdownView'], function(require) { var MarkdownView = require('views/markdown/markdownView'); appView.dialogRegion.show(new MarkdownView({})); }); },

Act 3

In closing

Code DebtA lot of these problems as the result of technical debt. When we started building the project we chose Backbone, and only later did we switch to Marionette.

Initially, we treated Marionette as a neat extension of Backbone, for things like CollectionViews etc so the transition was gradual and left a lot of technical debt around.

Marionette 2 PR

From a small prototype to a large application

A lot of the pain we’ve experienced has been down to the fact that we started off with a small application which has grown larger and larger.

Start as you mean to go on