beyond domready: ultra high-performance javascript

Post on 11-May-2015

2.762 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

Leverage patterns of large-scale JS – such as modules, publish-subscribe and delegation – to achieve extreme performance without sacrificing maintainability.

TRANSCRIPT

this is not a TEDTALK

not high-performance javascript

ultra high-performance javascript

what is ultra high-performance?

made possible via

• fast file loading.

• small file sizes.

• avoiding DOM bottlenecks.

what high-performance looks like

hint: in general it looks awful

high-performance != maintainability

the elements of high-performance are usually at odds with best practices in maintainability…

an approach

start with maintainability, and achieve high-performance by building it in via automated processes.

large-scale JS

to write maintainable code, look at the patterns used by other large-scale JS frameworks:

large-scale JS

• separate code into modules, each of which accomplishes a single function.

• expose the module through an interface.

modularprogramming

module pattern

module consists of 3 parts:

module pattern

module consists of 3 parts:

1. function (what it does)

var _transform = function(sel) {$(sel).toggleClass('robot');

}

// wrapped in a self-executing functionvar transformer = function() {

var _transform = function(sel) {$(sel).toggleClass('robot');

}}();

module pattern

module consists of 3 parts:

1. function (what it does)

2. dependencies (what it needs)

var transformer = function($) {var _transform = function(sel) {

$(sel).toggleClass('robot');}

}(jQuery);

module pattern

module consists of 3 parts:

1. function (what it does)

2. dependencies (what it needs)

3. interface (what it returns)

var transformer = function($) {var _transform = function(sel) {

$(sel).toggleClass('robot');}// sets what `transformer` is equal toreturn = {

transform: _transform}

}(jQuery);

var transformer = function($) {var _transform = function(sel) {

$(sel).toggleClass('robot');}// sets what `transformer` is equal toreturn = {

transform: _transform}

}(jQuery);

// usagetransformer.transform('.car');

var transformer = function($) {var _transform = function(sel) {

$(sel).toggleClass('robot');}// sets what `transformer` is equal toreturn = {

transform: _transform}

}(jQuery);

// usagetransformer.transform('.car');

// result<div class="car robot" />

benefits of modular programming

• self contained – includes everything it needs to accomplish it's function.

• namespaced – doesn't dirty the global scope.

// you can’t do this… yet

import "transformer.js" as transformer;

3rd party loaders

3rd party loaders

• LABjs

• HeadJS

• ControlJS

• RequireJS

• Load.js

• YepNope.js

• $script.js

3rd party loaders

• LABjs

• HeadJS

• ControlJS

• RequireJS

• Load.js

• YepNope.js

• $script.js

• LazyLoad

• curl.js

• JsDefer

• jquery.defer.js

• BravoJS

• JSLoad

• StealJS

3rd party loaders

• LABjs

• HeadJS

• ControlJS

• RequireJS

• Load.js

• YepNope.js

• $script.js

• LazyLoad

• curl.js

• JsDefer

• jquery.defer.js

• BravoJS

• JSLoad

• StealJS …and more

I’ll make it easy…

I’ll make it easy…

just use RequireJS.

I’ll make it easy…

just use RequireJS.

• plugin architecture (text, l10n, css, etc).

• built in support for has.js.

• support for r.js.

• James Burke knows his shit.

• author of the AMD standard.

// vanilla js modulevar transformer = function($) {

var _transform = function(sel) {$(sel).toggleClass('robot');

}return = {

transform: _transform}

}(jQuery);

// AMD module wraps everything in `define`define(function($) {

var _transform = function(sel) {$(sel).toggleClass('robot');

}return = {

transform: _transform}

}(jQuery));

// dependency array is the first parameter of define// dependencies mapped to parameters in the callbackdefine([

'jquery'], function($) {

var _transform = function(sel) {$(sel).toggleClass('robot');

}return = {

transform: _transform}

});

// dependency array is the first parameter of define// dependencies mapped to parameters in the callbackdefine([

'jquery','underscore'

], function($, _) {var _transform = function(sel) {

$(sel).toggleClass('robot');}return = {

transform: _transform}

});

// usagerequire([

'transformer'], function(transformer) {

transformer.transform('.car');});

example website

• common.js – code shared by all pages.

• home/main.js – code unique to the home page.

// common.jsdefine([

'jquery','ui/jquery.ui.core','ui/jquery.ui.widget'

], function($) {// setup code for all pages

});

// common.jsdefine([

'jquery','ui/jquery.ui.core','ui/jquery.ui.widget'

], function($) {// setup code for all pages

});

// but `jquery.ui.core` and `jquery.ui.widget`// aren’t modules!

// common.jsrequirejs.config({

shim: {'ui/jquery.ui.core': {

deps: ['jquery']},'ui/jquery.ui.widget': {

deps: ['ui/jquery.ui.core']}

}});define([

'jquery','ui/jquery.ui.core','ui/jquery.ui.widget'

], function($) {

// home/main.jsdefine([

'common','ui/jquery.ui.dialog'

], function($) {$('.modal').dialog();

});

// home/main.jsdefine([

'common','ui/jquery.ui.dialog'

], function($) {$('.modal').dialog();

});

// index.html<script src="require.js" data-main="home/main"></script>

// home/main.jsdefine([

'common','ui/jquery.ui.dialog'

], function($) {$('.modal').dialog();

});

// index.html<script src="require.js" data-main="<?php echo $template ?>/main"></script>

behind the scenes (phase 1)

1. download require.js – 1.

2. download home/main.js – 2.

3. check dependencies.

4. download common.js, ui/jquery.ui.dialog – 4.

5. check dependencies.

6. download jquery, ui/jquery.ui.core, ui/jquery.ui.widget – 7.

7. check dependencies.

behind the scenes (phase 2)

1. evaluate jquery, then jquery.ui.core, then jquery.ui.widget.

2. execute the common.js callback.

3. evaluate jquery.ui.dialog.

4. execute the home/main.js callback.

modular and maintainable

but crappy performance: 7 requests!

make it high-performance

introducing r.js

module optimizer.

// build.js({

modules: [{

name: "common"},{

name: "home/main"exclude: "common"

}]

})

// run it manually // or as part of automated build process java -classpath r.js/lib/rhino/js.jar \

org.mozilla.javascript.tools.shell.Main \r.js/dist/r.js -o build.js

// example output

Tracing dependencies for: common

common.js----------------jquery.jsui/jquery.ui.coreui/jquery.ui.widgetcommon.js

// example output

Tracing dependencies for: home/main

home/main.js----------------ui/jquery.ui.dialoghome/main.js

// example output

Uglifying file: common.jsUglifying file: home/main.js

new behind the scenes (phase 1)

1. download require.js – 1.

2. download home/main.js (includes ui/jquery.ui.dialog) – 2.

3. check dependencies.

4. download common.js (includes jquery, ui/jquery.ui.core, ui/jquery.ui.widget) – 3.

5. check dependencies.

only 3 requests!

• only 1 request per page after initial page load (require.js and common.js are cached for all pages).

• scripts loads asynchronously (non-blocking) and in parallel.

• all assets optimized (supports uglify or closure compiler).

mandatory builds for UI sucks

// build.js({

baseUrl: "js-src/", // input folderdir: "js/", // output foldermodules: [

{name: "common"

},{

name: "home/main"exclude: "common"

}]

})

// index.html<script src="js/require.js" data-main="<?php echo $_GET['dev'] ? 'js-src/home/main' : 'js/home/main' ?>"></script>

// index.html – production js, 3 requests// index.html?dev - development js, 7 requests

even better performance with has.js

feature detection library.

define(['has'

], function($) {// add a testvar re = /\bdev\b/;has.add('dev',re.test(window.location.search));// use `has`if (has('dev')) {

console.log('test');}

});

define(['has'

], function($) {// add a testvar re = /\bdev\b/;has.add('dev',re.test(window.location.search));// use `has`if (has('dev')) {

console.log('test');}

});

// index.html?dev// "test"

// build.js({

baseUrl: "js-src/",dir: "js/",has: {

dev: false},modules: [

…]

})

// originalif (has('dev')) {

console.log('test');}

// originalif (has('dev')) {

console.log('test');}

// after r.js pre-processingif (false) {

console.log('test');}

// originalif (has('dev')) {

console.log('test');}

// after r.js pre-processingif (false) {

console.log('test');}

// after uglify post-processing// nothing – uglify strips dead code branches

has.add('ie7-support', true);if (has('ie7-support') {

// some godawful hack to fix something in ie7}

make it ultra high-performance

even better performance with almond

intended for single page apps or mobile where request latency is much worse than desktop.

• require.js = 16.5k minified (6k gzipped)

• almond.js = 2.3k minified (~1k gzipped)

only 1 request… ever.

• shaves 14k of boilerplate.

1st step to ultra high performance

use modular programming.

• combine with require.js for asynchronous / parallel loading.

• automatic concatenation, optimization.

• for ultra performance use almond.js.

anyone not use jquery?

“Study shows half of all websites use jQuery”

– August, 2012

// example of a jquery plugin used with a moduledefine([

'jquery','jquery.craftyslide'

], function($) {$('#slideshow').craftyslide();

});

// closer look at `craftyslide.js`$.fn.craftyslide = function (options) {

function paginate() {…

}function captions() {

…}function manual() {

…}

paginate(); captions(); manual();}

problem with jquery plugins

they’re a black box.

• not easily extendable.

• not easily testable.

problem with jquery plugins

they’re a black box.

• not easily extendable.

• not easily testable.

jquery ui set out to solve this with…

uiwidgets

oh noes! not jquery ui

bloated piece of crap (210k omg!)

• jquery ui is modular – use just the bits you need.

• ui core + ui widget + effects core (16k minified or ~6k gzipped).

ui widgets

the two things plugins suck at, widgets do really well:

• they're fully extendable.

simple javascript inheritence

25 lines of javascript sexiness:

• constructors.

• object-oriented inheritence.

• access to overridden (super) methods.

simple javascript inheritence

25 lines of javascript sexiness:

• constructors.

• object-oriented inheritence.

• access to overridden (super) methods.

also the foundation of ui widget extensibility.

// example widget$.widget('ui.transformer', {

options: {…

},_create: function() {

…}

);

// example widget$.widget('ui.transformer', {

options: {…

},_create: function() {

…}

);

// extending it$.widget('ui.autobot', $.ui.transformer, {

// extend anything or everything});

not-so simple javascript inheritence

everything from simple javascript inheritence, plus:

• namespaces.

• public and private methods.

• getters/setters.

• disable/enable.

ui widgets

the two things plugins suck at, widgets do really well:

• they're fully extendable.

• they're tuned for testing.

// if `craftyslide` were a widget$.widget('ui.craftyslide', {

_create: function() {…this._paginate();this._captions();this._manual();

},_paginate: function(){ … },_captions: function(){ … },_manual: function(){ … }

);

// adding triggers as hooks for testing$.widget('ui.craftyslide', {

…_paginate: function(){

this._trigger('beforePaginate');…this._trigger('afterPaginate');

},…

);

// in your unit testfunction beforePaginate() {

// test conditions}function afterPaginate() {

// test conditions}$('#slideshow').craftyslide({

beforePaginate: beforePaginate, afterPaginate: afterPaginate

});

// plugin using `.on()`function manual() {

…$pagination.on('click', function (e) {

… });

}

// plugin using `.on()`function manual() {

…$pagination.on('click', function (e) {

… });

}

// widget using `._on()`manual: function() {

this._on($pagination, { click: '_click' }}

// `._on()` remembers all event bindings_on: function( element, handlers ) {

…this.bindings = this.bindings.add( element );

},

// `._on()` remembers all event bindings_on: function( element, handlers ) {

…this.bindings = this.bindings.add( element );

},

// `.remove()` triggers a `remove` eventthis._on({ remove: "destroy" });

// `._on()` remembers all event bindings_on: function( element, handlers ) {

…this.bindings = this.bindings.add( element );

},

// `.remove()` triggers a `remove` eventthis._on({ remove: "destroy" });

// `.destroy()` cleans up all bindings// leaving the DOM pristinedestroy: function() {

…this.bindings.unbind( this.eventNamespace );

}

// setup widget$('#slideshow').craftyslide();

// run tests…

// teardown// calls `.destroy()` // which automatically unbinds all bindings$('#slideshow').remove();

high-performance from code re-use

define(['jquery','ui/jquery.ui.core','ui/jquery.ui.widget','ui/jquery.ui.craftyslide'

], function($) {$.widget('ui.craftyslide', $.ui.craftyslide, {

_manual: function() {// extend to do whatever I want

}});

});

2nd step to ultra high performance

use object-oriented widgets as code building blocks.

• inheritance promotes code re-use, smaller codebase.

• built on an architecture that promotes testability.

made possible via

• fast file loading.

• small file sizes.

• avoiding DOM bottlenecks.

avoiding DOM bottlenecks

eventdelegation

<ul id="transformers"><li><a>Bumblebee</a></li><li><a>Ratchet</a></li><li><a>Ironhide</a></li>

</ul>

// typical event binding$('#transformers a').on('click', function() {

// do something});

<ul id="transformers"><li><a>Bumblebee</a></li><li><a>Ratchet</a></li><li><a>Ironhide</a></li>

</ul>

// typical event binding$('#transformers a').on('click', function() {

// do something});

// event bubbling allows us to do this$('#transformers').on('click', function() {

// do something});

// event delegation is similar$('#transformers').on('click', 'a', function() {

// do something});

// event delegation is similar$('#transformers').on('click', 'a', function() {

// do something});

// but allows us to do this$(document).on('click', '#transformers a', function()

// do something});

why does that kick ass?

• more performant – less memory, faster to bind/unbind.

• less maintenance – you can add/remove <ul id="transformers"> at any point in time and don't need to re-attach the event listener.

• faster – you can bind the event listener to document as soon as the javascript has loaded, you don't need to wait for domready.

how does this work with widgets?

it doesnt – widget's pitfall is they are a DOM bottleneck.

// example `lightbox` widget$('#gallery a').lightbox();

// widget depends on `this.element`$.widget('ui.lightbox', {

_create: function() {this._on(this.element, { click: 'show' });

}});

two workarounds

• one for legacy widgets.

• better approach for new widgets.

// legacy widgets$(document).on('click', '#gallery a', function() {

$(this).lightbox().lightbox('show');

});

// new widgets$.widget('ui.lightbox', {

_create: function() {var sel = this.options.selector;var handler = {};handler['click ' + sel] = 'show’;this._on(handler);

}});

// new widgets$.widget('ui.lightbox', {

_create: function() {var sel = this.options.selector;var handler = {};handler['click ' + sel] = 'show’;this._on(handler);

}});

// always instantiate on the document$(document).lightbox({

selector: '#gallery a' });

3rd step to ultra high performance

delegate anything and everything you can.

• will add interaction to elements that are lazy-loaded, inserted via ajax after page load, etc.

• allows for interaction before domready!

delegation isn’t a cure all

delegation works great when the widget doesn't need to know about the user up until the user interacts with it.

but what about widgets that need to affect the DOM on instantiation…

how we’ve done this previously

• document.load – the 80's of the internet.

• document.DOMContentLoaded – the new load event!

domready considered an anti-pattern

“the short story is that we don't want to wait for DOMContentReady (or worse the load event) since it leads to bad user experience. the UI is not responsive until all the DOM has been loaded from the network. so the preferred way is to use inline scripts as soon as possible”

– Google Closure team

<ul id="transformers"><li><a>Bumblebee</a></li><li><a>Ratchet</a></li><li><a>Ironside</a></li>

</ul><script>

$('#transformers').slideshow();</script>

oh no you didn’t

a problem with our modular approach:

• nothing is exposed to the global scope – you can't use modules from the DOM.

mediator pattern to the rescue

a central point of control that modules communicate through – instead of directly with each other.

pubsub

central point of control

• publish

• subscribe

• unsubscribe

it’s so easy

• publish = $.trigger

• subscribe = $.on

• unsubscribe = $.off

// in codevar proxy = $({});window.publish = function() {

proxy.trigger.apply(proxy, arguments);}window.subscribe = function() {

proxy.on.apply(proxy, arguments);}window.unsubcribe = function() {

proxy.off.apply(proxy, arguments);}

<ul id="transformers"><li><a>Bumblebee</a></li><li><a>Ratchet</a></li><li><a>Ironside</a></li>

</ul><script>

publish('load.transformers');</script>

define(['main'

], function() {subscribe('load.transformers', function() {

$('#transformers').slideshow();});

});

oh no you didn't

two problems with our modular approach:

• nothing is exposed to the global scope – you can't use modules from the DOM.

• if the JS is loaded asynchronously you don't know that it's available when the browser is parsing the HTML.

<head>// blocking, should be tiny (1k) or inlined!<script src="bootstrap.js"></script>// asynchronous non-blocking<script src="require.js" data-main="home/main"></script>

// bootstrap.js// needs to be some global object// but we can clean it up afterwardsdocument.queue = [];window.publish = function() {

document.queue.push(arguments);}

<script>publish('load.transformers');

</script>

// document.queue = [['load.transformers']]

// main.jsdefine([

'jquery'], function($) {

var proxy = $({});window.publish = function() {

proxy.trigger.apply(proxy, arguments);}window.unsubcribe = function() {

proxy.off.apply(proxy, arguments);}…

window.subscribe = function(event) {proxy.on.apply(proxy, arguments);

});

window.subscribe = function(event) {proxy.on.apply(proxy, arguments);$(document.queue).each(function(index) {

if (this[0] === event) {proxy.trigger.apply(proxy, this);document.queue.splice(index, 1);return false;

}});

});

ultra high-performance achieved!

ultra high-performance achieved!

1. use modular programming.

2. use object-oriented widgets as code building blocks.

3. delegate anything and everything you can.

4. use pubsub for everything else.

onesec

about me

about me

• I like Land Cruisers.

• lived in Costa Rica for 10 years (there is no excuse for how I speak).

• UI dev lead / mobile developer at Backcountry.

questionspreguntas?

top related