gitter marionette deck
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