8
votes

I have followed the basic tutorials (results in one file after you run r.js)

The problem is, my main.js file at the end is 500KB. That's too big. I want to split it into two files.

I want to optimize my main.js file into two files:

  1. One that holds the front page and user profile pages, since they're most accessed
  2. One that holds all the other pages (ordering, account settings, profile settings, etc.)

Most people will hit the front page and user profile pages, and I want those to load quickly first (while having the other pages load in the background in the 2nd main file)

The problem is, I don't know how to do this. There are examples like this online, but these examples do not use Backbone. They don't cover how to deal with router and app.js

I'm confused...because I only have one app.js, one router.js...how can I split router.js into two files?

I don't know how to split my project up when dealing with Backbone.

Below is the code

HTML PAGE (the entry point for my Single Page Application)

<html>
<head>
    <script type="text/javascript" data-main='/media/js/main' src='/media/js/lib/requirejs/require-jquery.js'></script>
</head>
<body>
    Hello
</body>
</html>

Main.js

require.config({
    paths:{
        jquery: 'lib/requirejs/require-jquery',
        jquery_ui:'lib/jquery-ui/jquery-ui-1.10.3.custom',
        underscore: 'lib/underscore/underscore-min',
        backbone:'lib/backbone/backbone-min',
        backbone_viewhelper:'lib/backbone/backbone.viewhelper',
        text: 'lib/requirejs/text',
        birthdaypicker: 'lib/birthdaypicker/bday-picker',
        //more paths
    },
    waitSeconds: 30,
    shim:{
        'underscore':{
            exports: '_'
        },
        'backbone':{
            deps:[ 'underscore', 'jquery'],
            exports: 'Backbone'
        },
        'backbone_viewhelper':{
            deps:['underscore','backbone']
        }
    }
});


require([
    'app',
    'json2',
    'jquery_ui',
    'backbone_viewhelper',
    'bootstrap_js',
    'bootstrap_select',
    'birthdaypicker',
    'accounting',
    'numbersonly',
    'main_alert',
    'string_tools',
    'plupload',
    //more things here
], function(App){
    App.initialize();
});

App.js

define([
    'jquery',
    'underscore',
    'backbone',
    'router'
], function($, _, Backbone, Router){    
    var initialize = function(){
        Router.initialize();
    }
    return {
        initialize: initialize
    };

});

Router.js

define([
    'jquery',
    'underscore',
    'backbone',
    'modules/index/view',
    'modules/home/view',
    'modules/listings_search/view',
    'modules/profile/view',
    //more modules
], function($, _, Backbone, indexView, homeView,searchView, profileView){
    var AppRouter = Backbone.Router.extend({
        initialize:function(){
            _.bindAll(this);
        },
        routes:{
            '':'index',
            'home': 'home',
            'register': 'register',
            'login': 'login',
            'listings(/start/:start)(/num/:num)': 'search',
            'listings/create': 'listingsCreate',
            'listings/:listing_id/edit': 'listingsEdit',
            'orders/listings/:listing_id/create': 'ordersCreate',
            'orders/buyer(/start/:start)(/num/:num)': 'ordersListBuyer',
            'orders/seller(/start/:start)(/num/:num)': 'ordersListSeller',
            'orders/:order_id': 'orders',
            'orders/:order_id/messages':'messages',
            '*actions': 'defaultAction'
            //more stuff
        },
        index:function(){
            app_router_view.show(indexView);
        },
        search:function(start, num){
            var options = {
                filters:{
                    start: start,
                    num: num
                }
            };
            app_router_view.show(searchView, options);
        },
        static:function(template){
            app_router_view.show(staticView, { static_view: { template: template }});
        },
        profile:function(){
            app_router_view.show(profileView);
        },
        passResetCode:function(code){
            app_router_view.show(passCodeView, {'code':code});
        },
        //more stuff
        home:function(){
            app_router_view.show(homeView);
        },
        defaultAction:function(actions){
            this.navigate('/', { trigger:true});
        }
    });
    var initialize = function(){
        var app_router = new AppRouter;
        Backbone.history.start({pushState:true, root: '/'});
        $(document).on('click', 'a:not([data-bypass])', function (evt) {
            var href = $(this).attr('href');
            if(href){
                var protocol = this.protocol + '//';
                if (href.slice(protocol.length) !== protocol && href != '#') {
                    evt.preventDefault();
                    app_router.navigate(href, { trigger: true});
                }
            }else{
            }
        });
    };
    return {
        initialize:initialize
    }
});

As you can see , my entire app starts with main.js, goes to app.js, and finally goes to router.js.

How can I split this?

4
Have you consider removing the size of your all.js by moving some vendor lib to CDN and not packing them inside your all.js file? (e.g. jquery)ekeren
What is the size of this file after being gzipped? And if you have the option to use nodejs, you could also try something like ezeljs.com to send a rendered page on the first responseJordan Denison

4 Answers

3
votes

I've created an example to show how it could be done. It contains a skeleton of Backbone application. The application there is split into:

  • a main bundle which contains the core of the application and only renders a "main" view (called views/app here),

  • and a secondary bundle which contains all other views.

The secondary bundle is loaded only as needed. In this application this means that it should be loaded only when the foo or bar views are used, and not before. (You can verify this by inspecting network operations in your browser.)

The key points are:

  1. The view in views/app is the "main" view of the application. Loading it instantiates and renders the view right away.

  2. The js/router module does not use the other views directly. It calls require to load the view first. This makes the foo and bar function asynchronous.

  3. This is the part of the build.js file that divides the application into two bundles:

    modules: [
        {
            name: "main"
        },
        {
            name: "secondary",
            // There no module named secondary in the source, so create it.
            create: true,
            // Make sure to include all the views other than the main one.
            include: [
                "views/foo",
                "views/bar"
            ],
            // Exclude everything we've included in `main`.
            exclude: ["main"]
        }
    ]
    
  4. The optimized version needs an initial configuration like this:

      bundles: {
         "secondary": ["views/foo", "views/bar"]
      }
    

    This tells RequireJS that the modules views/foo and views/bar are loaded by loading secondary.

Please read the README.md file for more details.

3
votes

Based on the code you have shared, I've created a sample web-app and committed the code in git-hub.

Application is divided into 2 modules:

  • main : contains modules/index/view and modules/profile/view
  • other : contains 'modules/order/view and modules/search/view

When you request for modules/index/view or modules/profile/view, main.js is downloaded if not downloaded yet. Similarly when request is placed for modules/order/view or modules/search/view, other.js is downloaded if not downloaded yet. Remember to use require.js v2.1.10 or greater, as it has bundle feature which is required for generating other.js.

You can further modularize it by defining order, search, profile as independent modules in build.js, so that they are downloaded only when needed.

Output of executing build command :

media/js/main.js
----------------
media/js/lib/jquery/jquery-min.js
media/js/lib/underscore/underscore-min.js
media/js/lib/backbone/backbone-min.js
media/js/router.js
media/js/app.js
media/js/main.js
media/js/modules/index/model.js
media/js/modules/index/view.js
media/js/modules/profile/model.js
media/js/modules/profile/view.js

media/js/other.js
----------------
media/js/modules/order/model.js
media/js/modules/order/view.js
media/js/modules/search/model.js
media/js/modules/search/view.js

The execution flow goes like this: index.html => media/js/main [it has index/view, profile/view, app.js and all the dependencies]. By default Index view is shown, as it is configured for home route.

When Profile links is clicked, no more files are downloaded, as main.js is already downloaded. When Search / Order links are clicked, other.js is downloaded.

0
votes

In your build profile you can specify how many modules do you want to create and which dependencies they have to include (see http://requirejs.org/docs/optimization.html#basics).

You can see the options here https://github.com/jrburke/r.js/blob/master/build/example.build.js , the part you're interested in starts on line 350.

Also see How to use RequireJS build profile + r.js in a multi-page project for a deeper answer

-3
votes

Splitting the code into two files is not the way to go.

Splitting the code into two files will only increase load times overall, as extra wrapper code would be needed. Also, splitting the code may unnecessarily increase the modularity of the code, causing design problems in future.

The way to go would to be to lazy-load the routed files (so you load your front page and user profile pages first), and then you can load the other files in the background after you've done loading the front page and user profile pages.

It is possible to split it - don't get me wrong - but doing so would just create unnecessary code, and really wouldn't help.

Here a link that may help.