Which makes me wonder how one decides where and when to get off the treadmill. Sadly I think it's a factor of age as much as technical merit. I hit the point where I felt that we'd reached perfection with Postgres to do relational plus the ability to go schemaless when it made sense, a nice MVC framework in a sane language such as Django or Rails, a smattering of client side magic where required using good old jQuery and finally something like PJAX or Turbolinks to give you that satisfying 'no page reload' feeling.
I saw a brilliant graphic a while back, but can't find it now. It shows 4 stages of development methodology in a big loop. It goes...
Spaghetti code -> MVC -> DI -> advanced framework
Personally, I embrace the suck and get off the roundabout at 'spaghetti code'. Define things as straight forward as I can as a sequence of instructions and abstract as little and as usefully as possible. Things that get repeated go into a function. Whole systems that get used more than once go into their own module. If I find myself copying and pasting between projects, it becomes its own library. If a library I'm using doesn't exactly fit my use case, I put it away and write a new one from scratch.
That last one was especially hard for me. It's so tempting to spend hours or even days on google searching for just the right library somebody else wrote that will fit your use case. Don't do it. Write it yourself. It'll be quicker and you'll learn more.
"...It'll be quicker and you'll learn more...." - Not to be snarky, but yeah - it'll be quicker to write up front, but you'll learn more...from finding and debugging all the edge cases the library authors may have already dealt with. And there goes the time you "saved" up front.
"may" being the key word here. The library you choose "may" have already dealt with those edge cases. Or you may spend hours or days debugging the god damned thing. In my experience in javascript development, the latter is far more likely.
but you'll learn more...from finding and debugging all the edge cases the library authors may have already dealt with. And there goes the time you "saved" up front.
That’s a common argument when you have a build-or-import decision, but that doesn’t necessarily make it correct.
If you’re talking about importing a well-established library that has substantial functionality and a large user base with a lot of collective experience then yes, if the functionality is a good fit for your project then it could be a big time-saver. Likewise if you’re talking about importing libraries from a carefully curated repository with good review, testing and maintenance practices.
However, in the modern JS world, many of the libraries that get pulled from GitHub/NPM and incorporated into projects are neither large and well-established in their own right nor part of a well-curated repository. In fact, many of them are written and (maybe) maintained primarily by a single developer, and in turn depend on other libraries also written and (maybe) maintained primarily by single developers.
In that case, it seems optimistic to assume that everything you’re bringing in will necessarily have higher quality than anything you’d write yourself. Certainly it might, but on the other hand, you could just be trading debugging someone else’s code for your own, and with someone else’s code you’re working with one hand tied behind your back. It might still be a good trade, depending on how much functionality the imported code offers and how much time it will save you, and depending on how much overhead you’ll incur finding and importing that code and adapting and maintaining your own code to match. Or it might not.
I've been using rollupify in my projects for a few months now with no issues, but I didnt know it didnt work on npm modules. I would be interesting to see benchmarks of rollupify vs rollup on ots own to see if its worth changing bundlers.
(rollupify author here.) It works if you configure Rollup to use a plugin like https://github.com/rollup/rollup-plugin-node-resolve, and it works best if the third-party packages also expose jsnext:main or the new "module" field in package.json.
Overall though there are still lots of edge cases that aren't well-covered without plugins (such as globals like process.nextTick etc.). Some details here: https://github.com/rollup/rollup/issues/552
This is really interesting. I think there's a lot of room for improvement in the tooling, though, and my gut instinct is that theoretically, Browserified/Webpacked code can be as fast if not faster than other compiled methods. The reason for this is that the bundler has more structural information about the code it's working with. More information theoretically equals more opportunities for optimization.
Not saying those optimizations will be easy. I'm marginally familiar with both the Browserify and Webpack codebases, and I certainly wouldn't want to be the one to implement any of this. But, assuming all the benchmarks in this post prove to be reproducible (and I'm expecting them to be), I hope that those more familiar with the tooling will be able to build a world where we get to have both the user experience and developer experience we want.
> the bundler has more structural information about the code it's working with
For CommonJS modules this is only true to a very limited extent as the export binding process can run arbitrary code which can't be analysed (it's Turing complete).
ES6 modules on the other hand are static and can be extensively analysed without difficulty. This makes, for example tree-shaking pretty easy. Rollup is an ES6 module builder which is doing this already. The future's bright.
The performance data is great to have, but gives me more reassurance than concern. Between browserify and closure compiler, we're looking at a 15ms to 40ms difference until we get well over 1000 modules. Even complex SPA apps I've written come in nowhere near 1000 modules, a few hundred at most.
Interestingly the performance difference between mobile and desktop on a fast connection was negligible.
Browserify and webpack are really developer friendly, so it's an easy business decision to sacrifice 15ms in overall load time and instead pre-hydrate the first page or use another trick to provide responsiveness while modules load.
I think front end dev process can still use plenty of improvement, but my primary takeaway here is to be conscious of the tools being used and the dependencies they import. It doesn't need to be a complex issue.
"Modules" refers to individual source files, not npm packages. A single npm package can contain many source files, each of which is a separate CommonJS module.
Most npm modules are small, so I'd expect in the average case that the majority of modules are actually your own app code.
Anything with assets expressed as JavaScript modules is easy to get to 100. Architectures that include compile-to-JS templates like hyperscript/react/jade. Or where there are many variations on a theme, like game characters and sprite components. But once we're into gaming, users are way more tolerant of load times.
Any standard CRUD app or BI dashboard should be nowhere near 1000, imho.
As I say in the post, I'm not sure it's safe to say "I have _n_ number of modules, therefore my overhead is the same as the '_n_ modules' benchmark." For runtime perf it's about the total number of require()s; the "_n_ modules" benchmark is more reflective of the bundle size cost.
E.g. consider the m.reddit.com example - 1050 modules, yet an order of magnitude slower than the "1000 modules" example. I might need to follow up with more benchmarks to reduce the confusion. ^_^;;
The problem isn't (necessarily) small modules, it's the _dynamics_ of the Common.js (and similar) module systems. By conflating fully general, extensible, dynamically typed objects with both namespaces and modules, the semantics can't be constrained enough to perform certain kinds of both intra- and inter-module optimizations at compile time.
By contrast, the Google Closure compiler's module system (also for JavaScript) does not suffer from this problem, but in tradeoff, disallows many types of runtime metaprogramming, such as merging modules with Object.assign or similar.
In haxe with static type system, dce and inling function we haven't this problem if we use haxe libraries.
The libraries can be really big or little, but dce inject only classes and methods used.
Same thing can be made with scala.js and clojure using google closure.
24 comments
[ 2.9 ms ] story [ 69.7 ms ] threadWhich makes me wonder how one decides where and when to get off the treadmill. Sadly I think it's a factor of age as much as technical merit. I hit the point where I felt that we'd reached perfection with Postgres to do relational plus the ability to go schemaless when it made sense, a nice MVC framework in a sane language such as Django or Rails, a smattering of client side magic where required using good old jQuery and finally something like PJAX or Turbolinks to give you that satisfying 'no page reload' feeling.
Get off my lawn...
That last one was especially hard for me. It's so tempting to spend hours or even days on google searching for just the right library somebody else wrote that will fit your use case. Don't do it. Write it yourself. It'll be quicker and you'll learn more.
That’s a common argument when you have a build-or-import decision, but that doesn’t necessarily make it correct.
If you’re talking about importing a well-established library that has substantial functionality and a large user base with a lot of collective experience then yes, if the functionality is a good fit for your project then it could be a big time-saver. Likewise if you’re talking about importing libraries from a carefully curated repository with good review, testing and maintenance practices.
However, in the modern JS world, many of the libraries that get pulled from GitHub/NPM and incorporated into projects are neither large and well-established in their own right nor part of a well-curated repository. In fact, many of them are written and (maybe) maintained primarily by a single developer, and in turn depend on other libraries also written and (maybe) maintained primarily by single developers.
In that case, it seems optimistic to assume that everything you’re bringing in will necessarily have higher quality than anything you’d write yourself. Certainly it might, but on the other hand, you could just be trading debugging someone else’s code for your own, and with someone else’s code you’re working with one hand tied behind your back. It might still be a good trade, depending on how much functionality the imported code offers and how much time it will save you, and depending on how much overhead you’ll incur finding and importing that code and adapting and maintaining your own code to match. Or it might not.
Overall though there are still lots of edge cases that aren't well-covered without plugins (such as globals like process.nextTick etc.). Some details here: https://github.com/rollup/rollup/issues/552
Not saying those optimizations will be easy. I'm marginally familiar with both the Browserify and Webpack codebases, and I certainly wouldn't want to be the one to implement any of this. But, assuming all the benchmarks in this post prove to be reproducible (and I'm expecting them to be), I hope that those more familiar with the tooling will be able to build a world where we get to have both the user experience and developer experience we want.
For CommonJS modules this is only true to a very limited extent as the export binding process can run arbitrary code which can't be analysed (it's Turing complete).
ES6 modules on the other hand are static and can be extensively analysed without difficulty. This makes, for example tree-shaking pretty easy. Rollup is an ES6 module builder which is doing this already. The future's bright.
Interestingly the performance difference between mobile and desktop on a fast connection was negligible.
Browserify and webpack are really developer friendly, so it's an easy business decision to sacrifice 15ms in overall load time and instead pre-hydrate the first page or use another trick to provide responsiveness while modules load.
I think front end dev process can still use plenty of improvement, but my primary takeaway here is to be conscious of the tools being used and the dependencies they import. It doesn't need to be a complex issue.
Most npm modules are small, so I'd expect in the average case that the majority of modules are actually your own app code.
Any standard CRUD app or BI dashboard should be nowhere near 1000, imho.
E.g. consider the m.reddit.com example - 1050 modules, yet an order of magnitude slower than the "1000 modules" example. I might need to follow up with more benchmarks to reduce the confusion. ^_^;;
Just to be sure, you're definitely counting modules using browserify-count-modules, not the number of directories in node_modules, right?
By contrast, the Google Closure compiler's module system (also for JavaScript) does not suffer from this problem, but in tradeoff, disallows many types of runtime metaprogramming, such as merging modules with Object.assign or similar.