Large-scale Javascript





Joel Kemp 
Bēhance/Adobe
@mrjoelkemp

Some of the Problems 

with JAvascript at scale


  • Spaghetti (unstructured) Code
  • Too many script tags hurt performance
  • Language malleability creates style differences
  • Tooling to solve the above problems
  • The “send your kid off to school” problem
  • JS on the server

Spaghetti code

       1. No clear separation of JavaScript from HTML and/or your scripting language (Python, PHP, etc).
 <div class='button' id='fansMoreBtn' onclick='loadMoreFans()'>MORE</div>


2. No clear idea of which code does what, why, and where

var visible = Number({visible});
var inviteStr = "{inviteStr}";  
var srcId= "{srcId}"; 
if (window.location.search.indexOf("v=1") > 0 ) {
  visible = 0;
}

var userFirstName = '{ui_facebookFirstName}';
var userLastName = '{ui_facebookLastName}';
var userFacebookId = '{ui_facebookId}';

Javascript MVC frameworks/libraries


Backbone.js
Angular.js
Ember.js


Why use them?
  • Add structure by forcing you to model your app their way
  • Makes application front-end architecture predictable
  • Community-driven solutions to common web-app problems
  • Zodiac Example in Backbone.js

    var UserModel = Backbone.Model.extend({   defaults: {    birthday: '',    age: 0,    sign: ''  },  initialize: function () {    // Determines the age from the birthday and sets the age for this user    this.computeAge();    this.computeZodiacSign();  },  computeZodiacSign: function () {    // Simplified logic    if (this.getMonth() === 'March' && this.getDay() >= 21 ||         this.getMonth() === 'April' && this.getDay() <= 20) {      this.set('sign', 'Aries');    }  }});
     

    ZODIAC EXAMPLE IN BACKBONE.JS

    var UserView = Backbone.View.extend({  tagName: 'div',  className: 'user',   template: _.template($('#user-template').html()),   render: function () {     this.$el.html(this.template(this.model.toJSON()));   }}); 

    The HTML Template

    <script id="user-template" type="text/template">  <span>Your birthday is <%= birthday %></span><br />  <span>You are a <%= sign %></span><br />  <span>You are <%= age %> years old</span><br />  <span><%= fortune %></span><br /></script>

    ZODIAC EXAMPLE IN BACKBONE.JS

    var AppView = Backbone.View.extend({  el: 'app',  events: {    'click button.submit': 'handleSubmit'  },  handleSubmit: function () {    // Grab the birthday from the input fields    // Initialize a new User Model    // Create a User View whose model is the new model    // Render the view    // Add the rendered view's html somewhere  }}); 

    Too many script tags hurt performance

    • Every (synchronous) script tag blocks the DOM from rendering and/or page interactivity
    • Users see a white screen (DOM render blocked)
      • depending on where the script tags are placed
    • Users can't scroll/interact with the page
      • window.load blocked
    • Every script tag incurs an http request's delay

    A typical page

    Blocks the DOM from rendering
    <html>  <head>    <script src="js/jquery.min.js"></script>
    <script src="js/underscore.min.js"></script> <script src="js/backbone.min.js"></script>
    <script src="js/UserModel.js"></script>
    <script src="js/UserView.js"></script>
    <script src="js/AppView.js"></script> <!-- All of your other plugins/scripts --> </head> <body> </body></html>

    Bundle all javascript into a single file

    • Concatenate all used JS files into one generated JS file
      • Don't do this manually, there are tools as we'll see
    • No additional http request delay
    • At most a single script tag (in the ideal case)
    • Put the script tag at the end of the <body> tag

    That file will still be large though, which could impact mobile users of your application.

    Solution: Minify the bundle

    Minified example

    Snippet of Backbone.js' minified source code
    (function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?

    Resolving File Dependencies

    • AppView depended on UserModel and UserView existing
      • Otherwise, it couldn't create instances of them
    • How did we guarantee that everything existed at the right time?
      • We organized the script tags a certain way
        • This doesn't scale.

    Solution: AMD or CommonJS module formats to automatically deduce the order dependencies

    AMD

    Define a Module
    define(['Backbone'], function (Backbone) {  return Backbone.Model.extend({    // Same implementation as before  });});
    Require (import) a Module
    define(['./UserModel', './UserView'], function (UserModel, UserView) {  var user = new UserModel();  ...})

    Commonjs

    Defining a module
    var Backbone = require('Backbone');
    module.exports = Backbone.Model.extend({ // Same stuff});
    Requiring a module
    var UserModel = require('./UserModel'),    UserView = require('./UserView');
    var user = new UserModel();

    Malleability influences style differences

    See http://sideeffect.kr/popularconvention#javascript

    Comma first vs Comma last
    Spaces vs Tabs
    Extra spaces before arguments, keywords
    Single Var vs Multi-Var
    Cuddled Braces vs No Cuddling
    Single Quotes vs Double Quotes
    ...


    Keeping the style the same

    • Style Guides
      • Airbnb, Google, Crockford, and more
    • JSHint
      • Enforce the same subset of the language
        • No double equals
        • No unused variables
        • No with statements
        • Only Single quotes
    • JSCS
      • Single space after keywords
      • Single var

    Tools to automate this stuff

    Grunt, Gulp, and Broccoli

    • Configuration based tools that let you set up your build environment
    • You say which tasks to execute at what time
      • When a .js file changes in this directory, re-concatenate all javascripts into a single file
      • When a concatenated bundle changes, minify that bundle
      • When a .js file changes, run JSHint and JSCS on it 
      • Resolve the use of the module system (r.js or browserify)

    Sample Gruntfile

     module.exports = function(grunt) {
      grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        concat: {
          dist: {
            src: ['src/**/*.js'],
            dest: 'dist/<%= pkg.name %>.js'
          }
        },
        uglify: {
          dist: {
            files: {
              'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
            }
          }
        },    jshint: {
          files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js']
        },
        watch: {
          files: ['<%= jshint.files %>'],
          tasks: ['jshint']
        }
      });};

    Sample gulp-file

     var gulp = require('gulp')
        , uglify = require('gulp-uglify')
        , minifyHTML = require('gulp-minify-html')
        , sass = require('gulp-sass');gulp.task('build', function(){
        var dist = 'dist/', dirPublic = 'public/'
            , distStylesheets = dist + dirPublic + 'stylesheets/'
            , distJavascripts = dist + dirPublic + 'javascripts/';        gulp.src('public/stylesheets/scss/*.scss')
            .pipe(sass())
            .pipe(gulp.dest(distStylesheets));
        gulp.src('*.html')
            .pipe(minifyHTML())
            .pipe(gulp.dest(dist))    gulp.src('public/javascripts/*.js')
            .pipe(uglify())
            .pipe(gulp.dest(distJavascripts))
    });gulp.task('default', function() {
        gulp.watch([
            'public/stylesheets/scss/**', 'public/javascripts/*.js', '*.html', '!dist/**'
        ], function(event) {
            gulp.run('build');
        });
    });

    Send your kids off to school

    You created this javascript
    Sent it off to a user's browser
    And you have no idea the trouble it's causing


    • It could throw errors that you'll never see
    • It could use JS features that don't exist in that browser (IE8)
    • It could be ill-performant on certain platforms
    • It could never run (JavaScript turned off)

    Javascript error logging

    Muscula
    Qbaka

    https://gist.github.com/cheeaun/5835757

    Catches errors thrown in the browser and reports them
    Keeps track of error stats:
    • the browser used 
    • geographic region
    • frequency count
    • time/date


    JS on the server

    Node.js allows JavaScript to be run on the server

    To do what Python, PHP, and Java have been doing
    • Implement web servers
    • Implement (server-side) application code
    • Interact with the database and other services
    • Interact with the filesystem and OS
    • and more...

    Can we reuse some of the code we've written already?

    We've written tons of browser-centric JavaScript.
    Can we reuse any of that code (those libraries)?

    Isomorphic JavaScript
    • A unified JavaScript application across the stack
    • Same tools
    • Same language
    • Reusable logic
    • New constraints on old problems 
      • scaling Node servers
      • Cross-site AJAX requests

    Problems I didn't have time for

    Cross-site Requests (AJAX vs CORS)
    Client-side vs Server-side rendered JS apps
    The future of JS: Ecmascript 6
    Package Management (Bower and NPM)
    RequireJS and Browserify
    JavaScript Templating Engines
    JS Pub/Sub Architectures vs Mediators
    JS Source Maps
    Pro JS Debugging with Dev Tools
    Taming Async code with Promises
    Reliable Functionality with Testing

    Leveling Up your Javascript

    Read, Code, and Read Code

    • Professional JavaScript for Web Developers, Zakas
    • JavaScript: The Good Parts, Crockford
    • Eloquent JavaScript (free online)
    • Check out John Resig's Talks on Youtube
    • Codecademy's JavaScript track
    • JavaScript Weekly 
    • Watch JSConf talks on Youtube

    Thanks

    • Twitter: @mrjoelkemp
    • Email: mrjoelkemp@gmail.com
    • Website: www.mrjoelkemp.com

    Large scale Javascript

    By Joel Kemp

    Large scale Javascript

    • 1,104