Performance issues with Browserify + Watchify + Tsify + Gulp

2.3k Views Asked by At

I have a "medium" Typescript application (as in, not trivial, but not enterprise-level either, many thousands of lines) with dependencies on jQuery, React and SocketIO - among other smaller libraries.

My current gulpfile is this:

var gulp = require("gulp"),
    $ = require("gulp-load-plugins")(),
    _ = require("lodash"),
    tsify = require("tsify"),
    browserify = require("browserify"),
    source = require("vinyl-source-stream"),
    debowerify = require("debowerify"),
    watchify = require("watchify"),
    lr = require("tiny-lr"),
    buffer = require("vinyl-buffer");

var lrServer = lr();

var config = {
    scripts: {
        base: __dirname + "/Resources/Scripts",
        main: "Application.ts",
        output: "App.js"
    },

    styles: {
        base: __dirname + "/Resources/Styles",
        sheets: ["Application.less", "Preload.less"],
        autoprefixer: ["last 2 version", "safari 5", "ie 8", "ie 9", "opera 12.1", "ios 6", "android 4"]
    },

    publicPath: __dirname + "/wwwroot"
};

function printError(err) {
    $.util.log($.util.colors.red.bold(err.type + " " + err.name + ":"), $.util.colors.white(err.message));
    this.emit("end");
}

function buildScripts(watch, debug) {
    var bundler = browserify({
            basedir: config.scripts.base,
            debug: false,
            entries: [config.scripts.base + "/" + config.scripts.main],
            cache: {},
            packageCache: {}
        })
        .plugin(tsify, {
            module: "commonjs",
            target: "es5",
            jsx: "react"
        })
        .transform(debowerify);

    function build() {
        return bundler.bundle()
            .on("error", printError)
            .pipe(source(config.scripts.output))
            .pipe($.if(!debug, buffer()))
            .pipe($.if(!debug, $.uglify()))
            .pipe(gulp.dest(config.publicPath + "/" + "scripts"));
    }

    if (!watch)
        return build();

    bundler
        .plugin(watchify)
        .on("update", function () {
            $.util.log($.util.colors.grey("Building scripts..."));
            build();
        })
        .on("time", function (timeMs) {
            $.util.log(
                $.util.colors.grey("Finished"),
                $.util.colors.cyan("'dev.scripts.watch' after"),
                $.util.colors.magenta(timeMs.toLocaleString() + " ms"));
        });

    return build();
}

gulp.task("prod.scripts", function() {
    return buildScripts(false, false);
});

gulp.task("dev.scripts", function () {
    return buildScripts(false, true);
});

gulp.task("dev.scripts.watch", function () {
    return buildScripts(true, true);
});

gulp.task("prod.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.uglifycss())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.sourcemaps.init())
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.sourcemaps.write())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles.watch", ["dev.styles"], function () {
    return gulp.watch(config.styles.base + "/**/*.{css,less}", ["dev.styles"]);
});

gulp.task("dev.watch", ["dev.scripts.watch", "dev.styles.watch"], function () {
    lrServer.listen(35729);

    gulp.watch(config.publicPath + "/styles/**").on("change", function(file) {
        lrServer.changed({ body: { files: [file.path] } });
    });
});

gulp.task("dev", ["dev.styles", "dev.scripts"]);
gulp.task("prod", ["prod.styles", "prod.scripts"]);

Everything works as expected, however, the build times when using the watch task are taking many seconds. The odd thing is that my task reports that the re-compilation of the scripts happen in under 500ms (the event handler on the "time" event), yet if I count in my head it doesn't finish until three to four seconds after.

Note that before I pasted in my existing TypeScript code, I was loading/bundling jQuery, React, Moment and the other libraries that I was using very quickly. Because of this, I don't think that using a separate vendor bundle would speed anything up. Also, not writing out sourcemaps doesn't seem to impact the performance either.

Before I switched to browserify, I was using gulp-typescript for compilation and requirejs for module loading. Those builds took under a second. However, requirejs was causing issues for other reasons - and either way, I want to move away from AMD to CommonJS.

For now it's not a huge concern, but as the project grows it could certainly cause issues with my development flow. With a project only this large, how much longer will it take to process anything larger?

Furthermore, it's also causing issues with Visual Studio. This is an ASP.NET 5 application, and Visual Studio apparently insists on re-loading/re-parsing the bundled JavaScript file every time it changes, causing a lag in the IDE for 1-2 seconds after every change: on top of the 3-4 seconds it takes for recompilation itself. The script is being rendered to my wwwroot folder, and there seems to be no way to "exclude" the scripts sub-folder with the ASP.NET 5 tooling.

I know that I'm missing something somewhere. A possible issue is that tsify isn't using typescript's "project" feature to implement reloading, causing the TypeScript compiler to re-process each file for every change.

Anyway, I can't be the only person who has used these tools beyond toy projects, so I'm asking here if anyone has a better solution; since besides this issue, everything is working very well.

EDIT --------------------------------

OK, gonna have to eat my own words. Builds are down to roughly a second now that I'm bundling my third party libraries into their own bundle. Here's my updated gulpfile (notice the new dev.scripts.vendor task and the .external call in the buildScripts function)

var gulp = require("gulp"),
    $ = require("gulp-load-plugins")(),
    _ = require("lodash"),
    tsify = require("tsify"),
    browserify = require("browserify"),
    source = require("vinyl-source-stream"),
    debowerify = require("debowerify"),
    watchify = require("watchify"),
    lr = require("tiny-lr"),
    buffer = require("vinyl-buffer");

var lrServer = lr();

var config = {
    scripts: {
        base: __dirname + "/Resources/Scripts",
        main: "Application.ts",
        output: "App.js",
        vendor: ["react", "jquery", "moment", "socket.io-client", "lodash", "react-dom"]
    },

    styles: {
        base: __dirname + "/Resources/Styles",
        sheets: ["Application.less", "Preload.less"],
        autoprefixer: ["last 2 version", "safari 5", "ie 8", "ie 9", "opera 12.1", "ios 6", "android 4"]
    },

    publicPath: __dirname + "/wwwroot"
};

function printError(err) {
    $.util.log($.util.colors.red.bold(err.type + " " + err.name + ":"), $.util.colors.white(err.message));
    this.emit("end");
}

function buildScripts(watch, debug) {
    var bundler = browserify({
            basedir: config.scripts.base,
            debug: false,
            entries: [config.scripts.base + "/" + config.scripts.main],
            cache: {},
            packageCache: {}
        })
        .plugin(tsify, {
            module: "commonjs",
            target: "es5",
            jsx: "react"
        });

    if (debug)
        bundler.external(config.scripts.vendor);

    function build() {
        return bundler.bundle()
            .on("error", printError)
            .pipe(source(config.scripts.output))
            .pipe($.if(!debug, buffer()))
            .pipe($.if(!debug, $.uglify()))
            .pipe(gulp.dest(config.publicPath + "/" + "scripts"));
    }

    if (!watch)
        return build();

    bundler
        .plugin(watchify)
        .on("update", function () {
            $.util.log($.util.colors.grey("Building scripts..."));
            build();
        })
        .on("time", function (timeMs) {
            $.util.log(
                $.util.colors.grey("Finished"),
                $.util.colors.cyan("'dev.scripts.watch' after"),
                $.util.colors.magenta(timeMs.toLocaleString() + " ms"));
        });

    return build();
}

gulp.task("prod.scripts", function() {
    return buildScripts(false, false);
});

gulp.task("dev.scripts", ["dev.scripts.vendor"], function () {
    return buildScripts(false, true);
});

gulp.task("dev.scripts.vendor", function() {
    return browserify({
            debug: true,
            cache: {},
            packageCache: {},
            require: config.scripts.vendor
        })
        .bundle()
        .on("error", printError)
        .pipe(source("Vendor.js"))
        .pipe(gulp.dest(config.publicPath + "/" + "scripts"));
});

gulp.task("dev.scripts.watch", ["dev.scripts.vendor"], function () {
    return buildScripts(true, true);
});

gulp.task("prod.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.uglifycss())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.sourcemaps.init())
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.sourcemaps.write())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles.watch", ["dev.styles"], function () {
    return gulp.watch(config.styles.base + "/**/*.{css,less}", ["dev.styles"]);
});

gulp.task("dev.watch", ["dev.scripts.watch", "dev.styles.watch"], function () {
    lrServer.listen(35729);

    gulp.watch(config.publicPath + "/styles/**").on("change", function(file) {
        lrServer.changed({ body: { files: [file.path] } });
    });
});

gulp.task("dev", ["dev.styles", "dev.scripts"]);
gulp.task("prod", ["prod.styles", "prod.scripts"]);

However, I'm still getting an odd issue. With sourcemaps disabled (which seem to have an impact now as far as speed), my on("time", () => {}) callback is reporting 60-80ms for every file change, but it still hangs for about a second. A second is about all I'm willing to wait for this, so again I'm worried that as the project grows this wait could grow as well.

It'd be interesting to see what this additional second of time is being spent on, when the event is reporting something much smaller. Perhaps I'll start digging into the source a bit since it seems nobody has the answer right off the bat.

ANOTHER ISSUE this is just a sidenote, but debowerify no longer works with this. When using debowerify+bower, it will continue to render the required module in the final output, even if that module is listed in the "external" list. So currently with this setup, I can only use npm modules unless I'm OK with adding more compilation time to my app bundle.

Also, I learned that debowerify will override npm modules, and that it's based off of the directory listing of bower_components, NOT your bower config file. I had jQuery installed in npm, and only bootstrap in bower; but since bootstrap pulled down jQuery as a dependency, the bower jQuery module was being loaded preferentially over the NPM jQuery. Just a heads up for people.

1

There are 1 best solutions below

1
On

Forget this, just use latest TS + webpack :)