diff --git a/browser-entry.js b/browser-entry.js
--- a/browser-entry.js
+++ b/browser-entry.js
@@ -45,7 +45,7 @@ process.removeListener = function (e, fn) {
     } else {
       global.onerror = function () {};
     }
-    var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn);
+    var i = uncaughtExceptionHandlers.indexOf(fn);
     if (i !== -1) {
       uncaughtExceptionHandlers.splice(i, 1);
     }
@@ -103,7 +103,7 @@ Mocha.Runner.immediately = function (callback) {
  * only receive the 'message' attribute of the Error.
  */
 mocha.throwError = function (err) {
-  Mocha.utils.forEach(uncaughtExceptionHandlers, function (fn) {
+  uncaughtExceptionHandlers.forEach(function (fn) {
     fn(err);
   });
   throw err;
diff --git a/karma.conf.js b/karma.conf.js
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -4,7 +4,6 @@ var fs = require('fs');
 var path = require('path');
 var mkdirp = require('mkdirp');
 var baseBundleDirpath = path.join(__dirname, '.karma');
-var osName = require('os-name');
 
 var browserPlatformPairs = {
   'chrome@latest': 'Windows 8',
@@ -24,15 +23,6 @@ module.exports = function (config) {
       'expect',
       'mocha'
     ],
-    plugins: [
-      'karma-browserify',
-      'karma-chrome-launcher',
-      'karma-phantomjs-launcher',
-      'karma-expect',
-      'karma-mocha',
-      'karma-spec-reporter',
-      require('@coderbyheart/karma-sauce-launcher')
-    ],
     files: [
       // we use the BDD interface for all of the tests that
       // aren't interface-specific.
@@ -49,6 +39,7 @@ module.exports = function (config) {
           .ignore('fs')
           .ignore('path')
           .ignore('supports-color')
+          .require(path.join(__dirname, 'node_modules', 'buffer'), {expose: 'buffer'})
           .on('bundled', function (err, content) {
             if (!err && bundleDirpath) {
               // write bundle to directory for debugging
@@ -58,14 +49,17 @@ module.exports = function (config) {
           });
       }
     },
-    reporters: ['spec'],
+    reporters: ['mocha'],
     colors: true,
-    browsers: [osName() === 'macOS Sierra' ? 'Chrome' : 'PhantomJS'], // This is the default browser to run, locally
-    logLevel: config.LOG_DEBUG,
+    browsers: ['PhantomJS'],
+    logLevel: config.LOG_INFO,
     client: {
       mocha: {
         reporter: 'html'
       }
+    },
+    mochaReporter: {
+      showDiff: true
     }
   };
 
diff --git a/lib/context.js b/lib/context.js
--- a/lib/context.js
+++ b/lib/context.js
@@ -1,11 +1,5 @@
 'use strict';
 
-/**
- * Module dependencies.
- */
-
-var JSON = require('json3');
-
 /**
  * Expose `Context`.
  */
@@ -77,11 +71,10 @@ Context.prototype.slow = function (ms) {
  * Mark a test as skipped.
  *
  * @api private
- * @return {Context} self
+ * @throws Pending
  */
 Context.prototype.skip = function () {
   this.runnable().skip();
-  return this;
 };
 
 /**
diff --git a/lib/reporters/json-stream.js b/lib/reporters/json-stream.js
--- a/lib/reporters/json-stream.js
+++ b/lib/reporters/json-stream.js
@@ -5,7 +5,6 @@
  */
 
 var Base = require('./base');
-var JSON = require('json3');
 
 /**
  * Expose `List`.
diff --git a/lib/runnable.js b/lib/runnable.js
--- a/lib/runnable.js
+++ b/lib/runnable.js
@@ -5,12 +5,10 @@
  */
 
 var EventEmitter = require('events').EventEmitter;
-var JSON = require('json3');
 var Pending = require('./pending');
 var debug = require('debug')('mocha:runnable');
 var milliseconds = require('./ms');
 var utils = require('./utils');
-var create = require('lodash.create');
 
 /**
  * Save timer references to avoid Sinon interfering (see GH-237).
@@ -64,9 +62,7 @@ function Runnable (title, fn) {
 /**
  * Inherit from `EventEmitter.prototype`.
  */
-Runnable.prototype = create(EventEmitter.prototype, {
-  constructor: Runnable
-});
+utils.inherits(Runnable, EventEmitter);
 
 /**
  * Set & get timeout `ms`.
diff --git a/lib/runner.js b/lib/runner.js
--- a/lib/runner.js
+++ b/lib/runner.js
@@ -10,15 +10,10 @@ var utils = require('./utils');
 var inherits = utils.inherits;
 var debug = require('debug')('mocha:runner');
 var Runnable = require('./runnable');
-var filter = utils.filter;
-var indexOf = utils.indexOf;
-var some = utils.some;
-var keys = utils.keys;
 var stackFilter = utils.stackTraceFilter();
 var stringify = utils.stringify;
 var type = utils.type;
 var undefinedError = utils.undefinedError;
-var isArray = utils.isArray;
 
 /**
  * Non-enumerable globals.
@@ -150,11 +145,11 @@ Runner.prototype.grepTotal = function (suite) {
  * @api private
  */
 Runner.prototype.globalProps = function () {
-  var props = keys(global);
+  var props = Object.keys(global);
 
   // non-enumerables
   for (var i = 0; i < globals.length; ++i) {
-    if (~indexOf(props, globals[i])) {
+    if (~props.indexOf(globals[i])) {
       continue;
     }
     props.push(globals[i]);
@@ -316,7 +311,7 @@ Runner.prototype.hook = function (name, fn) {
           if (name === 'beforeEach' || name === 'afterEach') {
             self.test.pending = true;
           } else {
-            utils.forEach(suite.tests, function (test) {
+            suite.tests.forEach(function (test) {
               test.pending = true;
             });
             // a pending hook won't be executed twice.
@@ -780,19 +775,19 @@ function cleanSuiteReferences (suite) {
     }
   }
 
-  if (isArray(suite._beforeAll)) {
+  if (Array.isArray(suite._beforeAll)) {
     cleanArrReferences(suite._beforeAll);
   }
 
-  if (isArray(suite._beforeEach)) {
+  if (Array.isArray(suite._beforeEach)) {
     cleanArrReferences(suite._beforeEach);
   }
 
-  if (isArray(suite._afterAll)) {
+  if (Array.isArray(suite._afterAll)) {
     cleanArrReferences(suite._afterAll);
   }
 
-  if (isArray(suite._afterEach)) {
+  if (Array.isArray(suite._afterEach)) {
     cleanArrReferences(suite._afterEach);
   }
 
@@ -890,7 +885,7 @@ function filterOnly (suite) {
   } else {
     // Otherwise, do not run any of the tests in this suite.
     suite.tests = [];
-    utils.forEach(suite._onlySuites, function (onlySuite) {
+    suite._onlySuites.forEach(function (onlySuite) {
       // If there are other `only` tests/suites nested in the current `only` suite, then filter that `only` suite.
       // Otherwise, all of the tests on this `only` suite should be run, so don't filter it.
       if (hasOnly(onlySuite)) {
@@ -898,8 +893,8 @@ function filterOnly (suite) {
       }
     });
     // Run the `only` suites, as well as any other suites that have `only` tests/suites as descendants.
-    suite.suites = filter(suite.suites, function (childSuite) {
-      return indexOf(suite._onlySuites, childSuite) !== -1 || filterOnly(childSuite);
+    suite.suites = suite.suites.filter(function (childSuite) {
+      return suite._onlySuites.indexOf(childSuite) !== -1 || filterOnly(childSuite);
     });
   }
   // Keep the suite only if there is something to run
@@ -914,7 +909,7 @@ function filterOnly (suite) {
  * @api private
  */
 function hasOnly (suite) {
-  return suite._onlyTests.length || suite._onlySuites.length || some(suite.suites, hasOnly);
+  return suite._onlyTests.length || suite._onlySuites.length || suite.suites.some(hasOnly);
 }
 
 /**
@@ -926,7 +921,7 @@ function hasOnly (suite) {
  * @return {Array}
  */
 function filterLeaks (ok, globals) {
-  return filter(globals, function (key) {
+  return globals.filter(function (key) {
     // Firefox and Chrome exposes iframes as index inside the window object
     if (/^\d+/.test(key)) {
       return false;
@@ -950,7 +945,7 @@ function filterLeaks (ok, globals) {
       return false;
     }
 
-    var matched = filter(ok, function (ok) {
+    var matched = ok.filter(function (ok) {
       if (~ok.indexOf('*')) {
         return key.indexOf(ok.split('*')[0]) === 0;
       }
@@ -969,7 +964,7 @@ function filterLeaks (ok, globals) {
 function extraGlobals () {
   if (typeof process === 'object' && typeof process.version === 'string') {
     var parts = process.version.split('.');
-    var nodeVersion = utils.reduce(parts, function (a, v) {
+    var nodeVersion = parts.reduce(function (a, v) {
       return a << 8 | v;
     });
 
diff --git a/lib/suite.js b/lib/suite.js
--- a/lib/suite.js
+++ b/lib/suite.js
@@ -383,7 +383,7 @@ Suite.prototype.titlePath = function () {
  * @return {number}
  */
 Suite.prototype.total = function () {
-  return utils.reduce(this.suites, function (sum, suite) {
+  return this.suites.reduce(function (sum, suite) {
     return sum + suite.total();
   }, 0) + this.tests.length;
 };
@@ -397,8 +397,8 @@ Suite.prototype.total = function () {
  * @return {Suite}
  */
 Suite.prototype.eachTest = function (fn) {
-  utils.forEach(this.tests, fn);
-  utils.forEach(this.suites, function (suite) {
+  this.tests.forEach(fn);
+  this.suites.forEach(function (suite) {
     suite.eachTest(fn);
   });
   return this;
diff --git a/lib/utils.js b/lib/utils.js
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -6,10 +6,9 @@
  * Module dependencies.
  */
 
-var JSON = require('json3');
 var basename = require('path').basename;
 var debug = require('debug')('mocha:watch');
-var exists = require('fs').existsSync || require('path').existsSync;
+var exists = require('fs').existsSync;
 var glob = require('glob');
 var path = require('path');
 var join = path.join;
@@ -39,20 +38,6 @@ exports.escape = function (html) {
   return he.encode(String(html), { useNamedReferences: false });
 };
 
-/**
- * Array#forEach (<=IE8)
- *
- * @api private
- * @param {Array} arr
- * @param {Function} fn
- * @param {Object} scope
- */
-exports.forEach = function (arr, fn, scope) {
-  for (var i = 0, l = arr.length; i < l; i++) {
-    fn.call(scope, arr[i], i);
-  }
-};
-
 /**
  * Test if the given obj is type of string.
  *
@@ -64,118 +49,6 @@ exports.isString = function (obj) {
   return typeof obj === 'string';
 };
 
-/**
- * Array#map (<=IE8)
- *
- * @api private
- * @param {Array} arr
- * @param {Function} fn
- * @param {Object} scope
- * @return {Array}
- */
-exports.map = function (arr, fn, scope) {
-  var result = [];
-  for (var i = 0, l = arr.length; i < l; i++) {
-    result.push(fn.call(scope, arr[i], i, arr));
-  }
-  return result;
-};
-
-/**
- * Array#indexOf (<=IE8)
- *
- * @api private
- * @param {Array} arr
- * @param {Object} obj to find index of
- * @param {number} start
- * @return {number}
- */
-var indexOf = exports.indexOf = function (arr, obj, start) {
-  for (var i = start || 0, l = arr.length; i < l; i++) {
-    if (arr[i] === obj) {
-      return i;
-    }
-  }
-  return -1;
-};
-
-/**
- * Array#reduce (<=IE8)
- *
- * @api private
- * @param {Array} arr
- * @param {Function} fn
- * @param {Object} val Initial value.
- * @return {*}
- */
-var reduce = exports.reduce = function (arr, fn, val) {
-  var rval = val;
-
-  for (var i = 0, l = arr.length; i < l; i++) {
-    rval = fn(rval, arr[i], i, arr);
-  }
-
-  return rval;
-};
-
-/**
- * Array#filter (<=IE8)
- *
- * @api private
- * @param {Array} arr
- * @param {Function} fn
- * @return {Array}
- */
-exports.filter = function (arr, fn) {
-  var ret = [];
-
-  for (var i = 0, l = arr.length; i < l; i++) {
-    var val = arr[i];
-    if (fn(val, i, arr)) {
-      ret.push(val);
-    }
-  }
-
-  return ret;
-};
-
-/**
- * Array#some (<=IE8)
- *
- * @api private
- * @param {Array} arr
- * @param {Function} fn
- * @return {Array}
- */
-exports.some = function (arr, fn) {
-  for (var i = 0, l = arr.length; i < l; i++) {
-    if (fn(arr[i])) {
-      return true;
-    }
-  }
-  return false;
-};
-
-/**
- * Object.keys (<=IE8)
- *
- * @api private
- * @param {Object} obj
- * @return {Array} keys
- */
-exports.keys = typeof Object.keys === 'function' ? Object.keys : function (obj) {
-  var keys = [];
-  var has = Object.prototype.hasOwnProperty; // for `window` on <=IE8
-
-  for (var key in obj) {
-    if (has.call(obj, key)) {
-      keys.push(key);
-    }
-  }
-
-  return keys;
-};
-
 /**
  * Watch the given `files` for changes
  * and invoke `fn(file)` on modification.
@@ -196,30 +69,6 @@ exports.watch = function (files, fn) {
   });
 };
 
-/**
- * Array.isArray (<=IE8)
- *
- * @api private
- * @param {Object} obj
- * @return {Boolean}
- */
-var isArray = typeof Array.isArray === 'function' ? Array.isArray : function (obj) {
-  return Object.prototype.toString.call(obj) === '[object Array]';
-};
-
-exports.isArray = isArray;
-
-/**
- * Buffer.prototype.toJSON polyfill.
- *
- * @type {Function}
- */
-if (typeof Buffer !== 'undefined' && Buffer.prototype) {
-  Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () {
-    return Array.prototype.slice.call(this, 0);
-  };
-}
-
 /**
  * Ignored files.
  *
@@ -292,18 +141,7 @@ exports.clean = function (str) {
 
   str = str.replace(re, '');
 
-  return exports.trim(str);
-};
-
-/**
- * Trim the given `str`.
- *
- * @api private
- * @param {string} str
- * @return {string}
- */
-exports.trim = function (str) {
-  return str.replace(/^\s+|\s+$/g, '');
+  return str.trim();
 };
 
 /**
@@ -314,7 +152,7 @@ exports.trim = function (str) {
  * @return {Object}
  */
 exports.parseQuery = function (qs) {
-  return reduce(qs.replace('?', '').split('&'), function (obj, pair) {
+  return qs.replace('?', '').split('&').reduce(function (obj, pair) {
     var i = pair.indexOf('=');
     var key = pair.slice(0, i);
     var val = pair.slice(++i);
@@ -411,7 +249,7 @@ var type = exports.type = function type (value) {
     return 'undefined';
   } else if (value === null) {
     return 'null';
-  } else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
+  } else if (Buffer.isBuffer(value)) {
     return 'buffer';
   }
   return Object.prototype.toString.call(value)
@@ -437,9 +275,9 @@ var type = exports.type = function type (value) {
 exports.stringify = function (value) {
   var typeHint = type(value);
 
-  if (!~indexOf(['object', 'array', 'function'], typeHint)) {
+  if (!~['object', 'array', 'function'].indexOf(typeHint)) {
     if (typeHint === 'buffer') {
-      var json = value.toJSON();
+      var json = Buffer.prototype.toJSON.call(value);
       // Based on the toJSON result
       return jsonStringify(json.data && json.type ? json.data : json, 2)
         .replace(/,(\n|$)/g, '$1');
@@ -448,7 +286,7 @@ exports.stringify = function (value) {
     // IE7/IE8 has a bizarre String constructor; needs to be coerced
     // into an array and back to obj.
     if (typeHint === 'string' && typeof value === 'object') {
-      value = reduce(value.split(''), function (acc, char, idx) {
+      value = value.split('').reduce(function (acc, char, idx) {
         acc[idx] = char;
         return acc;
       }, {});
@@ -484,9 +322,9 @@ function jsonStringify (object, spaces, depth) {
 
   depth = depth || 1;
   var space = spaces * depth;
-  var str = isArray(object) ? '[' : '{';
-  var end = isArray(object) ? ']' : '}';
-  var length = typeof object.length === 'number' ? object.length : exports.keys(object).length;
+  var str = Array.isArray(object) ? '[' : '{';
+  var end = Array.isArray(object) ? ']' : '}';
+  var length = typeof object.length === 'number' ? object.length : Object.keys(object).length;
   // `.repeat()` polyfill
   function repeat (s, n) {
     return new Array(n).join(s);
@@ -539,7 +377,7 @@ function jsonStringify (object, spaces, depth) {
     }
     --length;
     str += '\n ' + repeat(' ', space) +
-      (isArray(object) ? '' : '"' + i + '": ') + // key
+      (Array.isArray(object) ? '' : '"' + i + '": ') + // key
       _stringify(object[i]) + // value
       (length ? ',' : ''); // comma
   }
@@ -549,17 +387,6 @@ function jsonStringify (object, spaces, depth) {
     (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end);
 }
 
-/**
- * Test if a value is a buffer.
- *
- * @api private
- * @param {*} value The value to test.
- * @return {boolean} True if `value` is a buffer, otherwise false
- */
-exports.isBuffer = function (value) {
-  return typeof Buffer !== 'undefined' && Buffer.isBuffer(value);
-};
-
 /**
  * Return a new Thing that has the keys in sorted order. Recursive.
  *
@@ -593,7 +420,7 @@ exports.canonicalize = function canonicalize (value, stack, typeHint) {
 
   stack = stack || [];
 
-  if (indexOf(stack, value) !== -1) {
+  if (stack.indexOf(value) !== -1) {
     return '[Circular]';
   }
 
@@ -605,7 +432,7 @@ exports.canonicalize = function canonicalize (value, stack, typeHint) {
       break;
     case 'array':
       withStack(value, function () {
-        canonicalizedObj = exports.map(value, function (item) {
+        canonicalizedObj = value.map(function (item) {
           return exports.canonicalize(item, stack);
         });
       });
@@ -625,7 +452,7 @@ exports.canonicalize = function canonicalize (value, stack, typeHint) {
     case 'object':
       canonicalizedObj = canonicalizedObj || {};
       withStack(value, function () {
-        exports.forEach(exports.keys(value).sort(), function (key) {
+        Object.keys(value).sort().forEach(function (key) {
           canonicalizedObj[key] = exports.canonicalize(value[key], stack);
         });
       });
@@ -765,7 +592,7 @@ exports.stackTraceFilter = function () {
   return function (stack) {
     stack = stack.split('\n');
 
-    stack = reduce(stack, function (list, line) {
+    stack = stack.reduce(function (list, line) {
       if (isMochaInternal(line)) {
         return list;
       }

diff --git a/lib/test.js b/lib/test.js
--- a/lib/test.js
+++ b/lib/test.js
@@ -5,8 +5,8 @@
  */
 
 var Runnable = require('./runnable');
-var create = require('lodash.create');
-var isString = require('./utils').isString;
+var utils = require('./utils');
+var isString = utils.isString;
 
 /**
  * Expose `Test`.
@@ -33,9 +33,7 @@ function Test (title, fn) {
 /**
  * Inherit from `Runnable.prototype`.
  */
-Test.prototype = create(Runnable.prototype, {
-  constructor: Test
-});
+utils.inherits(Test, Runnable);
 
 Test.prototype.clone = function () {
   var test = new Test(this.title, this.fn);
diff --git a/test/unit/utils.spec.js b/test/unit/utils.spec.js
--- a/test/unit/utils.spec.js
+++ b/test/unit/utils.spec.js
@@ -2,7 +2,6 @@
 
 var utils = require('../../lib/utils');
 var toISOString = require('../../lib/to-iso-string');
-var JSON = require('json3');
 
 describe('lib/utils', function () {
   describe('clean', function () {
@@ -495,91 +494,6 @@ describe('lib/utils', function () {
     });
   });
 
-  describe('isBuffer()', function () {
-    var isBuffer = utils.isBuffer;
-    it('should test if object is a Buffer', function () {
-      expect(isBuffer(Buffer.from([0x01])))
-        .to
-        .equal(true);
-      expect(isBuffer({}))
-        .to
-        .equal(false);
-    });
-  });
-
-  describe('map()', function () {
-    var map = utils.map;
-    it('should behave same as Array.prototype.map', function () {
-      if (!Array.prototype.map) {
-        this.skip();
-        return;
-      }
-
-      var arr = [
-        1,
-        2,
-        3
-      ];
-      expect(map(arr, JSON.stringify))
-        .to
-        .eql(arr.map(JSON.stringify));
-    });
-
-    it('should call the callback with 3 arguments[currentValue, index, array]',
-      function () {
-        var index = 0;
-        map([
-          1,
-          2,
-          3
-        ], function (e, i, arr) {
-          expect(e).to.equal(arr[index]);
-          expect(i).to.equal(index++);
-        });
-      });
-
-    it('should apply with the given scope', function () {
-      var scope = {};
-      map([
-        'a',
-        'b',
-        'c'
-      ], function () {
-        expect(this).to.equal(scope);
-      }, scope);
-    });
-  });
-
-  describe('some()', function () {
-    var some = utils.some;
-
-    it(
-      'should return true when some array elements pass the check of the fn parameter',
-      function () {
-        var result = some([
-          'a',
-          'b',
-          'c'
-        ], function (e) {
-          return e === 'b';
-        });
-        expect(result).to.eql(true);
-      });
-
-    it(
-      'should return false when none of the array elements pass the check of the fn parameter',
-      function () {
-        var result = some([
-          'a',
-          'b',
-          'c'
-        ], function (e) {
-          return e === 'd';
-        });
-        expect(result).to.eql(false);
-      });
-  });
-
   describe('parseQuery()', function () {
     var parseQuery = utils.parseQuery;
     it('should get queryString and return key-value object', function () {

upgrade (mostly) production deps
after #3016 but before v4.0.0, these production deps should be upgraded to the latest version, if not too painful:

- `commander`
- `debug`
- `diff`
- `glob`
- `lodash.create`
- `readable-stream`
- `supports-color`

the following dev deps should be upgraded as well, with caveats:

- `karma-phantomjs-launcher`: this should be part of #2979 
- `karma`: depends on previous item
- `browserify`: unknown effect on browser bundle

