jspm / jspm-cli

ES Module Package Manager
https://jspm.org
Apache License 2.0
3.78k stars 272 forks source link

Performance with many files #872

Closed bernhardw closed 5 years ago

bernhardw commented 9 years ago

Hello,

Thanks a lot for all the great work on jspm :thumbsup:

During development, when importing libraries split across many files such as React or Angular2, es6-module-loader loads all the required files individually which takes around 2-4 seconds. It helps to import a provided build instead, but not all packages include one.

Is there a suggested way around it?

guybedford commented 9 years ago

There are a number of approaches here, but no prescribed workflow currently.

In an ideal world the development server would be a fast HTTP/2 server with incremental caching understanding SystemJS and doing server-side transpilation and providing hot reloading.

Note that just using a better HTTP server for development can make a lot of difference. Some of them are very badly optimized for large numbers of requests.

@geelen is doing some amazing work on these sort of ideas in http://npmjs.org/package/jspm-server, but I don't know how ready it is.

While we wait for the future, the best workflow is simply to have common bundles that you load at the top of your app:

  jspm bundle app/**/* - [app/**/*] dependency-bundle.js

You then include dependency-bundle.js at the top of the page, and you'll only be loading your own application code as separate files in the browser.

Other users have quite advanced pre-compilation gulp tasks to do ES6 transpilation locally before loading separate files in the browser.

It's an area where users are still on there own for now, so please share what you learn with others as we collectively refine these workflows.

bernhardw commented 9 years ago

Thank you Guy for the detailed answer. I was hoping to get rid of my pre-compilation tasks during development, but creating a dependency bundle works for now.

And with --inject, you don't even need to include your script manually:

jspm bundle app/**/* - [app/**/*] dependency-bundle.js --inject
geelen commented 9 years ago

jspm-server is definitely usable right now, and I'd love feedback on whether it improves your workflow. Basically, you can mark files as being "safe" to live-reload, and if those files change it won't have to reload the whole browser. It's still a work in progress, and will definitely benefit from changes coming down in the next version of JSPM & SystemJS, but right now I'm using it for all of my projects.

That said, doing the dependency bundling will definitely help as well, so that's a neat trick. I'd love there to be a higher-level command for doing that, something like jspm bundle-deps that bundles everything in jspm_packages? Would that be possible?

Also, @guybedford, you mention "better HTTP servers". Do you know of any? I want to find the best and build jspm-server around it. I don't know anything that supports HTTP/2 either. I have an issue for it here: https://github.com/geelen/jspm-server/issues/4

OrKoN commented 9 years ago

@geelen I am using nginx with spdy support here: https://github.com/OrKoN/jspm-ember-playground It's faster than any node-based servers I used. But I guess it would be hard to package nginx into the jspm-server.

Also there is node-spdy server https://github.com/indutny/node-spdy and https://github.com/molnarg/node-http2 which is not fully working right now (see https://github.com/molnarg/node-http2/issues/114).

OrKoN commented 9 years ago

I have also written a blog post about optimizing the performance with many files using gulp and nginx https://60devs.com/optimizing--default-jspm-workflow-with-gulp-and-nginx.html

@geelen I wonder if jspm-server can replace the gulp stuff I have. Is there a tutorial on how to migrate an app to jspm-server? Is there gulp-jspm-server or smth similar?

geelen commented 9 years ago

There's not really, jspm-server is sort of designed to be run independently of gulp or similar things.

I'd love to get a version of nginx statically linked and shipped inside an NPM package :smiley: I wonder if that's possible...

OrKoN commented 9 years ago

@geelen I think this should be possible similar to this https://jamie.curle.io/posts/webfaction-installing-custom-nginx/ But I guess it needs to be compiled on npm install.

bernhardw commented 9 years ago

I've set up a simple static file server based on http2 to play around, but I see no difference in performance between http2 and http. It doesn't seem to multiplex, but HTTP/2 is used. Any ideas why?

jpray commented 9 years ago

Regarding solutions other than HTTP/2... it seems that if you want even one unbundled ES6 module to be loaded while developing, then babel is what is affecting the load time because it dynamically loading dozens and dozens of dependencies. @guybedford, is there a way we can bundle the modules for babel into a single file?

Update: Tried it out and yes, babel can be bundled.

jspm bundle babel .tmp/js/bundles/babel.js

---index.html---

  <script src='/jspm_packages/es6-module-loader.js'></script> <!-- optional: to load ES6-style modules -->
  <script src='/jspm_packages/system.js'></script><!-- required -->
  <script src='/config.js'></script><!-- required -->
  <script src='/.tmp/js/bundles/babel.js'></script>
OrKoN commented 9 years ago

@bernhardw According to my tests there is no difference too. I guess http/2 may be helpful in production or if the server leverages the server push technologies. I have tried http2 module you mention but it didn't work with Chrome as a client. How did you get it work?

bernhardw commented 9 years ago

@OrKoN I also couldn't get serve-static to work, but it did work with the less used node-static.

var fs = require('fs');
var http2 = require('http2');
var fileServer = require('node-static');
var file = new fileServer.Server('./public');

var options = {
    key: fs.readFileSync('./localhost.key'),
    cert: fs.readFileSync('./localhost.crt')
};

var server = http2.createServer(options, function (req, res) {
    req.addListener('end', function () {
        file.serve(req, res);
    }).resume();
});

server.listen(8080);
bernhardw commented 9 years ago

@jpray Thanks, that did bring it down from ~1200ms to ~750ms. For anyone else trying to bundle the babel runtime, you can and should disable the runtime in your config.js:

System.config({
  "baseURL": "/",
  "transpiler": "babel",
  "babelOptions": {
    // "optional": [
    //   "runtime"
    // ],
    "blacklist": []
  },
  "paths": {
    "*": "*.js",
    "github:*": "jspm_packages/github/*.js",
    "npm:*": "jspm_packages/npm/*.js"
  }
});

That seems to be all there is to shave off. These are my network requests now:

network
guybedford commented 9 years ago

Two perf tips along these lines, and why this is hard currently:

  1. Finding a performant HTTP/2 server that does in-memory caching of files is hard. If you know any, please share :)
  2. When serving jspm_packages, it should actually be served with a far-future expires header so that it is cached in the browser and never requested again. That way just the local application folders are refreshed. Again, knowing to set this and how to set it is not obvious when running a dev server.
lookfirst commented 9 years ago

The caching is really complicated though cause in dev mode I currently have to constantly flush the cache (using a nice chrome extension i found for single click expiration) because my .map files are heavily cached and out of date all the time. Any ideas on that one?

guybedford commented 9 years ago

@lookfirst only cache jspm_packages, not anything else (the code you write yourself). Because paths in jspm_packages are versioned, updates get forced. Linking workflows break down here, and need custom rules though.

lookfirst commented 9 years ago

@guybedford Right, but my point is that right now, I'm hard flushing the entire chrome cache every time I reload the browser in order to get .map files to work properly. That would kill any benefit of caching jspm_packages.

guybedford commented 9 years ago

I guess a no-cache header on the .map files?

lookfirst commented 9 years ago

Great idea! =) Lol... (feeling dumb, i should have thought of that)...

On Wed, Jul 8, 2015 at 10:53 AM, Guy Bedford notifications@github.com wrote:

I guess a no-cache header on the .map files?

— Reply to this email directly or view it on GitHub https://github.com/jspm/jspm-cli/issues/872#issuecomment-119677065.

lookfirst commented 9 years ago

Sad face. https://github.com/BrowserSync/browser-sync/issues/719

On Wed, Jul 8, 2015 at 11:38 AM, Jon Stevens latchkey@gmail.com wrote:

Great idea! =) Lol... (feeling dumb, i should have thought of that)...

On Wed, Jul 8, 2015 at 10:53 AM, Guy Bedford notifications@github.com wrote:

I guess a no-cache header on the .map files?

— Reply to this email directly or view it on GitHub https://github.com/jspm/jspm-cli/issues/872#issuecomment-119677065.

geelen commented 9 years ago

@lookfirst have you looked into jspm-server? We should be able to add some caching rules in there (always cache jspm_packages, never cache .map files) pretty easily, no?

lookfirst commented 9 years ago

I'd rather use browsersync as the basis. I wish your server just extended that. I also don't really see the value proposition of jspm-server over what I've already got setup.

On Wed, Jul 8, 2015 at 3:38 PM, Glen Maddern notifications@github.com wrote:

@lookfirst https://github.com/lookfirst have you looked into jspm-server https://github.com/geelen/jspm-server? We should be able to add some caching rules in there (always cache jspm_packages, never cache .map files) pretty easily, no?

— Reply to this email directly or view it on GitHub https://github.com/jspm/jspm-cli/issues/872#issuecomment-119751010.

geelen commented 9 years ago

Well, it's aware of the dependencies within your SystemJS build, so it's able to live-reload modules within your application without having to reload the browser, which will always be quicker. But basically, I want a development server for JSPM that's as fast as possible out the box, without requiring any configuration (beyond adding some export let __hotReload = true statements around the place)

I had hoped I could port jspm-server to be an extension of BrowserSync but I think that might be too much work for not much gain at this point.

lookfirst commented 9 years ago

The gain is the functionality of browsersync without having to do much work. If your code could live within the context of a middleware, that would be optimal.

I'd worry the live reload of modules would break angular, no? It also seems like there could be potential for stuff to get messy there. I'd hate to have to debug a reload failure. Also adding __hotReload to all my files isn't very ideal.

So far, what I've got setup is pretty darn easy and fast. Gulp manages the watching of files and only recompiles ones (es6/less/templates) that change. I use my gulp-helpers so I don't have to rewrite gulp files ever time I start a new project.

https://github.com/lookfirst/gulp-helpers

Now, just waiting to see if browsersync can set the headers correctly in a middleware and I'll be all set.

Thanks for piping in... I did look at jspm-server, but it just didn't scratch my itch enough. =)

On Wed, Jul 8, 2015 at 4:25 PM, Glen Maddern notifications@github.com wrote:

Well, it's aware of the dependencies within your SystemJS build, so it's able to live-reload modules within your application without having to reload the browser, which will always be quicker. But basically, I want a development server for JSPM that's as fast as possible out the box, without requiring any configuration (beyond adding some export let __hotReload = true statements around the place)

I had hoped I could port jspm-server to be an extension of BrowserSync but I think that might be too much work for not much gain at this point.

— Reply to this email directly or view it on GitHub https://github.com/jspm/jspm-cli/issues/872#issuecomment-119757800.

geelen commented 9 years ago

Yeah, angular makes it tough. I don't know exactly how to handle reloading there. And even with jspm-server, there's always times you have to fully reload, and the speed is still an issue.

@guybedford back to the original point. Could we do something silly like run SystemJS in a serviceworker? Have it manage transpilation & caching on the client side, not the server? It seems like a good fit to me, but maybe I'm missing something...

guybedford commented 9 years ago

@geelen my previous plan was to build a jspm server that did the plugin transpilation and es6 transpilation. Basically it would run the loader pipeline on the server first, and just send the ready sources to the client. Then you get browser-cached transpiled sources, and it's a solid foundation development server to use with the native loader too in future.

lookfirst commented 9 years ago

@guybedford I thought about that as well and in practice, it was just easier to setup gulp with browsersync. If running the loader pipeline on the server first means also getting a single file to the server, then that doesn't work well for me cause it makes it harder to debug things. The individual files are nice during development so that I can quickly see which file has issues and go to that in my IDE.

geelen commented 9 years ago

So the native loader won't do any transpilation?

That feels like a use-case that's already covered pretty well but the webpack dev server, and there are plenty of issues with it. Reloading (they call it Hot Module Replacement) is pretty complex when you have to serialise a chunk down to the browser, in particular. When the loader knows it's running in the browser (except in bundle mode of course) it can be a bit smarter, in my mind.

That's why I've thought in-browser caching would be the way forward. But you're right, a serviceworker is disconnected enough from the browser context that it'd be the same problems as I just mentioned. So maybe just adding some caching of transpiled sources into SystemJS (and in development only) is the simpler approach?

guybedford commented 9 years ago

@geelen the reason I liked the server though was that it would be equally useful in development and production. I do still plan to work on this over the coming months, but it will be a little while unfortunately. With browser caching we have a development-only optimization, that may be confused for a production workflow.

The above server is also the natural home for hot reloading, so would be nice to merge efforts. What about the serializing gets complex there? You have been working on this problem far more than me though, so interested to hear more from your experiences.

Also in terms of the future of hot-reloading, had an interesting chat with @paulirish, where he described a workflow of editing ES6 in the Chrome devtools and having that hot-reload. V8 optimizations can do a lot more there than any other approach (code updates without re-execution) so it seems like this is where things should go for hot-reloading eventually. This workflow would then (in some distant future where V8 supports modules) be based on the loader listening to the edit event of a transpiled module from devtools, and then sending back the transpilation to be hot-swapped without refreshing the page. Food for thought :)

lookfirst commented 9 years ago

Well, this should help... https://github.com/BrowserSync/browser-sync/issues/719

guybedford commented 9 years ago

:+1:

lookfirst commented 9 years ago

If you're following the above browsersync issue, I've submitted the PR's and they should get integrated tomorrow or the next day. Once those are in, I'll add the middleware to the gulp-helpers project. If you use that middleware, then proper browser caching of .map and jspm_packages files will be a great speed improvement without having to resort to crazy hot reload optimizations, etc... I like browsersync, it works great and is very very well tested... we should all focus on using that and not inventing more projects...

Munter commented 9 years ago

I have a strong feeling that all the tools I've been working on previously can fit into this space and make the development experience faster.

Fusile can set up a directory for you that mirrors your web root. When you read from it, the sources are transpiled, sourcemapped, autoprefixed, cached and watched. It exposes a file system as an API, so any static file server hooks right into it and gains all the benefits of static file caching.

staticache is an experiment in keeping an always updated dependency graph of your entire code base alive in an express middleware. It serves files from memory and with a far future cache expiry. By watching all files it can let file updates trigger cache bustin on only the relevant assets by trickling up md5 hashes in all urls that point to it. This will trickle up the dependency graph till it hits the root nodes. Initial load will be slow and load all files, but next reload will only load the entry point plus the path to any updated assets.

expush is an express middleware that uses an in memory dependency graph of your static assets to do spdy push of the entire sub-graph you need for each request. This speeds up loads of many files in development both because of multiplexing, but also because a runtime module loader won't have to wait for the whole download, transpile, parse chain to finish before being able to incrementally discover the next dependency

geelen commented 9 years ago

This could be really really cool, I've been hoping we could do something together @munter :)

My vision basically involves the following:

I think the final thing should be called jspm-server, but I don't really mind how it's built or who maintains it. I just want it to have as nice an API as JSPM does :)

How does that sit with everyone?

Munter commented 9 years ago

I agree on those points. I want to augment the workflow on the premise of jspm and systemjs. The tools should be able to do the right thing based on configuration read out of package.json, config.js and possibly tool specific configuration.

Areas that are possible to improve on without having any impact on how you write code:

Areas that are possible to improve, but might need some extra configuration:

Anything I missed?

lookfirst commented 9 years ago

Ok, my changes are now in browsersync and I'm happy to report that WEEEE, things are much faster now and it is clear that the caching and expiration is working as expected. No more clearing the browser cache every time I change my .js files so that the map files work correctly!

Non-scientific speed tests: On a load of a single page with 131 requests (mostly from jspm_packages) and 2.6MB transferred, it takes 1.98s with a cleared cache. With a reload after the cache has been populated, it is 1.10s and 160KB transferred.

let expireHeaders = (proxyRes) => {
    if (proxyRes.req.path.indexOf('.map') !== -1 || proxyRes.req.path.indexOf('/js') !== -1) {
        proxyRes.headers['expires'] = 'Thu, 01 Dec 1983 20:00:00 GMT';
        proxyRes.headers['cache-control'] = 'public, max-age=0, no-cache, no-store';
    }
    if (proxyRes.req.path.indexOf('jspm_packages') !== -1) {
        proxyRes.headers['expires'] = 'Thu, 01 Dec 2021 20:00:00 GMT';
        proxyRes.headers['cache-control'] = 'private, max-age=3600';
    }
};

Use that function in your browsersync configuration.

    proxy: {
        proxyRes: [expireHeaders]
    }

Turn on browser caching even when devtools is open so that the browser caches files correctly.

So while I'm sure there is more optimizations that can be had, this makes me really really happy.

iksose commented 9 years ago

Anyone know how he's accruing these metrics, and if there's plans to get jspm to competitive with webpack? I assume he's using bundle-sfx, since he's a core angular contributor. But I do know this includes a "micro-loaded implementation" which may be causing slow loads?

https://www.reddit.com/r/angularjs/comments/3da3fe/angular2_webpack_starter_updated_to_alpha31/ct85ouk

guybedford commented 9 years ago

Thanks, I've posted a reply at https://www.reddit.com/r/angularjs/comments/3da3fe/angular2_webpack_starter_updated_to_alpha31/cta2mgx.

logiclabs commented 9 years ago

For a HTTP/2 server, try Caddy: https://caddyserver.com/

NervosaX commented 9 years ago

@logiclabs It does not support push yet: https://github.com/mholt/caddy/issues/144 https://github.com/bradfitz/http2/pull/39

f15gdsy commented 9 years ago

@guybedford Hi, In terms of using

jspm bundle app/**/* - [app/**/*] dependency-bundle.js

I got this error when running it on terminal:

err  Expected operator before app/src/components

Any ideas?

guybedford commented 9 years ago

@f15gdsy try putting quotes around the 'app/**/* - [app/**/*]' part for the terminal.

f15gdsy commented 9 years ago

@guybedford It works! Thanks! It reduces the compile time a lot! But I'm wondering if there is a way to reduce it further? Like only compile what's changed in app/ and cache the unchanged ones?

lookfirst commented 9 years ago

@f15gdsy That sounds terrible. You should really only bundle for production. Your production should be built using a CI server. Doing that would be asking to do caching on your CI server which seems like a dangerous and brittle thing.

f15gdsy commented 9 years ago

@lookfirst Hi, I'm not sure what you mean. What I'm trying to do is to reduce the time to get my changes in code reflect to browser during development. Here is what I'm doing in index.html

<script src="./jspm_packages/system.js"></script>
<script src="./config.js"></script>
<script>
    System.import("./build/dependency-bundle.js");
    System.import("./app/src/components/App/App.js");
</script>

where dependency-bundle.js is all the dependencies of what's inside ./app, and App.js is the entrance of the application.

So with the help of

jspm bundle app/**/* - [app/**/*] dependency-bundle.js

I don't need to compile the dependencies again when changing what's inside ./app. But I still need to compile everything inside ./app. So my question is is it possible to cache the unchanged code in ./app and only compile the changed ones to reduce compile time.

guybedford commented 9 years ago

@f15gdsy yes this completely a valid approach for development performance as the bundle doesn't need to be refreshed that regularly. Ideally such a bundle shouldn't be checked in to version control certainly though.

Better workflows around development with granular refreshing should exist certainly! If anyone is interested in working on these problems please do let me know. SystemJS builder does support build caching, so setting this up as a custom build task via require('jspm').Builder would be able to get the compile time down using the trace and compile caches (builder.getCache, builder.setCache). These features are still somewhat experimental so not currently documented, but let me know if you need further guidance here.

lookfirst commented 9 years ago

"What I'm trying to do is to reduce the time to get my changes in code reflect to browser during development."

I write my code in ES6, so what works great for me is to run a gulp based build that sits and watches for changes and feeds the changed file(s) through babel. This change watching is smart and only recompiles exactly the files that change. So, I just made a change to app.js and this is the output:

[00:47:05] File hub/src/main/js/home/app.js was changed, running tasks: babel
[00:47:05] Starting 'babel'...
[BS] 1 file changed (app.js)
[00:47:06] Finished 'babel' after 754 ms

I have that integrated with browserSync [BS] which watches for changes on my compiled files so that I don't even have to reload my browser, it just reloads automatically. I use Intellij with auto save turned on which saves me the step of even saving the file.

Any bundling that I do with jspm happens as a production step only.

All of this setup with gulp is made trivial with my gulp-helpers project, it is literally one line in my gulpfile and since I use angular, it even handles the ngAnnotation for me...

taskMaker.defineTask('babel', {taskName: 'babel', src: path.source, dest: path.output, ngAnnotate: true, compilerOptions: babelOptions, watchTask: true, notify: true});

The notify: true is also awesome in that I get operating system level notifications when there are compile failures in babel.

Using the browserSync caching header code referenced above, the browser ends up getting back mostly 304 headers for each request.

Sure, you can bundle all your dependencies into a separate file, but I just don't see any need to do that since I have the proper http caching headers turned on. I'm literally loading hundreds of files for each request and things happen quickly enough in development mode that it really isn't an issue.

guybedford commented 9 years ago

@lookfirst thanks for these points, sound like you have a nice workflow going! How are you refreshing SystemJS in the browser without reloading here, or is that specifically for assets? There's some effort to put some better dev docs together at https://github.com/jspm/jspm-cli/pull/1136, any feedback or assistance there is welcome.

jackfranklin commented 9 years ago

I just pulled out the very small module that we use at work into its own thing: https://github.com/jackfranklin/jspm-dev-builder.

It's a pretty straight forward setup that makes full use of the jspm file and trace caching to bundle our app up. We've seen pretty good perf on the cached bundles - down to 0.5 seconds or less, on our app which contains 800+ files.

This is an area that I'm really interested in so keen to see what directions people take.

JonahBraun commented 9 years ago

I did a bunch of testing with different servers. You can see the results: https://github.com/JonahBraun/jspm-perf-test

Unfortunately HTTP2 doesn't actually help (in fact, it hurts a bit). Just having Dev Tools open imposes a huge performance hit. I'd love to know why. When developing, it's normal to have Dev Tools open, but that huge of hit is unacceptable. Does anyone know why?

OliverJAsh commented 9 years ago

Jonah, did you have 'disable cache' enabled in DevTools?

On Tue, 20 Oct 2015 at 06:38 Jonah Braun notifications@github.com wrote:

I did a bunch of testing with different servers. You can see the results: https://github.com/JonahBraun/jspm-perf-test

Unfortunately HTTP2 doesn't actually help (in fact, it hurts a bit). Just having Dev Tools open imposes a huge performance hit. I'd love to know why. When developing, it's normal to have Dev Tools open, but that huge of hit is unacceptable. Does anyone know why?

— Reply to this email directly or view it on GitHub https://github.com/jspm/jspm-cli/issues/872#issuecomment-149438180.