Skip to content

Instantly share code, notes, and snippets.

@polotek
Last active August 29, 2015 13:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polotek/9383409 to your computer and use it in GitHub Desktop.
Save polotek/9383409 to your computer and use it in GitHub Desktop.

Today I had a brief debate on twitter about AMD vs CommonJS, https://twitter.com/TechWraith/status/441387541778808832. It's a debate worth having for sure. But I had to bail out of this one. The thing that annoyed me is that the first argument that people bring up to disquality AMD is "the syntax is too complex". I disagree with this. There are lots of reasons to prefer CommonJS over AMD, but the module authoring syntax is not a very good one. There was also a related statement that AMD authoring introduces more "cognitive overhead". This is absolutely true. But I don't consider this to be synonymous with "complexity" by any means.

So I thought I'd explore some comparable examples. Here's a simple one that was offered up by someone else in the thread. This was on twitter so I can forgive erring on the side of brevity.

AMD

define('myThing', ['some', 'deps'], function (some, deps) {
  //my code
  
  return myThing;
});

CommonJS

module.exports = myThing

The problem is that the CommonJS example leaves out dependencies. And you don't actually need to list the name in AMD. In fact it's discouraged. Let's fix that up shall we.

AMD

define(['some', 'deps'], function (some, deps) {

  //my code
  
  return myThing;
});

CommonJS

var some = require('some'),
  deps = require('deps');
  
//my code

module.exports = myThing

When we're actually comparing all of the right things, it's clear that there isn't a ton of difference here. But this is still a contrived example. IMO, there isn't enough here to constitute any cognitive overhead either way. Let's expand to a non-trivial example. This a snippet of real AMD module code in Yammer's front-end today. And I've adapted a comparable CommonJS example.

AMD

define([
  'yam.$',
  'yam._',
  'Mustache',
  'feeds/lib/ui/messages/message_list',
  'feeds/lib/ui/publisher/thread_starter_publisher',
  'feeds/lib/ui/feeds/spinner',
  'feeds/lib/ui/feeds/no_items_notice',
  'feeds/lib/ui/feeds/new_items_notice',
  'feeds/lib/ui/feeds/feed_toggle',
  'common-ui/lib/ui/shared/more_button'
], function (
  $,
  _,
  Mustache,
  MessageList,
  ThreadStarterPublisher,
  Spinner,
  NoItemsNotice,
  NewItemsNotice,
  FeedToggle,
  MoreButton
) {
  // lots of code
});

CommonJS

var $ = require('jquery'),
  _ = require('underscore'),
  Mustache = require('mustache'),
  MessageList = require('feeds/lib/ui/messages/message_list'),
  ThreadStarterPublisher = require('feeds/lib/ui/publisher/thread_starter_publisher'),
  Spinner = require('feeds/lib/ui/feeds/spinner'),
  NoItemsNotice = require('feeds/lib/ui/feeds/no_items_notice'),
  NewItemsNotice = require('feeds/lib/ui/feeds/new_items_notice'),
  FeedToggle = require('feeds/lib/ui/feeds/feed_toggle'),
  MoreButton = require('common-ui/lib/ui/shared/more_button');

  // lots of code

Now this is a good comparison. And we can draw some conclusions here

  • When dependency lists get large, the AMD syntax needs to spread out to stay readable.
  • Listing deps twice in AMD will get tedious at larger scale. This is the definition of boilerplate, and it is ugly.
  • Writing out long paths make both examples way more dense. But this is likely to be the case for legacy codebases and those who don't go all in on npm for deps.
  • Adding a dependency is much easier with CommonJS. It's just an assign statement. With AMD you have to add a line in 2 separate places, and they have to match up in order. And don't forget your commas. That sucks. cognitive_overhead++
  • The CommonJS example doesn't actually have less information in it. You still have module names/paths and local variable names. CommonJS lets you keep those pairs on one line and associated together. AMD makes you split them up. That also sucks. cognitive_overhead++
  • Yammer currently has way too many dependencies per module. This is a consequence of legacy. I don't know if this would by typical if you were able to start off with a better module system in place. Here's an example of a hefty file from geddyjs for comparison. https://github.com/geddy/geddy/blob/master/lib/controller/base_controller.js
  • The AMD example looks scary. (This is true, but inadmissable as evidence.)

So I look at these examples and come to a conclusion. And remember we're only focusing on module syntax. The CommonJS style clearly has advantages. It's the style I prefer the most. But the AMD style is not bad. It's kind of annoying. But it's not so much "cognitive load" that I would graduate it to "complex". It's just javascript. It's the same module pattern we've been using for ages, just with more arguments thrown in to be more explicit. To claim that an average developer is going to be so overwhelmed by this that they will regret choosing AMD is not an argument that a reasonable person would make.

It's not more problematic than nested async callbacks. It's not more problematic than the node callback style with if(err) { return callback(err); } absolutely everywhere. And it's not more problematic than what we put up with from the async module, https://github.com/caolan/async/blob/master/test/test-async.js#L267-L302. And everybody thinks that thing is the bees knees. Cognitive overhead indeed.

Now let me step back for a second. If I was starting a project today, I would still choose CommonJS and browserify over AMD. But for different reasons.

  • The niceties that AMD gives you for async loading only come in handy when your app gets very large. Requirejs, the de facto AMD implementation, IS complex when you take it in total. It's not what I would want to get started with.
  • I only know a handful of people who actually take advantage of async loading with AMD. Even Yammer is still working up to it. The barrier to actually getting there is higher than you think it's going to be.
  • CommonJS is the format used in node. If you're using node anyway, there is benefit to having only one module format in your projects.
  • npm is the best package manager there is. I want to use it for all my javascript things. Always. This is a personal bias, and I'm okay with that.

I'm gonna wrap this up. But I want to make the point that prompted me to write this. The people I had this debate with are friends of mine, and I have a lot of respect for them. But they fell into the trap that I see so many programmers fall into when trying to debate technical things. We take arguments that aren't really an issue in practice, often coming down to personal preference, and we blow them up into reasons that technology X should die in a fire. We love throwing things into metaphorical fires based on trivial shit. Always in pursuit of that ever elusive One True Way.

And that's cool. It's a quirk of ours. Some even find it endearing. But I'm kind of over it. And if I feel an argument going that way, you shouldn't be surprised if I choose to exit abruptly.

Bonus Here's the non-trivial example using some es6 features. This is completely unvetted.

import $ from 'jquery';
import  _ from 'underscore';
import Mustache from 'mustache';
import MessageList from 'feeds/lib/ui/messages/message_list';
import ThreadStarterPublisher from 'feeds/lib/ui/publisher/thread_starter_publisher';
import Spinner from 'feeds/lib/ui/feeds/spinner';
import NoItemsNotice from 'feeds/lib/ui/feeds/no_items_notice';
import NewItemsNotice from 'feeds/lib/ui/feeds/new_items_notice';
import FeedToggle from'feeds/lib/ui/feeds/feed_toggle';
import MoreButton from 'common-ui/lib/ui/shared/more_button';

// lots of code

export default SomeThing

Eerily reminiscent of java. Only slightly shorter than CommonJS. Very clean though.

@domenic
Copy link

domenic commented Mar 6, 2014

Nice article. I agree with pretty much all of it.

On the ES6 example: module 'SomeThing' { and the closing brace are not in ES6. And you probably want export default SomeThing.

@polotek
Copy link
Author

polotek commented Mar 6, 2014

The module wrapper isn't supported at all? That's surprising What happened to it? I thought having a blog scope around a module was one of the primary goals of the spec. Does this mean it's always one module per file like CommonJS?

@dangoor
Copy link

dangoor commented Mar 6, 2014

I was going to suggest that all of the arguments between CommonJS and AMD modules had been laid out on the CommonJS list in 2009 (which is true), but this is a way more succinct breakdown of it! Nicely done.

In so many things we encounter, we're arguing about having 6 of something vs. having a half-dozen of it. I'm hoping that ES6 will put this all to bed once it starts getting traction in real environments.

@polotek
Copy link
Author

polotek commented Mar 6, 2014

@domenic I updated the es6 example. Can you point to any history that explain why the module wrapper was removed? I don't have a problem with it. Just curious about the reasoning.

@conradz
Copy link

conradz commented Mar 6, 2014

Note that you can compromise with RequireJS by using the CommonJS-style wrapper:

define(function(require) {
  var some = require('some'),
    deps = require('deps');

  return myThing;
});

This might help to slightly lower the cognitive overhead, although your other reasons definitely still remain.

@jrburke
Copy link

jrburke commented Mar 6, 2014

Some things to help in your comparison: AMD supports a sugared syntax:

define(function(require) {
  var a = require('a');
  return function(){};
});

The AMD folks, including me, should have done a better job talking about this one. The dependency array is just a baseline expression of the semantics underneath.

And speaking of the semantics, if you believe that a module system for the browser should allow loading individual modules (not a given in the node community), it means the dynamic, sync require(variableName) is really not supportable.

I expect it falls over even in the browserify case, unless the developer makes a point to include all possible variableName modules in the built output manually. The ES6 semantics recognize this though, and have a proper allowance for an async way to get those kinds of dependencies.

I do not plan on reading/posting more here, but just giving some feedback on the syntax issues mentioned above. I do hope ES6 does help us avoid having to talk about it more, and do appreciate it would be great to not have a function wrapper like AMD does for module sources.

@bclinkinbeard
Copy link

Edit: LOL, I guess I should have refreshed the page before commenting. I didn't see the previous two before submitting mine.

I'm a rabid fan of CommonJS and Browserify, but if I were forced to use AMD I'd go with the "simplified CommonJS wrapping" syntax.

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

@polotek
Copy link
Author

polotek commented Mar 6, 2014

I'm aware of the CommonJS compatibility syntax. It was out of the scope of this piece.

@millermedeiros
Copy link

protip: if your module have more than 5 dependencies, you are probably breaking the single responsibility principle; split the logic into multiple (smaller) modules. - ideally, your files should have <200 LOC.

@medikoo
Copy link

medikoo commented Mar 6, 2014

And here's how load time in dev setup of both compares.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment