Death to async require and script tags

94 views
Skip to first unread message

Adam Crabtree

unread,
Feb 9, 2012, 1:58:34 PM2/9/12
to amd-im...@googlegroups.com
This started as a response to the following thread, but was noticeably off topic, so I'm starting a new thread:



AMD top level would be nonsensical in that some client-side module loaders are using node's CommonJS 1.0 modules. Also, for the node community, the familiarity of require is IMHO one of the best assets browser loaders have toward widespread adoption.

Then again...

most AMD requires are asynchronous, and this is itself at odds with node's require.

In fact, it's my belief that the installation of modules (downloading) needs to be separated from the parsing and execution of modules, and that the asynchronous require conflates the issue. On that note, I'd like to see the AMD community move more toward a synchronous require with an asynchronous installer (script loader):

// download or load from localStorage, IndexedDB, FileSystem
// & parse (dynamic script injection or eval)
amd('jquery', function(require) {
    // execute or load from cache (a la node's require)
    var jquery = require('jquery');

});

The HUGE benefit of this pattern is a decoupling of module loading from module installation, something that is sorely needed IMHO. What this allows for is for the synchronous require to reach a de facto standard (alongside  a define implementation) that's shared amongst script loaders. This is the natural next step now that the AMD spec for module definition is nearing consensus. AMD makes no contributions to module loading strategies and module loading strategies should be decoupled from script loading so that they can be standardized.

One place to look for inspiration on this is the evolution that node took with its module loading. Initially, module loading was done via paths, but was assumed to be from file I/O with a couple resolution patterns when loading from a path: file, directory/index.js, directory/package.json::main. Eventually, the manual nature of setting paths proved more complex than beneficial, and the node_modules convention was born. Once node reached that point, module installation became a trivial issue of getting the scripts into the file system while adhering to the loading strategies of node. The browser environment however, seems to be stuck on its addiction to <script>s.

This is unsustainable. 

Future script loading will likely IMHO depart a great deal from the script loading paradigm of now in order to support greater flexibility like manual control over download, parse and execution. I expect the IO sources for scripts will move beyond <script>s to include as stated above CORS, FileSystem, IndexedDB, localStorage, WebSockets, Server-Sent Events (for push hot/swapping), WebRTC (P2P), etc...

Because of this, I think the script tag will move toward obsolescence and "async" and "defer" will be artifacts of an antiquated paradigm. AMD needs to move beyond the narrow module definition spec (which it already in effect is) to a much broader module loading spec. In addition, how those modules are first loaded into memory (eval, dynamic script injection, etc...) should be the boundary of its concern, left to the realm of script loaders to innovate. This is what I would like to see, and I think the whole of the ecosystem would flourish with this distinction as you'll find much of the push back from AMD esp. within the node community revolves around the dislike of the asynchronous require and the need to wrap in a define. This is jut plain silly. Nonetheless, a porting of a node style synchronous require with AMD define() support would almost certainly find swift adoption as module loading in the browser is still at the moment unnecessarily painful, requiring too much configuration, along the line of node's now abandoned require.paths.

Cheers,
Adam Crabtree


--
Better a little with righteousness
       than much gain with injustice.
Proverbs 16:8

James Burke

unread,
Feb 9, 2012, 3:31:55 PM2/9/12
to amd-im...@googlegroups.com
On Thu, Feb 9, 2012 at 10:58 AM, Adam Crabtree <atcra...@gmail.com> wrote:
> In fact, it's my belief that the installation of modules (downloading) needs
> to be separated from the parsing and execution of modules, and that the
> asynchronous require conflates the issue. On that note, I'd like to see the
> AMD community move more toward a synchronous require with an asynchronous
> installer (script loader):
>
> // download or load from localStorage, IndexedDB, FileSystem
> // & parse (dynamic script injection or eval)
> amd('jquery', function(require) {
>     // execute or load from cache (a la node's require)
>     var jquery = require('jquery');
>
> });

This is exactly the problem with trying to decouple fetching from
executing -- I have to type that 'jquery' string twice, and in this
case (the vast majority of cases), it was completely unnecessary.

require([]) is sufficient to give an indication of something that will
be used in the future but is not needed at the moment. For modules
referenced in require([]), a loader can choose to prefetch them before
those calls are actually executed, or a builder could choose to inline
them as strings for rehydration later, but I do not believe any new
module definition machinery is needed.

> Because of this, I think the script tag will move toward obsolescence and
> "async" and "defer" will be artifacts of an antiquated paradigm.

While that may be true, what is not changing is that IO in general in
the browser is async. Luckily the AMD API does not encode the
assumption of script tag use for IO, just that IO may be async.

> AMD needs to move beyond the narrow module definition spec (which it already in effect
> is) to a much broader module loading spec. In addition, how those modules
> are first loaded into memory (eval, dynamic script injection, etc...) should
> be the boundary of its concern, left to the realm of script loaders to
> innovate. This is what I would like to see, and I think the whole of the
> ecosystem would flourish with this distinction as you'll find much of the
> push back from AMD esp. within the node community revolves around the
> dislike of the asynchronous require and the need to wrap in a define. This
> is jut plain silly. Nonetheless, a porting of a node style synchronous
> require with AMD define() support would almost certainly find swift adoption
> as module loading in the browser is still at the moment unnecessarily
> painful, requiring too much configuration, along the line of node's now
> abandoned require.paths.

define() wrappers need to exist if you want to concat some modules
together. That will always be there. There needs to be a way to name a
code unit and to contain that code unit in some fashion. It is common
to deliver a set of modules in one file.

Other IO schemes may allow more finegrained control over the source so
you could delay execution, and that is fine, and it can allow for
sugar that desugars nicely to AMD.

If you want sugar to avoid a define wrapper in dev, use CJS-style,
although it really needs a callback-require for computed dependencies,
to recognize the async nature of IO in the browser, and to allow
prefetching. And loader plugins. It should "desugar" nicely to AMD
because AMD works for all loading cases. But it is also fine if a
loader understands higher level sugar.

If you want to standardize a higher level sugar, for me it is the
basics of CJS style, with callback require and loader plugin support.
Although, at this point, trying to get anything going like that is
likely to be superseded by ES harmony modules. In other words, to take
advantage of the new IO options, newer browsers are needed, probably
the same set that are on rapid updates that will likely get ES harmony
modules.

Conversely, there will be old browsers around for a while. AMD works
there, and I believe many forms of the ES harmony syntax could be
transpiled back into AMD.

I fully support doing experiments with different loading strategies. I
have a demo I want to do for one that uses stringified modules that
are all built into one file. I think it is great if people share their
strategies on this list, but it is difficult to see what exactly would
need standardization. Sharing techniques and approaches, even
documenting those approaches is useful though.

In particular, for splitting fetching from execution, loaders and
build tools that can detect the use of require([]) via source analysis
seem enough. If you do not think so though, feel free to share where
it breaks down.

James

Adam Crabtree

unread,
Feb 9, 2012, 5:48:09 PM2/9/12
to amd-im...@googlegroups.com
Hey James,

Based on your response, I don't think I did a very good job making my point. I think AMD is great and that the difference b/w CJS and AMD are trivial and more or less a solved non-issue. AMD, of course though, in the strict sense, is only a definition spec. However, it's evolving now to include the loader and configuration APIs:


This is a good thing, but I think the inclusion of async require and loader plugins is glossing over the intermediary step of resolving module loading. 

_I recognize this is a mostly semantic argument, but I feel it's a necessary distinction._

In other words, it seems that a lot of the attention around AMD loaders is around IO and not about loading. The reason I bother to bring this up is not bike-shedding, but for a specific purpose. That purpose is that I'd like to see all AMD loaders share the same define/require implementations. _A shared implementation of define() & local/sync require() would help AMD achieve greater consensus, and energies could be placed on loading APIs/sugar/strategies._ 

That's the most significant point I was trying to make.

I have my own personal implementations of define()/require() that for the most part seem incredibly straightforward and not something that has a lot of leeway diversity in the implementation. Most of my time building my own module loader has been on the IO strategy, and not on the require/define module loading.

Lastly, obviously if you love async require, more power to you, but with async require, there's no built in means to separate IO, parse and execute. If you have a recommendation for how you'd like to see async require support this, I'd be happy to hear it. That's really my primary concern regarding async require.

One option would be to borrow from the define() spec where if you included a funct param called "require" in your callback, then you wouldn't build the module until it was synchronously required. This seems a little awkward to me, though not awful as you're passing in a variable that's obviously already available in the closure since it's what called it the first place.

Cheers,
Adam Crabtree

James Burke

unread,
Feb 9, 2012, 7:15:39 PM2/9/12
to amd-im...@googlegroups.com
On Thu, Feb 9, 2012 at 2:48 PM, Adam Crabtree <atcra...@gmail.com> wrote:
> This is a good thing, but I think the inclusion of async require and loader
> plugins is glossing over the intermediary step of resolving module loading.
>
> _I recognize this is a mostly semantic argument, but I feel it's a necessary
> distinction._
>
> In other words, it seems that a lot of the attention around AMD loaders is
> around IO and not about loading. The reason I bother to bring this up is not
> bike-shedding, but for a specific purpose. That purpose is that I'd like to
> see all AMD loaders share the same define/require implementations. _A shared
> implementation of define() & local/sync require() would help AMD achieve
> greater consensus, and energies could be placed on loading
> APIs/sugar/strategies._

To me, the unit tests for AMD provides enough validation of a baeline
behavior for define and require (as used inside define). We do not all
have to use the exact same code algorithm. In fact, I do not see how
that would work. I don't have the same for requirejs and almond.
Almond actually supports "ordered" and "undordered" define calls, that
necessitate a slight variation in the define implementation.

I do think though that we have enough common understanding of how
define and require works that we can naturally move "up the stack" to
loader APIs, as you mention. Maybe that is your larger point.

I guess maybe if you can list out specific things that still need to
be worked out for define and require, maybe that would help me
understand where you think we are on the the path. But to me, trying
to standardize an algorithm for define/require does not seem to be
necessary. I am not aware of that actually being a blocker to AMD
adoption. The larger issue is just educating developers on modular JS
vs using a bunch of browser globals.

If you think the deficiency with define/require is about separating
io, parse and execution, I'll expand on that more below.

> Lastly, obviously if you love async require, more power to you, but with
> async require, there's no built in means to separate IO, parse and execute.
> If you have a recommendation for how you'd like to see async require support
> this, I'd be happy to hear it. That's really my primary concern regarding
> async require.

I mean that require([]) already gives enough information to indicate
when a developer wants to possibly separate IO from parsing or
executing. Example:

define(function (require) {
var a = require('a'),
b = require('b');

return {
doSomething: function (cb) {
require(['c', 'd'], function (c, d) {
cb(c() + d());
});
});
};
});

This module needs 'a' and 'b' immediately, so they should be
fetched/executed when this module is fetched executed. However, a
loader could decide to introspect and see that 'c' and 'd' are needed
at some point. It can choose to just fetch them now and put them in
localStorage, or just hold them in strings. It could even parse them,
but not execute them, to find out that 'c' and 'd' also need 'e', and
do the IO fetch for 'e' too, but not execute it.

Then when that require([]) call is run, the loader knows to then
execute the code.

Similarly, a build tool could parse the module above, and include this
module and 'a' and 'b' in a built file, but then put 'c', 'd', and 'e'
as strings in that built file. So that a loader that understood that
built format would know how to unpack that information, likely by
having the builder include a specialized loader in that built file.

The builder could also just decide to put 'a' and 'b' as strings in
the built file too, and only evaluate them when the above module
executes, but that does not seem so useful since the current module
would need them right away.

If you think that AMD should adopt CommonJS modules 1.1. behavior of
not executing the dependency until there is an internal require(),
that goes against AMD 's define(['dep']) syntax, and how ES harmony
modules, as currently specified will work. It would also bloat the
basic API, as your previous example showed. It adds a global tax for
something that is very rarely needed.

And it does not help as much compared to require([]) when IO is async.
If any module is not needed until "later", then it really should not
even be fetched for best performance, and the use of a callback-style
require is enough of an API to signal to a loader that the dependency
is not needed right away, and that loader can decide if it wants to do
some prefetching or other analysis before the module is actually
needed.

James

Rawld

unread,
Feb 22, 2012, 1:55:11 PM2/22/12
to amd-im...@googlegroups.com
fwiw, I strongly agree with both James' responses to this thread. Also...

* In spite of Node's popularity, the volume of browser-side JavaScript
outweighs the volume of server-side JavaScript by at least an order of
magnitude. The cjs module loader [used by Node] does not fulfill the
requirements of the larger js user set. This is a cjs mistake, not an
AMD mistake. There is no reason server-side module loading couldn't
evolve to amd, if one is looking for unification.

* It may very well be true that future brower-side JS capabilities will
render AMD obsolete. But today those solns don't exist. AMD solves the
problem and many are using it with great success.

* I fail to see how AMD uptake is being harmed in any way by it's
current design and incompat with cjs. Indeed, the async feature is the
raison d'�tre for AMD. It's not going to change.

--Rawld

On 02/09/2012 04:15 PM, James Burke wrote:
> On Thu, Feb 9, 2012 at 2:48 PM, Adam Crabtree<atcra...@gmail.com> wrote:
>> This is a good thing, but I think the inclusion of async require and loader
>> plugins is glossing over the intermediary step of resolving module loading.
>>
>> _I recognize this is a mostly semantic argument, but I feel it's a necessary
>> distinction._
>>
>> In other words, it seems that a lot of the attention around AMD loaders is
>> around IO and not about loading. The reason I bother to bring this up is not
>> bike-shedding, but for a specific purpose. That purpose is that I'd like to
>> see all AMD loaders share the same define/require implementations. _A shared

>> implementation of define()& local/sync require() would help AMD achieve

Reply all
Reply to author
Forward
0 new messages