Repository: parcel-bundler/parcel
Pull request #5461 (2020-12-12 00:50:00)

Performance improvements
These are some random smaller performance improvements that add up to <strike>~23%</strike> <b>~26%</b> improvement on building Three.js in dev mode. See commits and inline comments for details.

height[bot]: > Link [Height](https://parcel.height.app) tasks by mentioning a task ID in the pull request title or description, commit messages, or comments.
>
> 💡Tip: You can also use "Close T-X" to automatically close a task when the pull request is merged.
<!-- Sent by Height -->
parcel-benchmark: ## Benchmark Results
<details><summary>Kitchen Sink ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 8.45s | -223.00ms |
| Cached | 2.15s | +3.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/legacy/parcel.d5807e82.webp | 102.94kb | +0.00b | 279.00ms | +110.00ms ⚠️ |
| dist/modern/parcel.d5807e82.webp | 102.94kb | +0.00b | 167.00ms | -26.00ms 🚀 |
| dist/legacy/index.dc3d7ad2.js | 1.10kb | +0.00b | 2.41s | +243.00ms ⚠️ |
| dist/modern/index.9c5edbbf.js | 1.10kb | +0.00b | 2.16s | -275.00ms 🚀 |
| dist/legacy/index.html | 701.00b | +0.00b | 2.56s | +274.00ms ⚠️ |
| dist/modern/index.html | 701.00b | +0.00b | 2.29s | -250.00ms 🚀 |
| dist/legacy/index.afdbda6b.css | 77.00b | +0.00b | 2.56s | +528.00ms ⚠️ |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/modern/parcel.d5807e82.webp | 102.94kb | +0.00b | 1.76s | -133.00ms 🚀 |
| dist/modern/index.9c5edbbf.js | 1.10kb | +0.00b | 1.86s | -97.00ms 🚀 |
| dist/modern/index.ef3960f9.css | 77.00b | +0.00b | 1.83s | -94.00ms 🚀 |

</p></details>

<details><summary>React HackerNews ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 26.24s | -506.00ms |
| Cached | 2.01s | -6.00ms |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/logo.24c8bf9e.png | 274.00b | +0.00b | 161.00ms | +28.00ms ⚠️ |

#### Cached Bundles

*No bundle changes detected.*

</p></details>

<details><summary>AtlasKit Editor ✅</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | 2.45m | -9.71s 🚀 |
| Cached | 4.18s | -243.00ms 🚀 |

#### Cold Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/popup.1ef21274.js | 169.35kb | +0.00b | 33.24s | -18.41s 🚀 |
| dist/EmojiPickerComponent.88cc2f04.js | 139.08kb | +0.00b | 12.62s | +908.00ms ⚠️ |
| dist/card.d65fb5b6.js | 53.76kb | +0.00b | 14.03s | +793.00ms ⚠️ |
| dist/Modal.c3eca545.js | 39.02kb | +0.00b | 6.36s | +457.00ms ⚠️ |
| dist/esm.79a76316.js | 27.71kb | +0.00b | 12.62s | +910.00ms ⚠️ |
| dist/component.b013dabf.js | 22.40kb | +0.00b | 6.36s | +459.00ms ⚠️ |
| dist/DatePicker.a3a7f16e.js | 21.16kb | +0.00b | 12.59s | +1.27s ⚠️ |
| dist/js.a617513d.js | 16.55kb | +0.00b | 6.36s | +457.00ms ⚠️ |
| dist/ui.3fa632fc.js | 14.11kb | +0.00b | 14.02s | +876.00ms ⚠️ |
| dist/component.ebf91df0.js | 6.27kb | +0.00b | 6.62s | -1.32s 🚀 |
| dist/card.ef28e700.js | 5.71kb | +0.00b | 14.06s | +825.00ms ⚠️ |
| dist/png-chunks-extract.a4983c65.js | 3.55kb | +0.00b | 6.36s | +457.00ms ⚠️ |
| dist/index.a89ddce4.css | 3.46kb | +0.00b | 58.29s | -32.62s 🚀 |
| dist/dropzone.4ee1965e.js | 3.44kb | +0.00b | 30.87s | -19.10s 🚀 |
| dist/Modal.be4d7ece.js | 3.15kb | +0.00b | 3.27s | -1.38s 🚀 |
| dist/16.6f170271.js | 2.49kb | +0.00b | 2.95s | +148.00ms ⚠️ |
| dist/ResourcedEmojiComponent.621cc121.js | 2.15kb | +0.00b | 14.02s | +1.35s ⚠️ |
| dist/date.70b38e9b.js | 1.96kb | +0.00b | 6.96s | +438.00ms ⚠️ |
| dist/images.07a0668d.js | 1.90kb | +0.00b | 7.08s | +531.00ms ⚠️ |
| dist/feedback.85c9ab3e.js | 1.86kb | +0.00b | 12.59s | +5.48s ⚠️ |
| dist/16.95553deb.js | 1.86kb | +0.00b | 6.79s | +540.00ms ⚠️ |
| dist/16.7caa2097.js | 1.79kb | +0.00b | 6.74s | +666.00ms ⚠️ |
| dist/workerHasher.8ad4130a.js | 1.75kb | +0.00b | 1.51m | +1.32m ⚠️ |
| dist/list-number.c28dbea9.js | 1.68kb | +0.00b | 7.08s | +420.00ms ⚠️ |
| dist/status.faf50fa4.js | 1.68kb | +0.00b | 7.30s | +370.00ms ⚠️ |
| dist/code.bae5edf4.js | 1.61kb | +0.00b | 6.96s | +530.00ms ⚠️ |
| dist/link.d81f6056.js | 1.53kb | +0.00b | 7.08s | +428.00ms ⚠️ |
| dist/16.81656417.js | 1.51kb | +0.00b | 6.56s | +458.00ms ⚠️ |
| dist/16.7e3cd50a.js | 1.46kb | +0.00b | 6.62s | -1.32s 🚀 |
| dist/16.6d662367.js | 1.46kb | +0.00b | 6.56s | +485.00ms ⚠️ |
| dist/emoji.ef33eb8c.js | 1.45kb | +0.00b | 6.97s | +421.00ms ⚠️ |
| dist/16.e42a1bf9.js | 1.45kb | +0.00b | 2.95s | +148.00ms ⚠️ |
| dist/16.d73da9a6.js | 1.44kb | +0.00b | 8.38s | +1.97s ⚠️ |
| dist/16.23416566.js | 1.40kb | +0.00b | 8.38s | +1.97s ⚠️ |
| dist/expand.32efe718.js | 1.38kb | +0.00b | 12.59s | +1.27s ⚠️ |
| dist/16.96477be9.js | 1.36kb | +0.00b | 6.79s | +494.00ms ⚠️ |
| dist/16.5859a374.js | 1.34kb | +0.00b | 2.95s | +150.00ms ⚠️ |
| dist/heading2.cf643822.js | 1.33kb | +0.00b | 7.43s | +464.00ms ⚠️ |
| dist/16.0349d8d1.js | 1.32kb | +0.00b | 6.79s | +490.00ms ⚠️ |
| dist/16.ac26ba20.js | 1.31kb | +0.00b | 8.38s | +1.97s ⚠️ |
| dist/media-card-analytics-error-boundary.163d021e.js | 1.30kb | +0.00b | 18.85s | -12.58s 🚀 |
| dist/mention.6e131244.js | 1.29kb | +0.00b | 7.19s | +483.00ms ⚠️ |
| dist/Modal.da758e16.js | 1.28kb | +0.00b | 6.36s | +458.00ms ⚠️ |
| dist/16.5360f030.js | 1.27kb | +0.00b | 2.95s | +150.00ms ⚠️ |
| dist/layout.e26f2b49.js | 1.27kb | +0.00b | 7.08s | +428.00ms ⚠️ |
| dist/16.abf1161b.js | 1.27kb | +0.00b | 6.79s | +539.00ms ⚠️ |
| dist/16.32ec918b.js | 1.26kb | +0.00b | 6.56s | +485.00ms ⚠️ |
| dist/16.67dfeae2.js | 1.26kb | +0.00b | 6.56s | +460.00ms ⚠️ |
| dist/16.66644ac7.js | 1.26kb | +0.00b | 6.62s | -1.32s 🚀 |
| dist/divider.2aea4732.js | 1.25kb | +0.00b | 6.96s | +438.00ms ⚠️ |
| dist/quote.e4f5d46b.js | 1.25kb | +0.00b | 7.30s | +370.00ms ⚠️ |
| dist/component.3d077ff8.js | 1.23kb | +0.00b | 3.27s | -1.38s 🚀 |
| dist/action.399063c8.js | 1.23kb | +0.00b | 8.54s | +2.11s ⚠️ |
| dist/16.600bef06.js | 1.23kb | +0.00b | 3.27s | -1.38s 🚀 |
| dist/decision.c50a3124.js | 1.21kb | +0.00b | 6.96s | +438.00ms ⚠️ |
| dist/16.3e0f980c.js | 1.18kb | +0.00b | 6.80s | -46.68s 🚀 |
| dist/list.aaf7995b.js | 1.18kb | +0.00b | 7.08s | +379.00ms ⚠️ |
| dist/panel-error.1a7ba35a.js | 1.11kb | +0.00b | 7.19s | +385.00ms ⚠️ |
| dist/panel.0d33c8ed.js | 1.10kb | +0.00b | 7.30s | +454.00ms ⚠️ |
| dist/table.f7812624.js | 1.09kb | +0.00b | 7.30s | +370.00ms ⚠️ |
| dist/media-picker-analytics-error-boundary.6408ae55.js | 1.05kb | +0.00b | 30.87s | -19.14s 🚀 |
| dist/panel-success.4e7a918e.js | 1.05kb | +0.00b | 7.19s | +385.00ms ⚠️ |
| dist/media-card-analytics-error-boundary.d50f5ebb.js | 1.05kb | +0.00b | 14.03s | +794.00ms ⚠️ |
| dist/panel-note.c50bbb87.js | 1.05kb | +0.00b | 7.19s | +385.00ms ⚠️ |
| dist/index.html | 119.00b | +0.00b | 3.83s | -2.40s 🚀 |

#### Cached Bundles

| Bundle | Size | Difference | Time | Difference |
| --- | --- | --- | --- | --- |
| dist/Toolbar.0ae0b492.js | 100.11kb | +0.00b | 1.18s | -61.00ms 🚀 |
| dist/component.b013dabf.js | 22.40kb | +0.00b | 912.00ms | -49.00ms 🚀 |
| dist/component.ebf91df0.js | 6.27kb | +0.00b | 932.00ms | -56.00ms 🚀 |
| dist/Modal.be4d7ece.js | 3.15kb | +0.00b | 896.00ms | -45.00ms 🚀 |
| dist/images.07a0668d.js | 1.90kb | +0.00b | 1.24s | +201.00ms ⚠️ |
| dist/16.7caa2097.js | 1.79kb | +0.00b | 936.00ms | -52.00ms 🚀 |
| dist/link.d81f6056.js | 1.53kb | +0.00b | 999.00ms | -53.00ms 🚀 |
| dist/16.7e3cd50a.js | 1.46kb | +0.00b | 935.00ms | -53.00ms 🚀 |
| dist/16.6d662367.js | 1.46kb | +0.00b | 953.00ms | -288.00ms 🚀 |
| dist/16.32ec918b.js | 1.26kb | +0.00b | 938.00ms | -56.00ms 🚀 |
| dist/16.66644ac7.js | 1.26kb | +0.00b | 934.00ms | -54.00ms 🚀 |

</p></details>

<details><summary>Three.js x4 🚨</summary><p>

#### Timings

| Description | Time | Difference |
| --- | --- | --- |
| Cold | **FAILED** | -0.00ms |
| Cached | **FAILED** | -0.00ms |

#### Cold Bundles

*No bundles found, this is probably a failed build...*
#### Cached Bundles

*No bundles found, this is probably a failed build...*
</p></details>

**[Click here to view a detailed benchmark overview.](https://benchmark.parceljs.org/benchmark/284668403544752648)**

Patch:
diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js
index 941f94f73..ad5bd8260 100644
--- a/packages/core/core/src/AssetGraph.js
+++ b/packages/core/core/src/AssetGraph.js
@@ -17,7 +17,11 @@ import type {
 
 import invariant from 'assert';
 import crypto from 'crypto';
-import {md5FromObject} from '@parcel/utils';
+import {
+  md5FromObject,
+  md5FromOrderedObject,
+  objectSortedEntries,
+} from '@parcel/utils';
 import nullthrows from 'nullthrows';
 import Graph, {type GraphOpts} from './Graph';
 import {createDependency} from './Dependency';
@@ -55,12 +59,15 @@ export function nodeFromDep(dep: Dependency): DependencyNode {
 
 export function nodeFromAssetGroup(assetGroup: AssetGroup): AssetGroupNode {
   return {
-    id: md5FromObject({
-      ...assetGroup,
-      // only influences building the asset graph
-      canDefer: undefined,
-      // if only the isURL property is different, no need to re-run transformation.
-      isURL: undefined,
+    id: md5FromOrderedObject({
+      filePath: assetGroup.filePath,
+      env: assetGroup.env.id,
+      isSource: assetGroup.isSource,
+      sideEffects: assetGroup.sideEffects,
+      code: assetGroup.code,
+      pipeline: assetGroup.pipeline,
+      query: assetGroup.query ? objectSortedEntries(assetGroup.query) : null,
+      invalidations: assetGroup.invalidations,
     }),
     type: 'asset_group',
     value: assetGroup,
diff --git a/packages/core/core/src/AssetGraphBuilder.js b/packages/core/core/src/AssetGraphBuilder.js
index 77883f9cb..3dc558bfb 100644
--- a/packages/core/core/src/AssetGraphBuilder.js
+++ b/packages/core/core/src/AssetGraphBuilder.js
@@ -32,7 +32,7 @@ import nullthrows from 'nullthrows';
 import path from 'path';
 import {
   escapeMarkdown,
-  md5FromObject,
+  md5FromOrderedObject,
   md5FromString,
   PromiseQueue,
   flatMap,
@@ -105,7 +105,7 @@ export default class AssetGraphBuilder extends EventEmitter {
     this.workerFarm = workerFarm;
     this.assetRequests = [];
 
-    this.cacheKey = md5FromObject({
+    this.cacheKey = md5FromOrderedObject({
       parcelVersion: PARCEL_VERSION,
       name,
       entries,
diff --git a/packages/core/core/src/BundlerRunner.js b/packages/core/core/src/BundlerRunner.js
index eab0e0866..55a486f83 100644
--- a/packages/core/core/src/BundlerRunner.js
+++ b/packages/core/core/src/BundlerRunner.js
@@ -24,7 +24,7 @@ import MutableBundleGraph from './public/MutableBundleGraph';
 import {Bundle, NamedBundle} from './public/Bundle';
 import {report} from './ReporterRunner';
 import dumpGraphToGraphViz from './dumpGraphToGraphViz';
-import {normalizeSeparators, unique, md5FromObject} from '@parcel/utils';
+import {normalizeSeparators, unique, md5FromOrderedObject} from '@parcel/utils';
 import PluginOptions from './public/PluginOptions';
 import applyRuntimes from './applyRuntimes';
 import {PARCEL_VERSION} from './constants';
@@ -169,7 +169,7 @@ export default class BundlerRunner {
     let name = this.config.getBundlerName();
     let {version} = await this.config.getBundler();
 
-    return md5FromObject({
+    return md5FromOrderedObject({
       parcelVersion: PARCEL_VERSION,
       name,
       version,
diff --git a/packages/core/core/src/Dependency.js b/packages/core/core/src/Dependency.js
index aa17a6dbc..a3bd4723c 100644
--- a/packages/core/core/src/Dependency.js
+++ b/packages/core/core/src/Dependency.js
@@ -5,9 +5,8 @@ import type {
   ModuleSpecifier,
   Symbol,
 } from '@parcel/types';
-import {md5FromObject} from '@parcel/utils';
+import {md5FromOrderedObject} from '@parcel/utils';
 import type {Dependency, Environment, Target} from './types';
-import {getEnvironmentHash} from './Environment';
 
 type DependencyOpts = {|
   id?: string,
@@ -33,10 +32,10 @@ type DependencyOpts = {|
 export function createDependency(opts: DependencyOpts): Dependency {
   let id =
     opts.id ||
-    md5FromObject({
+    md5FromOrderedObject({
       sourceAssetId: opts.sourceAssetId,
       moduleSpecifier: opts.moduleSpecifier,
-      env: getEnvironmentHash(opts.env),
+      env: opts.env.id,
       target: opts.target,
       pipeline: opts.pipeline,
     });
diff --git a/packages/core/core/src/Environment.js b/packages/core/core/src/Environment.js
index 78a8316a5..084010f4d 100644
--- a/packages/core/core/src/Environment.js
+++ b/packages/core/core/src/Environment.js
@@ -1,7 +1,7 @@
 // @flow
 import type {EnvironmentOpts} from '@parcel/types';
 import type {Environment} from './types';
-import {md5FromObject} from '@parcel/utils';
+import {md5FromOrderedObject} from '@parcel/utils';
 
 const DEFAULT_ENGINES = {
   browsers: ['> 0.25%'],
@@ -78,7 +78,8 @@ export function createEnvironment({
     }
   }
 
-  return {
+  let res: Environment = {
+    id: '',
     context,
     engines,
     includeNodeModules,
@@ -88,6 +89,9 @@ export function createEnvironment({
     scopeHoist,
     sourceMap,
   };
+
+  res.id = getEnvironmentHash(res);
+  return res;
 }
 
 export function mergeEnvironments(
@@ -95,20 +99,21 @@ export function mergeEnvironments(
   b: ?EnvironmentOpts,
 ): Environment {
   // If merging the same object, avoid copying.
-  if (a === b) {
+  if (a === b || !b) {
     return a;
   }
 
+  // $FlowFixMe - ignore the `id` that is already on a
   return createEnvironment({
     ...a,
     ...b,
   });
 }
 
-export function getEnvironmentHash(env: Environment): string {
+function getEnvironmentHash(env: Environment): string {
   // context is excluded from hash so that assets can be shared between e.g. workers and browser.
   // Different engines should be sufficient to distinguish multi-target builds.
-  return md5FromObject({
+  return md5FromOrderedObject({
     engines: env.engines,
     includeNodeModules: env.includeNodeModules,
     outputFormat: env.outputFormat,
diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js
index 3243736c2..8f5b628e8 100644
--- a/packages/core/core/src/PackagerRunner.js
+++ b/packages/core/core/src/PackagerRunner.js
@@ -19,7 +19,7 @@ import type {FileSystem, FileOptions} from '@parcel/fs';
 
 import invariant from 'assert';
 import {
-  md5FromObject,
+  md5FromOrderedObject,
   md5FromString,
   blobToStream,
   TapStream,
@@ -481,7 +481,7 @@ export default class PackagerRunner {
 
     // TODO: add third party configs to the cache key
     let {publicUrl} = bundle.target;
-    return md5FromObject({
+    return md5FromOrderedObject({
       parcelVersion: PARCEL_VERSION,
       packager,
       optimizers,
diff --git a/packages/core/core/src/ParcelConfig.js b/packages/core/core/src/ParcelConfig.js
index a9bf55983..c57f9f39b 100644
--- a/packages/core/core/src/ParcelConfig.js
+++ b/packages/core/core/src/ParcelConfig.js
@@ -23,7 +23,7 @@ import type {
   PureParcelConfigPipeline,
   ExtendableParcelConfigPipeline,
 } from './types';
-import {isMatch} from 'micromatch';
+import {makeRe} from 'micromatch';
 import {basename} from 'path';
 import loadPlugin from './loadParcelPlugin';
 
@@ -59,6 +59,7 @@ export default class ParcelConfig {
   reporters: PureParcelConfigPipeline;
   pluginCache: Map<PackageName, any>;
   autoinstall: boolean;
+  regexCache: Map<string, RegExp>;
 
   constructor(
     config: ProcessedParcelConfig,
@@ -80,6 +81,7 @@ export default class ParcelConfig {
     this.validators = config.validators || {};
     this.pluginCache = new Map();
     this.autoinstall = autoinstall;
+    this.regexCache = new Map();
   }
 
   static deserialize(serialized: SerializedParcelConfig): ParcelConfig {
@@ -343,10 +345,15 @@ export default class ParcelConfig {
       patternPipeline = null;
     }
 
+    let re = this.regexCache.get(patternGlob);
+    if (!re) {
+      re = makeRe(patternGlob, {dot: true});
+      this.regexCache.set(patternGlob, re);
+    }
+
     return (
       (pipeline === patternPipeline || (!pipeline && !patternPipeline)) &&
-      (isMatch(filePath, patternGlob, {dot: true}) ||
-        isMatch(basename(filePath), patternGlob, {dot: true}))
+      (re.test(filePath) || re.test(basename(filePath)))
     );
   }
 
diff --git a/packages/core/core/src/Transformation.js b/packages/core/core/src/Transformation.js
index fdf493d9e..b3dae1949 100644
--- a/packages/core/core/src/Transformation.js
+++ b/packages/core/core/src/Transformation.js
@@ -23,7 +23,7 @@ import path from 'path';
 import nullthrows from 'nullthrows';
 import {
   escapeMarkdown,
-  md5FromObject,
+  md5FromOrderedObject,
   normalizeSeparators,
 } from '@parcel/utils';
 import logger, {PluginLogger} from '@parcel/logger';
@@ -448,7 +448,7 @@ export default class Transformation {
     await Promise.all(
       assets.map(asset =>
         asset.commit(
-          md5FromObject({
+          md5FromOrderedObject({
             configs: getImpactfulConfigInfo(configs),
           }),
         ),
@@ -472,7 +472,7 @@ export default class Transformation {
       uniqueKey: a.value.uniqueKey,
     }));
 
-    return md5FromObject({
+    return md5FromOrderedObject({
       parcelVersion: PARCEL_VERSION,
       assets: assetsKeyInfo,
       configs: getImpactfulConfigInfo(configs),
@@ -745,6 +745,7 @@ function normalizeAssets(
       }
 
       let internalAsset = mutableAssetToUncommittedAsset(result);
+      // $FlowFixMe - ignore id already on env
       return {
         ast: internalAsset.ast,
         content: await internalAsset.content,
diff --git a/packages/core/core/src/UncommittedAsset.js b/packages/core/core/src/UncommittedAsset.js
index 4386d7d19..3493b04ad 100644
--- a/packages/core/core/src/UncommittedAsset.js
+++ b/packages/core/core/src/UncommittedAsset.js
@@ -105,14 +105,7 @@ export default class UncommittedAsset {
     // and hash while it's being written to the cache.
     await Promise.all([
       contentKey != null &&
-        this.options.cache.setStream(
-          contentKey,
-          this.getStream().pipe(
-            new TapStream(buf => {
-              size += buf.length;
-            }),
-          ),
-        ),
+        this.commitContent(contentKey).then(s => (size = s)),
       this.mapBuffer != null &&
         mapKey != null &&
         this.options.cache.setBlob(mapKey, this.mapBuffer),
@@ -141,6 +134,36 @@ export default class UncommittedAsset {
     this.value.committed = true;
   }
 
+  async commitContent(contentKey: string): Promise<number> {
+    let content = await this.content;
+    if (content == null) {
+      return 0;
+    }
+
+    let size = 0;
+    if (content instanceof Readable) {
+      await this.options.cache.setStream(
+        contentKey,
+        content.pipe(
+          new TapStream(buf => {
+            size += buf.length;
+          }),
+        ),
+      );
+
+      return size;
+    }
+
+    if (typeof content === 'string') {
+      size = Buffer.byteLength(content);
+    } else {
+      size = content.length;
+    }
+
+    await this.options.cache.setBlob(contentKey, content);
+    return size;
+  }
+
   async getCode(): Promise<string> {
     if (this.ast != null && this.isASTDirty) {
       throw new Error(
diff --git a/packages/core/core/src/applyRuntimes.js b/packages/core/core/src/applyRuntimes.js
index 593e739d7..7a2a6e359 100644
--- a/packages/core/core/src/applyRuntimes.js
+++ b/packages/core/core/src/applyRuntimes.js
@@ -70,7 +70,6 @@ export default async function applyRuntimes({
               // Runtime assets should be considered source, as they should be
               // e.g. compiled to run in the target environment
               isSource: true,
-              query: {},
             };
 
             connections.push({
diff --git a/packages/core/core/src/assetUtils.js b/packages/core/core/src/assetUtils.js
index fd6e45ed9..ddcd270ad 100644
--- a/packages/core/core/src/assetUtils.js
+++ b/packages/core/core/src/assetUtils.js
@@ -36,7 +36,6 @@ import {
   md5FromString,
   md5FromFilePath,
 } from '@parcel/utils';
-import {getEnvironmentHash} from './Environment';
 import {hashFromOption} from './utils';
 
 type AssetOptions = {|
@@ -45,7 +44,7 @@ type AssetOptions = {|
   hash?: ?string,
   idBase?: ?string,
   filePath: FilePath,
-  query?: QueryParameters,
+  query?: ?QueryParameters,
   type: string,
   contentKey?: ?string,
   mapKey?: ?string,
@@ -79,14 +78,14 @@ export function createAsset(options: AssetOptions): Asset {
         : md5FromString(
             idBase +
               options.type +
-              getEnvironmentHash(options.env) +
+              options.env.id +
               uniqueKey +
               (options.pipeline ?? ''),
           ),
     committed: options.committed ?? false,
     hash: options.hash,
     filePath: options.filePath,
-    query: options.query || {},
+    query: options.query,
     isIsolated: options.isIsolated ?? false,
     isInline: options.isInline ?? false,
     isSplittable: options.isSplittable,
@@ -222,6 +221,10 @@ export async function getInvalidationHash(
   invalidations: Array<RequestInvalidation>,
   options: ParcelOptions,
 ): Promise<string> {
+  if (invalidations.length === 0) {
+    return '';
+  }
+
   let sortedInvalidations = invalidations
     .slice()
     .sort((a, b) => (getInvalidationId(a) < getInvalidationId(b) ? -1 : 1));
diff --git a/packages/core/core/src/public/Asset.js b/packages/core/core/src/public/Asset.js
index 08fa80e66..5138a2fff 100644
--- a/packages/core/core/src/public/Asset.js
+++ b/packages/core/core/src/public/Asset.js
@@ -108,7 +108,7 @@ class BaseAsset {
   }
 
   get query(): QueryParameters {
-    return this.#asset.value.query;
+    return this.#asset.value.query ?? {};
   }
 
   get meta(): Meta {
diff --git a/packages/core/core/src/public/Config.js b/packages/core/core/src/public/Config.js
index d1777ad1a..8ed3fc23f 100644
--- a/packages/core/core/src/public/Config.js
+++ b/packages/core/core/src/public/Config.js
@@ -109,6 +109,10 @@ export default class PublicConfig implements IConfig {
       }
     }
 
+    if (filePaths.length === 0) {
+      return null;
+    }
+
     let parse = options && options.parse;
     let conf = await loadConfig(
       this.#options.inputFS,
diff --git a/packages/core/core/src/requests/AssetRequest.js b/packages/core/core/src/requests/AssetRequest.js
index 0a06b3592..4515d909d 100644
--- a/packages/core/core/src/requests/AssetRequest.js
+++ b/packages/core/core/src/requests/AssetRequest.js
@@ -6,7 +6,7 @@ import type {AssetRequestInput, AssetRequestResult} from '../types';
 import type {ConfigAndCachePath} from './ParcelConfigRequest';
 import type {TransformationResult} from '../Transformation';
 
-import {md5FromObject} from '@parcel/utils';
+import {md5FromOrderedObject, objectSortedEntries} from '@parcel/utils';
 import nullthrows from 'nullthrows';
 import createParcelConfigRequest from './ParcelConfigRequest';
 
@@ -23,7 +23,7 @@ export type AssetRequest = {|
 |};
 
 function generateRequestId(type, obj) {
-  return `${type}:${md5FromObject(obj)}`;
+  return `${type}:${md5FromOrderedObject(obj)}`;
 }
 
 export default function createAssetRequest(
@@ -42,11 +42,22 @@ const type = 'asset_request';
 function getId(input: AssetRequestInput) {
   // eslint-disable-next-line no-unused-vars
   let {optionsRef, ...hashInput} = input;
-  return `${type}:${md5FromObject(hashInput)}`;
+  return md5FromOrderedObject({
+    type,
+    filePath: input.filePath,
+    env: input.env.id,
+    isSource: input.isSource,
+    sideEffects: input.sideEffects,
+    code: input.code,
+    pipeline: input.pipeline,
+    query: input.query ? objectSortedEntries(input.query) : null,
+    invalidations: input.invalidations,
+  });
 }
 
 async function run({input, api, options, farm}: RunInput) {
-  api.invalidateOnFileUpdate(await options.inputFS.realpath(input.filePath));
+  let realpath = await options.inputFS.realpath(input.filePath);
+  api.invalidateOnFileUpdate(realpath);
   let start = Date.now();
   let {optionsRef, ...request} = input;
   let {cachePath} = nullthrows(
@@ -57,9 +68,7 @@ async function run({input, api, options, farm}: RunInput) {
   // These are used to compute the cache key for assets during transformation.
   request.invalidations = api.getInvalidations().filter(invalidation => {
     // Filter out invalidation node for the input file itself.
-    return (
-      invalidation.type !== 'file' || invalidation.filePath !== input.filePath
-    );
+    return invalidation.type !== 'file' || invalidation.filePath !== realpath;
   });
 
   let {assets, configRequests, invalidations} = (await farm.createHandle(
diff --git a/packages/core/core/src/requests/ParcelConfigRequest.js b/packages/core/core/src/requests/ParcelConfigRequest.js
index a381024fd..c9bf69bbb 100644
--- a/packages/core/core/src/requests/ParcelConfigRequest.js
+++ b/packages/core/core/src/requests/ParcelConfigRequest.js
@@ -32,6 +32,7 @@ import assert from 'assert';
 
 import ParcelConfigSchema from '../ParcelConfig.schema';
 import {optionsProxy} from '../utils';
+import ParcelConfig from '../ParcelConfig';
 
 type ConfigMap<K, V> = {[K]: V, ...};
 
@@ -91,6 +92,29 @@ export default function createParcelConfigRequest(): ParcelConfigRequest {
   };
 }
 
+const parcelConfigCache = new Map();
+
+export function getCachedParcelConfig(
+  result: ConfigAndCachePath,
+  options: ParcelOptions,
+): ParcelConfig {
+  let {config: processedConfig, cachePath} = result;
+  let config = parcelConfigCache.get(cachePath);
+  if (config) {
+    return config;
+  }
+
+  config = new ParcelConfig(
+    processedConfig,
+    options.packageManager,
+    options.inputFS,
+    options.autoinstall,
+  );
+
+  parcelConfigCache.set(cachePath, config);
+  return config;
+}
+
 export async function loadParcelConfig(
   options: ParcelOptions,
 ): Promise<{|...ParcelConfigChain, usedDefault: boolean|}> {
diff --git a/packages/core/core/src/requests/PathRequest.js b/packages/core/core/src/requests/PathRequest.js
index 5f86f52a9..3887e4168 100644
--- a/packages/core/core/src/requests/PathRequest.js
+++ b/packages/core/core/src/requests/PathRequest.js
@@ -16,7 +16,9 @@ import {report} from '../ReporterRunner';
 import PublicDependency from '../public/Dependency';
 import PluginOptions from '../public/PluginOptions';
 import ParcelConfig from '../ParcelConfig';
-import createParcelConfigRequest from './ParcelConfigRequest';
+import createParcelConfigRequest, {
+  getCachedParcelConfig,
+} from './ParcelConfigRequest';
 
 export type PathRequestResult = AssetGroup | null | void;
 
@@ -50,18 +52,15 @@ export default function createPathRequest(
     input,
   };
 }
+
 async function run({input, api, options}: RunOpts) {
-  let {config} = nullthrows(
+  let configResult = nullthrows(
     await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()),
   );
+  let config = getCachedParcelConfig(configResult, options);
   let resolverRunner = new ResolverRunner({
     options,
-    config: new ParcelConfig(
-      config,
-      options.packageManager,
-      options.inputFS,
-      options.autoinstall,
-    ),
+    config,
   });
   let assetGroup = await resolverRunner.resolve(input);
 
@@ -125,7 +124,7 @@ export class ResolverRunner {
 
     let pipeline;
     let filePath;
-    let query: QueryParameters = {};
+    let query: ?QueryParameters;
     let validPipelines = new Set(this.config.getNamedPipelines());
     if (
       // Don't consider absolute paths. Absolute paths are only supported for entries,
diff --git a/packages/core/core/src/requests/TargetRequest.js b/packages/core/core/src/requests/TargetRequest.js
index 8c88cfe04..c850f26b8 100644
--- a/packages/core/core/src/requests/TargetRequest.js
+++ b/packages/core/core/src/requests/TargetRequest.js
@@ -26,8 +26,9 @@ import {
   validateSchema,
 } from '@parcel/utils';
 import {createEnvironment} from '../Environment';
-import createParcelConfigRequest from './ParcelConfigRequest';
-import ParcelConfig from '../ParcelConfig';
+import createParcelConfigRequest, {
+  getCachedParcelConfig,
+} from './ParcelConfigRequest';
 // $FlowFixMe
 import browserslist from 'browserslist';
 import jsonMap from 'json-source-map';
@@ -74,16 +75,10 @@ async function run({input, api, options}: RunOpts) {
   );
   let targets = await targetResolver.resolve(input.packagePath);
 
-  let {config} = nullthrows(
+  let configResult = nullthrows(
     await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()),
   );
-
-  let parcelConfig = new ParcelConfig(
-    config,
-    options.packageManager,
-    options.inputFS,
-    options.autoinstall,
-  );
+  let parcelConfig = getCachedParcelConfig(configResult, options);
 
   // Find named pipelines for each target.
   let pipelineNames = new Set(parcelConfig.getNamedPipelines());
diff --git a/packages/core/core/src/summarizeRequest.js b/packages/core/core/src/summarizeRequest.js
index a6c1c0b46..dff7a2fb6 100644
--- a/packages/core/core/src/summarizeRequest.js
+++ b/packages/core/core/src/summarizeRequest.js
@@ -2,7 +2,7 @@
 import type {Blob, FilePath} from '@parcel/types';
 import type {FileSystem} from '@parcel/fs';
 
-import {md5FromReadableStream, md5FromString, TapStream} from '@parcel/utils';
+import {md5FromReadableStream, md5FromString} from '@parcel/utils';
 import path from 'path';
 
 const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
@@ -27,48 +27,42 @@ async function isFilePathSource(fs: FileSystem, filePath: FilePath) {
   );
 }
 
-function summarizeDiskRequest(
+async function summarizeDiskRequest(
   fs: FileSystem,
   req: {|filePath: FilePath, code?: string|},
 ): Promise<{|content: Blob, hash: string, size: number|}> {
-  return new Promise((resolve, reject) => {
-    let code = req.code;
-    let content: Blob;
-    let hash: string;
-    let size: number;
-    if (code == null) {
-      // As an optimization for the common case of source code, while we read in
-      // data to compute its md5 and size, buffer its contents in memory.
-      // This avoids reading the data now, and then again during transformation.
-      // If it exceeds BUFFER_LIMIT, throw it out and replace it with a stream to
-      // lazily read it at a later point.
-      content = Buffer.from([]);
-      size = 0;
-
-      let stream = fs.createReadStream(req.filePath);
-      stream.on('error', reject);
-
-      md5FromReadableStream(
-        stream.pipe(
-          new TapStream(buf => {
-            size += buf.length;
-            if (content instanceof Buffer) {
-              if (size > BUFFER_LIMIT) {
-                // if buffering this content would put this over BUFFER_LIMIT, replace
-                // it with a stream
-                content = fs.createReadStream(req.filePath);
-              } else {
-                content = Buffer.concat([content, buf]);
-              }
-            }
-          }),
-        ),
-      ).then(hash => resolve({content, hash, size}), reject);
+  let code = req.code;
+  let content: Blob;
+  let hash: string;
+  let size: number;
+  if (code == null) {
+    // Get the filesize. If greater than BUFFER_LIMIT, use a stream to
+    // compute the hash. In the common case, it's faster to just read the entire
+    // file first and do the hash all at once without the overhead of streams.
+    size = (await fs.stat(req.filePath)).size;
+    if (size > BUFFER_LIMIT) {
+      return new Promise((resolve, reject) => {
+        let stream = fs.createReadStream(req.filePath);
+        stream.on('error', reject);
+        md5FromReadableStream(stream).then(
+          hash =>
+            resolve({
+              content: fs.createReadStream(req.filePath),
+              hash,
+              size,
+            }),
+          reject,
+        );
+      });
     } else {
-      content = code;
-      hash = md5FromString(code);
-      size = Buffer.from(code).length;
-      resolve({content, hash, size});
+      content = await fs.readFile(req.filePath);
+      hash = md5FromString(content);
     }
-  });
+  } else {
+    content = code;
+    hash = md5FromString(code);
+    size = Buffer.byteLength(code);
+  }
+
+  return {content, hash, size};
 }
diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js
index 8c41c5cc1..8083dd7bf 100644
--- a/packages/core/core/src/types.js
+++ b/packages/core/core/src/types.js
@@ -60,6 +60,7 @@ export type ProcessedParcelConfig = {|
 |};
 
 export type Environment = {|
+  id: string,
   context: EnvironmentContext,
   engines: Engines,
   includeNodeModules:
@@ -109,7 +110,7 @@ export type Asset = {|
   committed: boolean,
   hash: ?string,
   filePath: FilePath,
-  query: QueryParameters,
+  query: ?QueryParameters,
   type: string,
   dependencies: Map<string, Dependency>,
   isIsolated: boolean,
@@ -247,7 +248,7 @@ export type AssetRequestInput = {|
   pipeline?: ?string,
   optionsRef: SharedReference,
   isURL?: boolean,
-  query: QueryParameters,
+  query?: ?QueryParameters,
   invalidations?: Array<RequestInvalidation>,
 |};
 
diff --git a/packages/core/core/src/worker.js b/packages/core/core/src/worker.js
index f645fe9ca..25662c067 100644
--- a/packages/core/core/src/worker.js
+++ b/packages/core/core/src/worker.js
@@ -48,10 +48,13 @@ function loadOptions(ref, workerApi) {
 }
 
 async function loadConfig(cachePath, options) {
-  let processedConfig =
-    parcelConfigCache.get(cachePath) ??
-    nullthrows(await options.cache.get(cachePath));
-  let config = new ParcelConfig(
+  let config = parcelConfigCache.get(cachePath);
+  if (config) {
+    return config;
+  }
+
+  let processedConfig = nullthrows(await options.cache.get(cachePath));
+  config = new ParcelConfig(
     // $FlowFixMe
     ((processedConfig: any): ProcessedParcelConfig),
     options.packageManager,
diff --git a/packages/core/fs/src/NodeFS.js b/packages/core/fs/src/NodeFS.js
index b84172629..501f42d41 100644
--- a/packages/core/fs/src/NodeFS.js
+++ b/packages/core/fs/src/NodeFS.js
@@ -22,7 +22,9 @@ import packageJSON from '../package.json';
 // Most of this can go away once we only support Node 10+, which includes
 // require('fs').promises
 
-const realpath = promisify(fs.realpath);
+const realpath = promisify(
+  process.platform === 'win32' ? fs.realpath : fs.realpath.native,
+);
 
 export class NodeFS implements FileSystem {
   readFile: any = promisify(fs.readFile);
diff --git a/packages/core/utils/src/index.js b/packages/core/utils/src/index.js
index 45806d417..b689dd853 100644
--- a/packages/core/utils/src/index.js
+++ b/packages/core/utils/src/index.js
@@ -47,6 +47,7 @@ export {
   md5FromString,
   md5FromReadableStream,
   md5FromObject,
+  md5FromOrderedObject,
   md5FromFilePath,
 } from './md5';
 export {fuzzySearch} from './schema';
diff --git a/packages/core/utils/src/md5.js b/packages/core/utils/src/md5.js
index b3fb460bb..7d7f7b6a6 100644
--- a/packages/core/utils/src/md5.js
+++ b/packages/core/utils/src/md5.js
@@ -34,6 +34,13 @@ export function md5FromReadableStream(stream: Readable): Promise<string> {
   });
 }
 
+export function md5FromOrderedObject(
+  obj: {+[string]: mixed, ...},
+  encoding: StringHashEncoding = 'hex',
+): string {
+  return md5FromString(JSON.stringify(obj), encoding);
+}
+
 export function md5FromObject(
   obj: {+[string]: mixed, ...},
   encoding: StringHashEncoding = 'hex',


Test patch:
diff --git a/packages/core/core/test/Environment.test.js b/packages/core/core/test/Environment.test.js
index 40dd9b048..2aeaa3a5b 100644
--- a/packages/core/core/test/Environment.test.js
+++ b/packages/core/core/test/Environment.test.js
@@ -6,6 +6,7 @@ import {createEnvironment} from '../src/Environment';
 describe('Environment', () => {
   it('assigns a default environment with nothing passed', () => {
     assert.deepEqual(createEnvironment(), {
+      id: 'd893b0f359833ebee3d11f698c4dc13d',
       context: 'browser',
       engines: {
         browsers: ['> 0.25%'],
@@ -21,6 +22,7 @@ describe('Environment', () => {
 
   it('assigns a node context if a node engine is given', () => {
     assert.deepEqual(createEnvironment({engines: {node: '>= 10.0.0'}}), {
+      id: '12f0c7bb61ca06ca47583860e07602c1',
       context: 'node',
       engines: {
         node: '>= 10.0.0',
@@ -38,6 +40,7 @@ describe('Environment', () => {
     assert.deepEqual(
       createEnvironment({engines: {browsers: ['last 1 version']}}),
       {
+        id: '618f428f683b1b2f0a59a84dbbf66200',
         context: 'browser',
         engines: {
           browsers: ['last 1 version'],
@@ -54,6 +57,7 @@ describe('Environment', () => {
 
   it('assigns default engines for node', () => {
     assert.deepEqual(createEnvironment({context: 'node'}), {
+      id: 'cebf5179f904ad2c9432ca3dd6222bfe',
       context: 'node',
       engines: {
         node: '>= 8.0.0',
@@ -69,6 +73,7 @@ describe('Environment', () => {
 
   it('assigns default engines for browsers', () => {
     assert.deepEqual(createEnvironment({context: 'browser'}), {
+      id: 'd893b0f359833ebee3d11f698c4dc13d',
       context: 'browser',
       engines: {
         browsers: ['> 0.25%'],
diff --git a/packages/core/core/test/TargetRequest.test.js b/packages/core/core/test/TargetRequest.test.js
index 643450d51..b6dcd60fb 100644
--- a/packages/core/core/test/TargetRequest.test.js
+++ b/packages/core/core/test/TargetRequest.test.js
@@ -103,6 +103,7 @@ describe('TargetResolver', () => {
           publicUrl: '/',
           distDir: path.resolve('customA'),
           env: {
+            id: '2bbfcf5ea2bb87e160da13caff67db8f',
             context: 'browser',
             includeNodeModules: true,
             engines: {
@@ -120,6 +121,7 @@ describe('TargetResolver', () => {
           publicUrl: '/',
           distDir: path.resolve('customB'),
           env: {
+            id: '6998383bc9be968857af07758ec34b4f',
             context: 'node',
             includeNodeModules: false,
             engines: {
@@ -148,6 +150,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: 'f4baf35b3b94e63f7c6307153fce8f3e',
             context: 'node',
             engines: {
               node: '>= 8.0.0',
@@ -177,6 +180,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: '5a263676bee4cf8248c32f0b48b22902',
             context: 'browser',
             engines: {
               browsers: ['last 1 version'],
@@ -208,6 +212,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/assets',
           env: {
+            id: 'ee3d4744fab93996af88b671fb4a0029',
             context: 'browser',
             engines: {
               browsers: ['last 1 version'],
@@ -247,6 +252,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: 'cebf5179f904ad2c9432ca3dd6222bfe',
             context: 'node',
             engines: {
               node: '>= 8.0.0',
@@ -288,6 +294,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: 'f4baf35b3b94e63f7c6307153fce8f3e',
             context: 'node',
             engines: {
               node: '>= 8.0.0',
@@ -320,6 +327,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: 'f6f700fe6b930b17f56439448c57e060',
             context: 'browser',
             engines: {
               browsers: ['last 1 version'],
@@ -352,6 +360,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: '61e2044abf03e54962ab861fc0c8e3a8',
             context: 'browser',
             engines: {
               browsers: ['ie11'],
@@ -390,6 +399,7 @@ describe('TargetResolver', () => {
           distEntry: undefined,
           publicUrl: 'www',
           env: {
+            id: 'fbd1c2018703515f23707d6fb810642e',
             context: 'browser',
             engines: {
               browsers: '> 0.25%',
@@ -416,6 +426,7 @@ describe('TargetResolver', () => {
         distEntry: 'index.js',
         publicUrl: '/',
         env: {
+          id: '60e4160438cbc3393873f192d9d5d162',
           context: 'node',
           engines: {},
           includeNodeModules: false,
@@ -450,6 +461,7 @@ describe('TargetResolver', () => {
         distEntry: 'index.html',
         publicUrl: '/',
         env: {
+          id: '731766329e2871538bb5cbfdbe551905',
           context: 'browser',
           engines: {},
           includeNodeModules: true,
@@ -489,6 +501,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/',
           env: {
+            id: 'f4baf35b3b94e63f7c6307153fce8f3e',
             context: 'node',
             engines: {
               node: '>= 8.0.0',
@@ -518,6 +531,7 @@ describe('TargetResolver', () => {
           distEntry: 'index.js',
           publicUrl: '/assets',
           env: {
+            id: 'ee3d4744fab93996af88b671fb4a0029',
             context: 'browser',
             engines: {
               browsers: ['last 1 version'],
@@ -561,6 +575,7 @@ describe('TargetResolver', () => {
           distDir: serveDistDir,
           publicUrl: '/',
           env: {
+            id: '731766329e2871538bb5cbfdbe551905',
             context: 'browser',
             engines: {},
             includeNodeModules: true,
@@ -586,6 +601,7 @@ describe('TargetResolver', () => {
           distDir: path.join(DEFAULT_DISTPATH_FIXTURE_PATHS.none, 'dist'),
           publicUrl: '/',
           env: {
+            id: 'f30b1b1f9bc625becd70ec6bc8198eb3',
             context: 'browser',
             engines: {
               browsers: ['Chrome 80'],
@@ -614,6 +630,7 @@ describe('TargetResolver', () => {
           distEntry: undefined,
           publicUrl: '/',
           env: {
+            id: 'f30b1b1f9bc625becd70ec6bc8198eb3',
             context: 'browser',
             engines: {
               browsers: ['Chrome 80'],
@@ -647,6 +664,7 @@ describe('TargetResolver', () => {
           distEntry: undefined,
           publicUrl: '/',
           env: {
+            id: 'f6f700fe6b930b17f56439448c57e060',
             context: 'browser',
             engines: {
               browsers: ['last 1 version'],
@@ -670,6 +688,7 @@ describe('TargetResolver', () => {
           distEntry: undefined,
           publicUrl: '/',
           env: {
+            id: '0036eed1f5d93ad9b6510b2e27210cec',
             context: 'browser',
             engines: {
               browsers: ['IE 11'],