aurelia / framework

The Aurelia 1 framework entry point, bringing together all the required sub-modules of Aurelia.
MIT License
11.76k stars 625 forks source link

Aurelia takes 3 seconds to load on Samsung Galaxy S6/Android/Chrome on small bundled project with few deps #254

Closed insidewhy closed 7 years ago

insidewhy commented 8 years ago
mkdir blah
cd blah
yo aurelia
gulp serve
gulp bundle # in another window after the serve completes

Loads in the blink of an eye on desktop browser. Takes 7-10 seconds to load the page on my Moto E using Chrome on Android 5.1. This is a reasonably powered (if budget) smartphone. I was surprised to have to wait so long and that it is the same no matter the state of the cache.

I'd love to use Aurelia for a project I'm working on but I can't consider it with that mobile performance. Is there any likelihood this will considerably improve within the next few months?

insidewhy commented 8 years ago

I've used jspm bundles a lot but the way webpack handles bundles is superior as it calculates the optimal bundles by scanning the code for dynamic and static imports calculating the minimum number of chunked bundles necessary. To do this manually can be untenable for huge projects.

I'm not really a fan of webpack but this one feature is indispensable for large projects.

EisenbergEffect commented 8 years ago

It's not really hard with aurelia's bundler, you simply supply globs to tell it what should be in the different bundles. If you organize your app by features or by user scenarios, then that makes it even easier.

insidewhy commented 8 years ago

It isn't hard in simple apps but when you have many external dependencies per each of your code modules then calculating the optimal bundles becomes much less simple. In huge apps making a mega bundle of third party dependencies such as in the Aurelian skeleton app will lead to a very suboptimal user experience while they wait for that huge bundle to compile. With webpack these are non-issues.

insidewhy commented 8 years ago

@rosenfeld and me recently had a discussion where I was arguing that jspm's approach was good enough but he won me over with some very convincing arguments, he has some great blog articles on how to optimise page load times.

rosenfeld commented 8 years ago

I believe you are discussing different subjects. I employ both techniques in the application I maintain but they are not necessarily related to each other. The technique described by @EisenbergEffect is commonly referred to as creating separate bundles, like vendor and app, both of which must be loaded for the application to load. This is handled in webpack by the CommonsChunkPlugin.

Even though it's also explained in the code-splitting section of webpack documentation, the lazy code loading technique described by @ohjames is another feature related to code-splitting, but a completely different one from creating a vendor bundle. They can be combined (which I recommend) but this is not required.

Lazy code loading is the hardest part to implement transparently, like webpack does, but it allows you to load only the code you need for the initial rendering of the page and then download more code as you need. By doing so you can speed up the page loading by reducing the size of your initial JS.

Webpack is smart enough to understand which modules are already available in the initial load as it assumes both the commons chunk and the app chunk will be loaded upon initialization. So if some part of the code will call require(['lazy-module', 'another-one'], function(){...}) webpack understands that it should pack those modules and their dependencies and exclude any modules which are already part of the commons and app bundles. This is a very powerful and interesting feature I only found in webpack so far...

EisenbergEffect commented 8 years ago

The bundles created using system.builder or the aurelia bundler do not have to be loaded up front. They can be loaded on demand. SystemJS knows what modules reside in which bundles. As a result, it only downloads the bundle when a module that resides inside that bundle is needed. It does not download it otherwise. This is basically the same capability as you are describing with Webpack. What it doesn't do is automatically determine how to break your app up into these different downloadable bundles. For that, you have to tell it what modules should go in which bundle. You can use globs so it's easy to define. We could add something in the future to have the aurelia bundler try to automatically determine an optimal deploy...but in reality the developer usually knows best because they understand the actual usage patterns of the application.

dpinart commented 8 years ago

I haven't tried webpack yet, if it works as fine as @rosenfeld and @ohjames I'll have to consider its usage. I'm implementing lazy load via aurelia bundles, I do have a bundle that contains minimal stuff to show a login screen to the user. Meanwhile user is logging in I'm importing some module contained in the following bundle to load (inspecting the url I can know which view user is navigating to once he logs in) so I can start the bundle download. App has just 3 bundles, It took me a while configuring the bundles (via Globs and excludes) but it wasn't a nightmare.

rosenfeld commented 8 years ago

@EisenbergEffect I'd love to better understand how this works since I'm interested in webpack alternatives which would support persistent caching so that it could be useful for incremental builds to speed up the deploy process.

Suppose you want to generate 3 bundles, Vendor, App and OnDemand. Suppose both Vendor, App and OnDemand depend on jQuery. How does SystemJS know that Vendor and App should be considered to be loaded upon initialization so that it only includes jQuery in Vendor and not in App and OnDemand? Also, what if both App and OnDemand depend on Common module? How would SystemJS know that Common should be included only in the App module?

EisenbergEffect commented 8 years ago

Basically, you tell it what goes into each bundle. Then it generates a manifest of the bundle contents so it knows what is where. Whenever jQuery is request is simply ensures that the bundle that contains jQuery gets loaded.

rosenfeld commented 8 years ago

@EisenbergEffect, if I understood it correctly, you basically tell the build system that modules in the Vendor bundle shouldn't be included in the App bundle and that neither Vendor, App and their dependencies should be included in the Common bundle and the build system is able to manage this correctly to remove any duplicates, is that correct? For example, if all bundles require jQuery neither App nor Common bundles would include it because they were told not to include Vendor or any of their dependencies and jQuery is one of them.

What is the exact tool that manages this? SystemJS? jspm?

This is certainly manageable, although not as much transparent and convenient as the webpack approach... Anyway, if it supports incremental build persistent caching I'd certainly consider it as an alternative to my current build system. Does it support persistent caching for incremental builds?

Could you provide an example showing how Vendor, App and Common would be configured using your solution?

rosenfeld commented 8 years ago

Also, how do you manage the lazy load behavior? Does that system already provide you the required tools to ask for the module and run a callback once it's loaded? Or do you have to manage this part with your own custom code?

dpinart commented 8 years ago

For each bundle there's an entry in the config file that lists the modules contained. Whenever app imports a module SystemJS knows if it's included in a bundle, if so SystemJS first downloads the bundle before resolving the import, bundles are only download once. A bundle is not downloaded until threre's an import asking for a module contained in.

rosenfeld commented 8 years ago

@dpinart, I'll take your words to build a pseudo example.

"For each bundle there's an entry in the config file that lists the modules contained."

Here's the config:

var config = {
  entries: {
    Vendor: ['jquery', 'knockout'],
    App: ['./app.js'], // app.js requires jquery and ./common.js
    OnDemand: ['./on-demand.js'] // on-demand.js requires jquery and ./common.js
  }
}

How do you tell the config that all dependencies (and their dependencies) included in Vendor should not be part of the App bundle and that all dependencies from App (including the ones bundled in Vendor) should not be part of the OnDemand bundle? You haven't explained that part.

How does SystemJS downloads missing bundles? Is this implemented by SystemJS itself when generating the bundles, transparently?

Also, how does SystemJS know that both Vendor and App have been included in the head section? For example, suppose you include two async script tags in the head, one for App and another for Vendor and suppose Vendor is much bigger than App so that App would usually be downloaded first for the first access (no cache). Since App depends on Vendor, how would SystemJS know that Vendor is already being downloaded and wait for it to finish rather than requesting the vendor bundle again and then prevent it to be downloaded and executed twice?

AshleyGrant commented 8 years ago

The Aurelia bundler is built upon the SystemJS Builder. The Aurelia Bundler Documentation is currently here: http://aurelia.io/docs.html#/aurelia/framework/1.0.0-beta.1.1.4/doc/article/bundling-your-app-for-deploy. It lays out the answers to all of your configuration questions.

When the SystemJS Builder builds up the bundles (based upon the configuration passed to it by the Aurelia Bundler tooling), it writes out, in the app's config.js file, a list of exactly what files are contained within each bundle. SystemJS reads this configuration information at runtime. Using this information, it would know, for example, that app.js is included in the App bundle.

how does SystemJS know that both Vendor and App have been included in the head section?

That's not how the bundler works. You do not include script tags for any of the bundles. You rely on the module loader to load the bundles on demand.

dpinart commented 8 years ago

@rosenfeld You have to be aware to avoid modules being duplicated. Aurelia bundler does not warrants this point. This task is a bit tedious and it requires some time. I ended watching in the debugger what modules were being downloaded in order to show the login view and I set up the "login" bundle with these modules. Then I built the "app" bundle excluding all the modules that were present in "login" bundle and finally I built the third bundle 'app-ext'... At least for me, it was a bit tedious and manually task, although it wasn't a nightmare.

The advantage I can see in webpack is that it automatically ensures a module is not present in more than a bundle. I don't know how you configure different bundles...

rosenfeld commented 8 years ago

Hi @dpinart, this is what I was trying to understand.

I've detailed how I use webpack to create two bundles (vendor and app) and load them async and how to load more code lazily in this article:

http://rosenfeld.herokuapp.com/en/articles/2016-02-29-getting-an-spa-to-load-the-fastest-possible-way-and-how-webpack-can-help-you

Webpack consider each chunk in an special way. Take a look at "Chunk types" in its documentation to understand them:

https://webpack.github.io/docs/code-splitting.html

There is the entry chunk, an initial chunk (non-entry) which is achieved using the CommonsChunkPlugin (like the vendor chunk) and normal chunks which are loaded on demand.

You only specify the entry chunks and you can use the CommonsChunkPlugin to create initial non-entry chunks. The normal chunks are created automatically by webpack by looking at the async requires and bundling together everything which is not included in the initial chunks (entry or not). You can optionally provide a name for this async require so that you would be able to include other modules as well in other places by giving the same name.

I'm not sure if that plugin would support more than one initial chunk besides the entry chunk. You can look at more details about the plugin here:

https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin

There's a downside though that webpack assumes you will ensure the commons chunks (Vendor) will be executed before the entry chunk (App). That means you can't load them asynchronously, so I had to add some custom logic to the chunks to be able to handle them async and I also had to create a small plugin to change the initial chunk a little bit, which is demonstrated in the referred article in case you are curious. It would be awesome if webpack allowed you to inform that those chunks will be loaded async so that it could handle that itself when generating the chunks...

Now, if you were asking about multiple entry points, then webpack supports that pretty well.

MaximBalaganskiy commented 8 years ago

@rosenfeld Could you please give more details on how to separate aurelia and app code into two bundles? I'm having an exception when try to do that, although one bundle app works fine. I managed to trace it to an undefined Element injection. I feel that it has something to do with in which bundle aurelia-pal-browser is located but couldn't get to the bottom of it.

rosenfeld commented 8 years ago

Hi @MaximBalaganskiy, I don't actually use Aurelia myself. I was brought to this discussion by someone while we were discussing some performance issues related to the build tool and just contributed to this specific subject. I can't give you details on how that applies to an aurelia application. In my application I bundle all libraries in a vendor JS and the application code in another JS. I also load some code on demand. But I don't know much of Aurelia to help you understanding how you can split your application and help you debugging any issues, sorry.

insidewhy commented 8 years ago

Using the latest Aurelia with a jspm configuration, the loading spinner is still shown for 3 seconds even on a Samsung Galaxy S6. It's too much :( Could switching to webpack help this?

Site is at http://kchomp.co and code is fully bundled. The only non-aurelia deps I'm using are lodash and blbuebird.

EisenbergEffect commented 8 years ago

Just try not using system.js for starters.

insidewhy commented 8 years ago

@EisenbergEffect I was under the impression that jspm needed system.js. I imagined that system.js' job just became much smaller for a bundled app. I'd have no idea how to remove system.js without also removing jspm. Do you think this slowdown is called by system.js then?

EisenbergEffect commented 8 years ago

Neither one really needs the other, it's just more convenient to use them together. You could use npm instead of jspm for packages and still use system.js or you could use npm with require.js or npm with webpack.

insidewhy commented 8 years ago

Thanks, I will assume this is an issue with system.js for now then and port to webpack. Will reopen if it doesn't help or open a bug against system.js if it does. I loved system.js as a concept but this much load time is obviously not good enough, I can only assume from your switch to webpack that you guys have also found systemjs to be inadequate.

EisenbergEffect commented 8 years ago

We are moving to provide multiple alternatives to system.js. We've also discovered that Babel generates code that creates runtime checks in class constructors. We're working on removing that, which should improve startup time as well (and reduce size a bit more). Performance is an ongoing project for us and we're always investigating different options. After our v1 release, we'll also begin work on server-render too.

insidewhy commented 8 years ago

Thanks, you're the man. Our team really appreciates all of the advice and help you've given and have had a great time building with Aurelia! We'll committing some more work back besides my animator fixes soon :)

insidewhy commented 8 years ago

A member of my team has determined that 75% of the time delay is due to system.js, so Aurelia is off the hook. <3 aurelia.

dpinart commented 8 years ago

We moved to webpack a couple of months ago and we could observe a significative startup time improvement. Anyway, on mobile devices (android 5.1 +) startup time is annoyingly long, about 15/20 seconds. Even deploying the app within an apache cordova application results in very long startup times. It seems the bottleneck is at aurelia plugins/feature initialization (main.js). Once app is launched performance is quite acceptable. There's no significative difference amongst webpack or systemjs on mobile devices

gheoan commented 8 years ago

@dpinart Is that for bundled applications?

AshleyGrant commented 8 years ago

I tested the Aurelia docs app today on my phone (Nexus 6p) It loads in 2-3 seconds.

ACT1GMR commented 8 years ago

Hi that's the case for me already Im using aurelia enhance feature that helps me to extend my server side template engine and use aurelia just for interactive components that's great and appreciable. check out my website here (language is Persian): http://otex.ir really it works like a monster on desktop browsers and take too long for startup on android + windows phone (about 5-10 sec) vice versa it's weird that on Iphone everything works fast and smoothly that's weird for me because safari is also contains a webkit engine what's the big deal between android and IOS? have to mentioned that site is bundled and I'm using systemjs + jspm + js + aurelia and systemjs latest builds

insidewhy commented 8 years ago

My team member was wrong to lay the blame on system.js... this startup time is still such a big problem for us that we have to port to angular 2.

insidewhy commented 8 years ago

Ah it seems like this is a duplicate of #136.

EisenbergEffect commented 8 years ago

@ohjames I'm not sure that is going to solve any problem for you. In the videos I've seen, the ng2 startup time is actually longer than Aurelia. I'd love to help you with this but we haven't experienced any severe issues. There are usually many ways to handle these types of problems if you encounter them but it does depend on the details of your app.

insidewhy commented 7 years ago

I'd love to help you with this but we haven't experienced any severe issues.

If I reload the majority of the websites linked from http://builtwithaurelia.com then I get 3+ seconds of loading on every reload. I've switched phones recently (to a comparatively much more powerful Xiaomi Redmi Note 2) so it rules out my previous phone being the issue. Am stuck on Android 5.1 though, but using the latest Chrome.

There are a few examples linked that do load within a second after the first reload though, I'd really like to know what is different about those.

There's only about 10 or so links to go through on that website, it wouldn't take long to verify the long load. One notable example is the "North winds".

Thanood commented 7 years ago

Northwind takes like 6 seconds to load on my Galaxy S6 and around 5 seconds in my PC. The longest "wait times" are loading something from the breeze controller, though. Especially "Metadata" which takes nearly a second.

There seems to by a relatively huge gap (around 1 second) between loading google analytics and index.js.

That being said, I think it uses SystemJS. That's kinda slow if not everything is bundled (at least materialize and views is not).

JavaScriptKicks loads instantly.

http://kchomp.co/ takes around 3.5 seconds with a 2 second gap between bluebird.js and app.js.

@ohjames

My team member was wrong to lay the blame on system.js...

Would you mind to share any details on this conclusion? As far as I know, SystemJS uses a promise-based approach which essentially means there's overhead (waiting for resolves). With a lot of files, this is a lot of overhead.

insidewhy commented 7 years ago

I suspect the issue is with systemjs being slow after doing some of my own analysis, although I doubt its use of promises are exclusively to blame.

I just upgraded to the latest aurelia-bundler and now kchomp.co takes a lot longer to load on mobile. Oh no ;)

insidewhy commented 7 years ago

SystemJS is just super slow even when bundled, I've hacked the loader a little and made it much faster. My approach isn't suitable for a PR though.

MaximBalaganskiy commented 7 years ago

SystemJS is just super slow even when bundled

well, this is not what we see in our environment. While the web site is definitely slower on a mobile, on the desktop it's almost instantaneous.

insidewhy commented 7 years ago

well, this is not what we see in our environment

Every single aurelia website deployed with systemjs, bundled or otherwise, spends at least 3 seconds bootstrapping on mobile. Bet you can't find a single example to the contrary.

insidewhy commented 7 years ago

on the desktop it's almost instantaneous.

That's true, but this issue is all about mobile performance which is terrible due to systemjs.

insidewhy commented 7 years ago

After several hours porting the build system to webpack I've brought mobile load times down to less than a second with pretty much the same code as before.

AshleyGrant commented 7 years ago

@ohjames, I'm glad to hear Aurelia isn't the problem.

insidewhy commented 7 years ago

Well it's opened up a whole other can of worms (https://github.com/aurelia/loader-webpack/issues/24 !?) but at least I managed to hack the build enough and peg dependencies just so that my build stopped throwing unhelpful error messages enough to work. The lack of documentation and bugs were a huge source of pain though.