Upload
aglemann
View
2.769
Download
0
Tags:
Embed Size (px)
DESCRIPTION
Leverage patterns of large-scale JS – such as modules, publish-subscribe and delegation – to achieve extreme performance without sacrificing maintainability.
Citation preview
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?