title: Performance issues with Browserify + Watchify + Tsify + Gulp
body: <p>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.</p>
  
  <p>My current gulpfile is this:</p>
  
  <pre><code>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"]);
  </code></pre>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>For now it's not a <em>huge</em> 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?</p>
  
  <p>Furthermore, it's also causing issues with Visual Studio. This is an ASP.NET 5 application, and Visual Studio apparently <em>insists</em> 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.</p>
  
  <p>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. </p>
  
  <p>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.</p>
  
  <p>EDIT --------------------------------</p>
  
  <p>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)</p>
  
  <pre><code>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"]);
  </code></pre>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p><strong>ANOTHER ISSUE</strong> 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.</p>
  
  <p>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.</p>