Why is it recommended to use concat then uglify when the latter can do both?
Asked Answered
L

2

39

I keep seeing the recommendation for making JS files ready for production to be concat then uglify.

For example here, in on of Yeoman's grunt tasks.

By default the flow is: concat -> uglifyjs.

Considering UglifyJS can do both concatenation and minification, why would you ever need both at the same time?

Thanks.

Leyes answered 20/3, 2014 at 20:43 Comment(1)
I used to use concat, but chose to use only uglify, which does all that's needed - as you pointed out. I guess some people use both to either compensate for the complexity of their projects or because they'd rather have uglify doing only what it does besides concatenation. concat also allows for separators, which uglify doesn't as far as I'm aware of.Scranton
M
49

Running a basic test to see if there is a performance difference between executing concat and then uglify vs. just uglify.

package.json

{
  "name": "grunt-concat-vs-uglify",
  "version": "0.0.1",
  "description": "A basic test to see if we can ditch concat and use only uglify for JS files.",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-contrib-concat": "^0.5.0",
    "grunt-contrib-uglify": "^0.6.0",
    "load-grunt-tasks": "^1.0.0",
    "time-grunt": "^1.0.0"
  }
}

Gruntfile.js

module.exports = function (grunt) {

    // Display the elapsed execution time of grunt tasks
    require('time-grunt')(grunt);
    // Load all grunt-* packages from package.json
    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        paths: {
            src: {
                js: 'src/**/*.js'
            },
            dest: {
                js: 'dist/main.js',
                jsMin: 'dist/main.min.js'
            }
        },
        concat: {
            js: {
                options: {
                    separator: ';'
                },
                src: '<%= paths.src.js %>',
                dest: '<%= paths.dest.js %>'
            }
        },
        uglify: {
            options: {
                compress: true,
                mangle: true,
                sourceMap: true
            },
            target: {
                src: '<%= paths.src.js %>',
                dest: '<%= paths.dest.jsMin %>'
            }
        }
    });

    grunt.registerTask('default', 'concat vs. uglify', function (concat) {
        // grunt default:true
        if (concat) {
            // Update the uglify dest to be the result of concat
            var dest = grunt.config('concat.js.dest');
            grunt.config('uglify.target.src', dest);

            grunt.task.run('concat');
        }

        // grunt default
        grunt.task.run('uglify');
    });
};

In src, I've put a bunch of JS files, including the uncompressed source of jQuery, copied several times, spread around into subfolders. Much more than what a normal site/app usually has.

Turns out the time it takes to concat and compress all of these files is essentially the same in both scenarios.
Except when using the sourceMap: true option on concat as well (see below).

On my computer:

grunt default      : 6.2s (just uglify)
grunt default:true : 6s   (concat and uglify)

It's worth noting that the resulting main.min.js is the same in both cases.
Also, uglify automatically takes care of using the proper separator when combining the files.

The only case where it does matter is when adding sourceMap: true to the concat options.
This creates a main.js.map file next to main.js, and results in:

grunt default      : 6.2s (just uglify)
grunt default:true : 13s  (concat and uglify)

But if the production site loads only the min version, this option is useless.

I did found a major disadvantage with using concat before uglify.
When an error occurs in one of the JS files, the sourcemap will link to the concatenated main.js file and not the original file. Whereas when uglify does the whole work, it will link to the original file.

Update:
We can add 2 more options to uglify that will link the uglify sourcemap to concat sourcemap, thus handling the "disadvantage" I mentioned above.

    uglify: {
        options: {
            compress: true,
            mangle: true,
            sourceMap: true,
            sourceMapIncludeSources: true,
            sourceMapIn: '<%= paths.dest.js %>.map',
        },
        target: {
            src: '<%= paths.src.js %>',
            dest: '<%= paths.dest.jsMin %>'
        }
    }

But it seems highly unnecessary.

Conclusion

I think it's safe to conclude that we can ditch concat for JS files if we're using uglify, and use it for other purposes, when needed.

Mettle answered 12/12, 2014 at 21:38 Comment(4)
Nice effort at showing performance advantage! I'd just like to point out that my answer was meant to explain why some people still choose the transformation flow (aka. concat -> uglify) workflow to javascript compilation. This workflow choice has little to do with performance - afaik - and I personally think it only makes sense in very complex projects.Scranton
@WallaceSidhrée Thanks! What would be the benefit of using concat before uglify in a very complex project? Perhaps the process option in concat?Mettle
There's another case you didn't consider: using concat but not uglify. You'd probably only use this when in development mode. My anecdotal tests show this to be on the order of 100x faster (example: 40ms vs 4s) which could be a huge benefit while you're working on your project.Sayyid
@Sayyid The OP asked specifically about the combination of concat with uglify. You're right if you only need to concat files and not compress them, you'd obviously use only concat, but that's not the discussed use case. And it's also better to develop on the end result any way. Less friction between dev and production.Mettle
U
29

In the example you mention, which I'm quoting below, the files are first concatenated with concat and then uglified/minified by uglify:

{
  concat: {
    '.tmp/concat/js/app.js': [
      'app/js/app.js',
      'app/js/controllers/thing-controller.js',
      'app/js/models/thing-model.js',
      'app/js/views/thing-view.js'
    ]
  },
  uglifyjs: {
    'dist/js/app.js': ['.tmp/concat/js/app.js']
  }
}

The same could be achieved with:

{
  uglifyjs: {
    'dist/js/app.js': [
      'app/js/app.js',
      'app/js/controllers/thing-controller.js',
      'app/js/models/thing-model.js',
      'app/js/views/thing-view.js'
    ]
  }
}

Typically, the task clean would then run after tasks that write to a temporary folder (in this example concat) and delete whatever content is in that folder. Some people also like to run clean before tasks like compass, to delete things like randomly named image sprites (which are newly generated every time the task runs). This would keep wheels turning even for the most paranoid.

This is all a matter of preference and workflow, as is with when to run jshint. Some people like to run it before the compilation, others prefer to run it on compiled files.

Complex projects with an incredible amount of JavaScript files - or with a increasingly broad number of peers & contributors, might choose to concatenate files outside uglify just to keep things more readable and maintainable. I think this was the reasoning behind Yeoman's choice of transformation flow.

uglify can be notoriously slow depending of the project's configuration, so there might be some small gain in concatenating it with concat first - but that would have to be confirmed.

concat also supports separators, which uglify doesn't as far as README.md files are concerned.

concat: {
  options: {
    separator: ';',
  }
}
Unquestioning answered 20/3, 2014 at 22:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.