Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

loadTasks as they are needed to speed up Grunt load time #975

Open
dylang opened this issue Nov 9, 2013 · 41 comments
Open

loadTasks as they are needed to speed up Grunt load time #975

dylang opened this issue Nov 9, 2013 · 41 comments
Milestone

Comments

@dylang
Copy link
Contributor

dylang commented Nov 9, 2013

The problem is that Grunt loads all tasks every time it runs. Even if the user just wants to run grunt jshint, it will still load grunt-uglify, grunt-sass, etc.

This can be slow if a task has a lot of dependencies that are require'ed before the task is run.

For example, this is a slow to load task:

module.exports = function(grunt) {

    // These `require` statements execute even if this task isn't run.
    var path = require('path'),
        semver = require('semver'),
        q = require('q'),
        utils = require('./utils/utils'),
        constants = require('./utils/constants'),
        local = require('./core/local')(grunt),
        styles = require('./core/styles')(grunt),
        debug = require('./utils/debug')(grunt),
        install = require('./core/install')(grunt);

    grunt.registerTask(
        'build',
        'Build project',
        function(){
           // Code here runs when the task runs.
           // Grunt would load faster if devs put their `require`
           // statements here because they would only run
           // when the task is run but it's much more common
           // in the Node world to put all `require`s at the top.
           // (code removed for this example)
        }
    );

I've made some changes to time-grunt so you can see how long it takes for tasks to load vs their actual run time.
screenshot
In that screenshot there are other tasks loading that aren't used but they still contribute to the load time.

@cowboy
Copy link
Member

cowboy commented Nov 9, 2013

Maybe plugin authors should be told to defer requiring of libs until needed.

Also, task names can be totally different from plugin names. For example, if plugin "grunt-foo" had tasks "bar" and "baz" how could Grunt know which task files to run when the user ran grunt qux which is an alias task for "bar" followed by "baz"?

@dylang
Copy link
Contributor Author

dylang commented Nov 9, 2013

Also, task names can be totally different from plugin names. For example, if plugin "grunt-foo" had tasks "bar" and "baz" how could Grunt know which task files to run when the user ran grunt qux which is an alias task for "bar" followed by "baz"?

Maybe it's worth revisiting this feature for Grunt 0.5? If you want a task called foo then put your code in a file called /tasks/foo.js. If you also need a task called bar then create tasks/bar.js.

Or we could more extreme - the name of the node module is the only task that is registered. grunt-foo only can register a task called foo.

Maybe plugin authors should be told to defer requiring of libs until needed.

I think this is an awkward pattern for seasoned Node developers. We're changing our Grunt tasks to this pattern and it feels "dirty" to me. This is only my opinion, what do others think?

@cowboy
Copy link
Member

cowboy commented Nov 9, 2013

If "require" was declarative and processed as part of a pre-compilation step—like requiring stdio.h in C—it would make sense to specify all libraries-to-be-required up-front. Unfortunately, require in Node.js doesn't behave this way. It's just a function call. As such, requiring a library in Node.js is subject to a run-time performance penalty, and should probably be deferred when necessary.

Lazily evaluating require calls might be considered an anti-pattern by those who maintain that require is declarative (which it is not) or by those who write tools that scan .js files for require calls in order to build library dependency graphs (which is very hacky), but it is a completely valid technique for solving this specific problem: deferring expensive operations until later.

module.exports = function(grunt) {
  // Just one way to solve this problem...
  var lib1, lib2, lib3;
  var init = function() {
    lib1 = require("lib1");
    lib2 = require("lib2");
    lib3 = require("lib3");
    init = function() {};
  };

  grunt.registerTask("foo", "do something.", function() {
    init();
    lib1(lib2, lib3).whatever();
  });
};

I'd imagine that with proxies, a "lazy" require will be able to be created to simplify this process.

@dylang
Copy link
Contributor Author

dylang commented Nov 9, 2013

I'm proposing taking advantage of lazy loading but doing it in Grunt instead of in the tasks. This removes the responsibility from the task developers, at the cost of backwards compatibility for tasks with source files that don't match the task names.

Currently grunt.tasks.loadTasks scans directories and does the require right away:

function loadTasks(tasksdir) {
  try {
     // Scan for available tasks
    var files = grunt.file.glob.sync('*.{js,coffee}', {cwd: tasksdir, maxDepth: 1});
    files.forEach(function(filename) {
      // "require" the task file
      loadTask(path.join(tasksdir, filename));
    });
  } catch(e) {
    grunt.log.verbose.error(e.stack).or.error(e);
  }
}

I'm proposing not calling loadTask for a task until the task needs to run.

BTW, I really appreciate that you are taking time from a well-deserved vacation (and Node Knockout?) to post replies to discussions like this one. I have no expectation of a quick reply and was impressed to see one.

@tmaslen
Copy link

tmaslen commented Nov 22, 2013

I've wanted this feature for a while too, especially as I'm using a plugin that depends on imagemin which can take up to 30 seconds to spin up. This slows everything down and is especially annoying as the task that relies on imagemin is barely used.

I've recently found a way round this. I've moved the loadNpmTask into a custom task so its conditionally loaded only when its needed. This is how the gruntfile used to be structured...

grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('images', ['copy:standardImages', 'responsive_images', 'imagemin']);

And this is how it is now...

grunt.registerTask('images', [], function () {
    grunt.loadNpmTasks('grunt-contrib-imagemin');
    grunt.task.run('copy:standardImages', 'responsive_images', 'imagemin');
});

Hope this helps and that I'm not doing something obviously wrong :-).

/t

@jacksleight
Copy link

@maslen Thanks for this, works great for me. Managed to reduce my compass compilation task by ~1.5 secs (was taking just over 3), which makes all the difference when you're using watch/livereload. For me grunt-contrib-imagemin is one of the worst offenders for startup lag, adds about a second.

@stephanebachelier
Copy link

Really helpful trick!

This seems to me the best solution :) Each task loads its own dependencies.
But what happens if two modules load the same modules? Does grunt know what
task are loaded ?

Stéphane Bachelier,
Tél. 06 42 24 48 09
B8A5 2007 0004 CDE4 5210 2317 B58A 335B B5A4 BFC2

2013/11/22 Tom Maslen notifications@github.com

I've wanted this feature for a while too, especially as I'm using a plugin
that depends on imagemin which can take up to 30 seconds to spin up. This
slows everything down and is especially annoying as the task that relies on
imagemin is barely used.

I've recently found a way round this. I've moved the loadNpmTask into a
custom task so its conditionally loaded only when its needed. This is how
the gruntfile used to be structured...

grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('images', ['copy:standardImages', 'responsive_images', 'imagemin']);

And this is how it is now...

grunt.registerTask('images', [], function () {
grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.task.run('copy:standardImages', 'responsive_images', 'imagemin');
});

Hope this helps and that I'm not doing something obviously wrong :-).

/t


Reply to this email directly or view it on GitHubhttps://github.com//issues/975#issuecomment-29058707
.

@shaekuronen
Copy link

@maslen this is dope, shaved 1.9s off tasks loading in my preview task (which uses watch/livereload, so definitely matters)

if anyone wants to see an implementation of maslen's technique, just pushed it up to grunt-ejs-static-boilerplate

what's strange is it also shaved almost that much time off tasks loading for my optimize task, which uses all the modules I was previously loading indiscriminately for the preview task. Somehow just moving loadNpmTasks inside registerTask dramatically reduced the amount of time to load. Did not see increases in any other tasks, so looks like net gain.

thanks!

@wildeyes
Copy link

+1

@yuanyan
Copy link

yuanyan commented Dec 10, 2013

@maslen Register a custom task for delay load really really nice trick.

@lmartins
Copy link

Some tasks can have a huge impact in running times, as I've just found out with the grunt-contrib-imagemin.

Im running a watcher to compile SASS, which takes about 24ms. When I add grunt-contrib-imagemin to the gruntfile, even when im not optimizing any image compilation times will immediately jump to more than 3 seconds. The time-grunt plugin shows me that 99% of that is spent on loading tasks.

You might say that 2-3 seconds is not a huge time, but when working with CSS where you are constantly saving for visualizing your changes this really degrades the workflow.

Would be really cool if this was something that could be improved in any way.

@vladikoff
Copy link
Member

@lmartins imagemin is being updated in gruntjs/grunt-contrib-imagemin#125

@kud
Copy link

kud commented Dec 19, 2013

👍

Just a sample of my watch:

Execution Time (2013-12-19 09:39:00 UTC)
loading tasks          3s  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 62%
jsvalidate:compile  241ms  ▇▇▇ 6%
jshint:dev             1s  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 28%
handlebars:compile   97ms  ▇▇ 2%
concat:compile       56ms  ▇ 1%

Loading tasks is ALWAYS the hugest task (except Sass for sure).

@lmartins
Copy link

Tried @maslen solution and it really helps. Shaves 2+ seconds on every save and now looks like this:

Execution Time (2013-12-19 09:55:38 UTC)
loading tasks  349ms  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 93%
sass:compile    23ms  ▇▇▇▇ 6%
Total 374ms

Thanks man, this makes it viable to me keep using grunt to compile sass.

@kud
Copy link

kud commented Dec 19, 2013

Indeed. Loading just packages you use is perfectly sane and efficient. Thanks @maslen !

@kud
Copy link

kud commented Dec 19, 2013

Execution Time (2013-12-19 11:45:37 UTC)
loading tasks       419ms  ▇▇ 3%
jsvalidate:compile  296ms  ▇▇ 2%
jshint:dev             1s  ▇▇▇▇▇▇ 11%
shell:fontcustom    970ms  ▇▇▇▇▇ 8%
concurrent:concat      9s  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 74%
Total 12s

\o/

So much better, thanks!

@shootaroo
Copy link

I created a JIT(Just In Time) plugins loader for Grunt.
https://github.com/shootaroo/jit-grunt

You can speed up while maintaining the simple Gruntfile. Try it.

@henrahmagix
Copy link

@shootaroo Your jit-grunt module worked fantastically well for me! Thanks!

@thSoft
Copy link

thSoft commented Feb 24, 2014

@shootaroo, thank you for your excellent module!

@bmds
Copy link

bmds commented Feb 27, 2014

@shootaroo You just shaved nearly 4 seconds off my build process, fantastic module

@abarnwell
Copy link

@shootaroo, awesome plugin, thank you!

@Scotchester
Copy link

@shootaroo Thank you for the great module! It's working perfectly for me.

@vladikoff vladikoff modified the milestones: 0.4.6, 0.4.5 Apr 8, 2014
@gpluess
Copy link

gpluess commented May 5, 2014

@shootaroo Fantastic module, thank you!

@stevenvachon
Copy link

Indeed. Works great for me as well, and he quickly fixed issues with it too.

@eckdanny
Copy link

Sweeeeeeeeet! Thanks a ton @shootaroo!

@vladikoff vladikoff modified the milestones: 0.5.0, 0.4.6 Jun 1, 2014
@thom4parisot
Copy link

Thanks @shootaroo, you have the good approach I think.

Registering a plugin should not imply to load the task itself: grunt should just know about the task dependencies tree, then asynchronously loads the tasks (if not already loaded) and run them.
So it goes in the same direction as @dylang proposes: keeping the same syntax and just changing how grunt handles their load under the hood.

@arthurpf
Copy link

@shootaroo Thank you! This is exactly what I was looking for!

@raphaeleidus
Copy link

similar to @shootaroo's module I created this for lazyloading plugins https://github.com/raphaeleidus/grunt-lazyload

@raphaeleidus
Copy link

@oncletom @shootaroo The grunt API allows one module to register multiple tasks, so unless you eager load the module or provide a full list of tasks registered this would be broken by lazy-loading. my module(https://github.com/raphaeleidus/grunt-lazyload) requires specifying the task names. I am not sure how many grunt plugins are actually taking advantage of this but unless the design pattern changes I could not find a way to lazy load tasks using the standard API.

@dickeylth
Copy link

@shootaroo Thank you!

@henrahmagix
Copy link

I think @shootaroo deserves many, many thanks for jit-grunt. He keeps it
up-to-date and maintains it really well.

On Tuesday, December 23, 2014, 弘树 notifications@github.com wrote:

@shootaroo https://github.com/shootaroo Thank you!


Reply to this email directly or view it on GitHub
#975 (comment).

@Scotchester
Copy link

👍 Thanks, @shootaroo!

@appeltaert
Copy link

When westerners are busy knitting bombastic language together, japan is here to save the day.. @shootaroo well done!

@gogromat
Copy link

gogromat commented Feb 5, 2015

Thanks for JIT grunt @shootaroo! 👍

@rbosneag
Copy link

Great job @shootaroo! Saved my Less compile time: from 1,5sec to 0,2. Awesome!

@jabranr
Copy link

jabranr commented Apr 22, 2015

Thanks @shootaroo! Works great, especially for grunt-contrib-imagemin 👍

@rdsubhas
Copy link

Kudos @shootaroo this is awesome!

@juanbrujo
Copy link

great work @shootaroo 🌟

@runemoennike
Copy link

Thanks @shootaroo, great work :)

@ajmerainfo
Copy link

Hi I am new to grunt. How can I use task.run? on below code

module.exports = function (grunt) {

    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-compass');

    grunt.initConfig({
        uglify: {
            my_target: {
                files: {
                    'assets/js/scripts.js': ['assets/components/js/*.js']
                }
            }
        },
        compass: {
          dev: {
              options: {
                  config: 'config.rb'
              }
          }  
        },
        watch: {
            options: { livereload: true },
            scripts: {
                files: ['assets/components/js/*.js'],
                tasks: ['uglify']
            },
            sass: {
                files: ['assets/components/sass/*.scss'],
                tasks: ['compass:dev']
            },
            html: {
                files: ['*.html']
            }

        }
    });

    grunt.registerTask('default', 'watch');
}

phightower-squiz pushed a commit to phightower-squiz/squiz-boilerplate that referenced this issue Oct 7, 2015
@Jeff-Lewis
Copy link

For anyone reading this issue hoping to speed up their grunt load tasks, use https://github.com/shootaroo/jit-grunt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests