58

I'm making a website using Angular2 and I'm having what i suppose is an issue. On the first load of my angular page, SystemJS is making more than 500 hundred requests to retrieve every Angular2 file in angular2/src directory. In total, the first load downloads more than 4MB and it takes more than 14 seconds to start.

My index.html does the following scripts includes:

<script src="libs/angular2/bundles/angular2-polyfills.js"></script>
<script src="libs/systemjs/dist/system.src.js"></script>
<script src="libs/rxjs/bundles/Rx.js"></script>
<script src="libs/angular2/bundles/angular2.min.js"></script>
<script src="libs/angular2/bundles/http.dev.js"></script>
<script src="libs/jquery/jquery.js"></script>
<script src="libs/lodash/lodash.js"></script>
<script src="libs/bootstrap/js/bootstrap.js"></script>

And my systemJs initialization code looks like this:

    <script>
      System.config({
        defaultJSExtensions: true,
        paths: {
          '*': 'libs/*',
          'app/*': 'app/*'
        },
        packageConfigPaths: ['libs/*/package.json'],
        packages: {
          app: {
            format: 'register',
            defaultExtension: 'js'
          }
        }
      });
      System.import('app/main')
            .then(null, console.error.bind(console));

    </script>

My public folder has the following structure:

.
├── img
├── styles
├── app
├── libs
|   └── angular2
|   └── systemjs
|   └── rxjs
|   └── jquery
|   └── lodash
|   └── bootstrap
└── index.html

A couple of screenshots of some of the js files that are being requested: enter image description here

enter image description here

Is there a way to avoid all of those requests?

Michael
  • 1,453
  • 3
  • 20
  • 28
Marcos Basualdo
  • 2,580
  • 3
  • 18
  • 11
  • 1
    Use webpack and make bundle served from memory via webpack-dev-server, here is great demo https://github.com/AngularClass/angular2-webpack-starter – Eggy Feb 08 '16 at 22:30
  • 1) Use some sort of task runner at build time, like [Grunt](http://gruntjs.com/), to combine and minify your JS files. 2) Rewrite your code to eliminate the need for jQuery, if possible...Angular's [jqLite](https://docs.angularjs.org/api/ng/function/angular.element) should be able to do a lot of what you need. – Sam Feb 08 '16 at 22:31
  • 1
    @Eggy while that is a good strategy, the angular2 files are already bundled. – SnareChops Feb 08 '16 at 22:31
  • You're mapping your full libs folder which contains rxjs. Make the pattern more specific to skip rxjs folder (everything is set by Rxjs bundle) – Eric Martinez Feb 08 '16 at 22:37
  • 1
    My recipe for bundling (with JSPM) may be a helpful reference: http://stackoverflow.com/a/34616199/3532945 – brando May 07 '16 at 15:11
  • I came here to post the same question. May you have many upvotes! – Greg Woods May 13 '16 at 10:47

9 Answers9

47

I had the exact same problem, was actually looking at this post for an answer. Here is what I did to solve the problem.

  1. Modify your project to use webpack. Follow this short tutorial: Angular2 QuickStart SystemJS To Webpack
  2. This method will give you a single javascript file however it is quite large (my project file was over 5MB) and needs to be minified. To do this I installed webpack globaly:npm install webpack -g. Once installed, run webpack -p from your apps root directory. This brought my file size down to about 700KB

From 20 seconds and 350 requests down to 3 seconds and 7 requests.

Community
  • 1
  • 1
David Herod
  • 784
  • 1
  • 9
  • 13
  • 2
    I just found this link in the official docs: https://angular.io/docs/ts/latest/guide/webpack.html . Perhaps it's more of the same, but maybe it'll help someone – evandongen May 26 '16 at 06:05
  • 2
    This is so simple and straightforward, this should have a 1000 upvotes! Cut my load time from 300+ requests to ~20. – Aviad P. Aug 18 '16 at 14:56
  • 4
    Don't forget to enable gzip on your server. Should drop file size by another 50%. – David Herod Sep 14 '16 at 02:35
  • @DavidHerod After webpack -p what should be the command to run project? is it "npm start" or "npm run server:prod" or any other?? – Niraj Dec 02 '16 at 06:23
33

I see you already have a response, which is good of course. BUT for those who want to use systemjs (like I also do), and not go to webpack, you can still bundle the files. However, it does involve using another tool also (I use gulp). So... you would have the folowing systemjs config (not in the html, but in a separate file - let's call it "system.config.js"):

(function(global) {

    // map tells the System loader where to look for things
    var map = {
        'app':                        'dist/app', // this is where your transpiled files live
        'rxjs':                       'node_modules/rxjs',
        'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api', // this is something new since angular2 rc.0, don't know what it does
        '@angular':                   'node_modules/@angular'
    };

    // packages tells the System loader how to load when no filename and/or no extension
    var packages = {
        'app':                        { main: 'boot.js',  defaultExtension: 'js' },
        'rxjs':                       { defaultExtension: 'js' },
        'angular2-in-memory-web-api': { defaultExtension: 'js' }
    };

    var packageNames = [
        '@angular/common',
        '@angular/compiler',
        '@angular/core',
        '@angular/http',
        '@angular/platform-browser',
        '@angular/platform-browser-dynamic',
        //'@angular/router', // I still use "router-deprecated", haven't yet modified my code to use the new router that came with rc.0
        '@angular/router-deprecated',
        '@angular/http',
        '@angular/testing',
        '@angular/upgrade'
    ];

    // add package entries for angular packages in the form '@angular/common': { main: 'index.js', defaultExtension: 'js' }
    packageNames.forEach(function(pkgName) {
        packages[pkgName] = { main: 'index.js', defaultExtension: 'js' };
    });

    var config = {
        map: map,
        packages: packages
    };

    // filterSystemConfig - index.html's chance to modify config before we register it.
    if (global.filterSystemConfig) { global.filterSystemConfig(config); }

    System.config(config);
})(this);

Then, in your gulpfile.js you would build a bundle like this (using the info from system.config.js and tsconfig.json files):

var gulp = require('gulp'),
    path = require('path'),
    Builder = require('systemjs-builder'),
    ts = require('gulp-typescript'),
    sourcemaps  = require('gulp-sourcemaps');

var tsProject = ts.createProject('tsconfig.json');

var appDev = 'dev/app'; // where your ts files are, whatever the folder structure in this folder, it will be recreated in the below 'dist/app' folder
var appProd = 'dist/app';

/** first transpile your ts files */
gulp.task('ts', () => {
    return gulp.src(appDev + '/**/*.ts')
        .pipe(sourcemaps.init({
            loadMaps: true
        }))
        .pipe(ts(tsProject))
        .pipe(sourcemaps.write('.'))
        .pipe(gulp.dest(appProd));
});

/** then bundle */
gulp.task('bundle', function() {
    // optional constructor options
    // sets the baseURL and loads the configuration file
    var builder = new Builder('', 'dist/system.config.js');

    /*
       the parameters of the below buildStatic() method are:
           - your transcompiled application boot file (the one wich would contain the bootstrap(MyApp, [PROVIDERS]) function - in my case 'dist/app/boot.js'
           - the output (file into which it would output the bundled code)
           - options {}
    */
    return builder
        .buildStatic(appProd + '/boot.js', appProd + '/bundle.js', { minify: true, sourceMaps: true})
        .then(function() {
            console.log('Build complete');
        })
        .catch(function(err) {
            console.log('Build error');
            console.log(err);
        });
});

/** this runs the above in order. uses gulp4 */
gulp.task('build', gulp.series(['ts', 'bundle']));

So, when running "gulp build", you will get the "bundle.js" file with everything you need. Sure, you also need a few more packages for this gulp bundle task to work:

npm install --save-dev github:gulpjs/gulp#4.0 gulp-typescript gulp-sourcemaps path systemjs-builder

Also, make sure that in your tsconfig.json you have "module":"commonjs". Here is my tsconfig.json which is used in my 'ts' gulp task:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false
    },
    "exclude": [
        "node_modules",
        "typings/main",
        "typings/main.d.ts"
    ]
}

Then, in your html file you only need to include this:

<!-- Polyfill(s) for older browsers -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>

<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="dist/app/bundle.js"></script>

And that's it... I got from 600 requests, 4mb in about 5 seconds... to 20 requests, 1.4mb in 1.6 seconds (local development machine). But these 20 requests ~1.4mb in 1.6 seconds also include some other js and css that the admin theme came with plus a few html templates that get required on the first load, I prefer to use external templates - templateUrl: '', instead of inline ones, written in my component.ts file. Sure, for an app that would have millions of users, this still wouldn't be enough. Also server-side rendering for initial load and cache system should be implemented, I actually managed to do that with angular universal, but on Angular2 beta (took about 200-240 milliseconds to load the initial render of the same admin app that above takes 1.6 seconds - I know: WOW!). Now it's incompatible since Angular2 RC came out, but I'm sure the guys doing universal will get it up to speed soon, specially since ng-conf is coming up. Plus, they're also planing to make Angular Universal for PHP, ASP and a few other - right now it's only for Nodejs.

Edit: Actually, I've just found out that on NG-CONF they said Angular Universal already supports ASP (but it doesn't support Angular2 > beta.15 :)) ... but let's give them some time, RC just came out a few days ago

MrCroft
  • 3,049
  • 2
  • 34
  • 50
  • 1
    Thanks for this very detailed answer, I got my stuff working thanks to it. – webaba May 11 '16 at 16:37
  • @webaba Now it's even easier with angular-cli (but only works with apps created using angular-cli, of course). Line breaks don't seem to work in comments, so I separated the comands by a pipe: npm install angular-cli -g | ng new my-app | cd my-app | ng serve -prod | Then simply access localhost:4200 and check the dist folder for the js files – MrCroft May 11 '16 at 20:53
  • Sounds cool, my app got quite huge I'm not sure if I can migrate to angular-cli, I ended up handcrafting something similar, serving a different html file depending on NODE_ENV env variable. – webaba May 11 '16 at 22:04
  • thanks. my requests went from 671 to 17 however :( i removed system.js, systemjs.config.js and system.import statements from my index.html. is this correct? now i'm getting a 'Module must be loaded as AMD or CommonJS' error. how do i resolve this? – CurlyFro May 19 '16 at 20:47
  • @CurlyFro I've edited my answer, now included the entire process, not just bundle the files. Hope you've just overlooked something and you'll realize it right away. This setup works for me. I mean I still had it because I haven't deleted the fodler, and I've just tested it. But not actually using it anymore. I use the angular cli now (Alpha, not really recommended to start any bigger app for production soon). – MrCroft May 20 '16 at 22:09
  • And yes, correct... I no longer have `system.js` and `system.config.js` included in my `index.html` file. P.S. I haven't mentioned initially, you must have `"module": "commonjs"` in your `tsconfig.json` – MrCroft May 20 '16 at 22:20
  • This answer should be the accepted one as it is 100% related. – Vassilis Pits Jun 21 '16 at 13:56
  • I've implemented your solution and I am are successfully producing a bundle.js file which I am including in the page. However, when loading up our Angular 2 app and looking in the Network tab of Google Chrome it appears we are getting all the same original calls to all the .js and .html files (we've been using separate files for our html templates), and then one call that loads the bundle, which then makes the same .html calls. Can you advice on why we may be getting this? – Matthew Peel Jul 05 '16 at 16:00
  • The generated bundle only contains the @angular files (from node_modules/), those that rarely change. The rest (your components) are transpiled separately from this bundle, each to an individual file. I've never tried to bundle those too, don't know how I could make that work. Also, later on, maybe we'd be using lazy loading (not loading a component until we actually need it), so bundling those also would be counter productive. And those files also change frequently. It's better to invalidate cache just for those that are modified, not for an entire bundle each time you would deploy. – MrCroft Jul 05 '16 at 17:39
  • As for the other issue, having an html template being requested twice... That shouldn't happen, of course... Nothing in the bundle should request any of your templates, just your components do that. Or maybe if you have some 3rd party / custom components in your bundle, I don't know what those might do. Some debugging is required here, a demo (plunkr or something)... I doubt anyone could simply guess what's going on there. – MrCroft Jul 05 '16 at 17:46
  • Thanks for the help so far. I can possibly see my problem then as my bundle.js file is including all my components transpiled js files. It looks like this is what is triggering the second call. How are you avoiding this issue in your solution? I think the only difference between the files you’ve posted and the files in my project is the name of the angular 2 entry file (boot.js vs main.js). Here is our system.config.js and gulpfile.js https://plnkr.co/edit/TBoFuVHqkDxXOVJMBmjA?p=catalogue I've put an explanation of our folder structure in the description of the plnkr. – Matthew Peel Jul 06 '16 at 10:09
  • Sorry for the late response. I haven't used this code in a while, don't have the code anymore. I'm using the CLI. Don't remember this issue occuring, though it wasn't a complex project, it was just a small test project. You can take a look at the angular 2 quickstart, don't know how long ago it happened, but I see they're going with a similar system js config. You can also check out this repo: https://github.com/mschwarzmueller/angular2-seed It belongs to Maximilian Schwarzmüller, an instructor on udemy which actually helped me in the beginning to get the bundle going. Hope this helps! – MrCroft Jul 07 '16 at 14:00
  • Thanks for this answer! – Syntactic Fructose Dec 11 '16 at 23:24
3

I think that your question is related to this one:

To have something ready for production (and speed it up), you need to package it.

I mean transpiling all files into JavaScript ones and concat them the same way Angular2 does for example. This way you will have several modules contained into a single JS file. This way you will reduce the number of HTTP calls to load your application code into the browser.

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
3

I found a simple solution, using browserify & uglifyjs on mgechev's angular2-seed repository

Here's my version:

pacakge.json:

{
...
  "scripts": {
      "build_prod": "npm run clean && npm run browserify",
      "clean": "del /S/Q public\\dist",
      "browserify": "browserify -s main  public/YourMainModule.js > public/dist/bundle.js && npm run minify",
      "minify": "uglifyjs public/dist/bundle.js --screw-ie8 --compress --mangle --output public/dist/bundle.min.js"
    },
...
  "devDependencies": {
      "browserify": "^13.0.1",    
      "typescript": "^1.9.0-dev.20160625-1.0",
      "typings": "1.0.4",
      "uglifyjs": "^2.4.10"
    }
}
  1. Build your project.
  2. Run: npm run build_prod It'll create bundle.js & bundle.min.js under public\dist directory.
  3. Edit your index.html file: Instead of running System.import('YourMainModule')... , add <script src="/dist/bundle.min.js"></script>
FreeBird72
  • 123
  • 1
  • 2
  • @FreeBirs72 I tried your answer but now my Angular 2 app won't bootstrap after removing the System.import. Any ideas? – Loves2Develop Nov 01 '16 at 13:55
  • Try to remove "--mangle" from the "minify" script. I had problems with "--mangle" after I wrote my answer – FreeBird72 Nov 02 '16 at 13:18
  • Out of all of the answers, this is the only one that actually worked, the only problem is a) the minify script failed... even after removing the `--mangle`... also, it would really help to be able to run this via `gulp`... – Serj Sagan May 04 '17 at 03:10
  • Actually I did get it to work with gulp using `gulp-exec` – Serj Sagan May 04 '17 at 03:17
1

On the first load of my angular page, systemjs is making more than 500 hundred requests to retrieve every angular2 file in angular2/src directory. In total, the first load downloads more than 4mb and it takes more than 14s to start.

The SystemJs workflows are fairly new and don't have enough research in them for best deployment.

Suggest going back to commonjs + webpack. More : https://basarat.gitbooks.io/typescript/content/docs/quick/browser.html

Here is an example : https://github.com/AngularClass/angular2-webpack-starter

basarat
  • 261,912
  • 58
  • 460
  • 511
1

@FreeBird72 Your answer is awesome.

If you want to use SystemJS for development and speed up the production server like I do. Check this out.

NOTE: Only import the components that you use, DO NOT import from the whole package.

Eg: If you want to use Modal from ng2-bootstrap.

import {MODAL_DIRECTIVES} from "ng2-bootstrap/components/modal";

Instead of:

import {MODAL_DIRECTIVES} from "ng2-bootstrap/ng2-bootstrap";

This will import the modal component instead of the whole ng2-bootstrap

Then follow the answer from @FreeBird72

Add this package.json

{
  ...
  "scripts": {
    ...
    "prod": "npm run tsc && npm run browserify",
    "browserify": "browserify -s main  dist/main.js > dist/bundle.js && npm run minify",
    "minify": "uglifyjs dist/bundle.js --screw-ie8 --compress --mangle --output dist/bundle.min.js",
    ...
  },
  "devDependencies": {
    ...
    "browserify": "^13.0.1",    
    "uglifyjs": "^2.4.10",
    ...
  }
  ...
}

Then you can npm run tsc on development and npm run prod on production server Also remove System.import(.... from your index.html and change it to <script src="/dist/bundle.min.js"></script>

lthh89vt
  • 154
  • 5
0

If you want to stick with SystemJS, you can bundle your app with JSPM. I've had good success with this so far, using JSPM's bundle-sfx command to make single JS files for Angular 2 apps.

There's some useful information in this Gist, and there's a seed project.

Harry
  • 3,312
  • 4
  • 34
  • 39
0

I am using AG2 RC version While using MrCroft's solution with systemjs-builder, i was hitting a lot of issues like: error TS2304: Cannot find name 'Map' error TS2304: Cannot find name 'Promise'...

After many tries, i added: ///<reference path="../../typings/index.d.ts" /> into my boot.ts and now I got my bundle file compiled.

0

The Angular command line interface now supports bundling (with tree-shaking to strip out unused code from imports), minification, and ahead-of-time template compilation, which not only hugely minimises the number of requests made, but also makes the bundle very small. It uses WebPack underneath.

It's incredibly easy to make production builds with it:

ng build --prod --aot

https://github.com/angular/angular-cli

Harry
  • 3,312
  • 4
  • 34
  • 39