diff --git a/integration/ngcc/test.sh b/integration/ngcc/test.sh
index 8c0fd2011e..6333a2d1a5 100755
--- a/integration/ngcc/test.sh
+++ b/integration/ngcc/test.sh
@@ -82,6 +82,10 @@ grep "FocusMonitor.decorators =" node_modules/@angular/cdk/bundles/cdk-a11y.umd.
 ivy-ngcc -l debug | grep 'Skipping'
 if [[ $? != 0 ]]; then exit 1; fi
 
+# Does it process the tasks in parallel?
+ivy-ngcc -l debug | grep 'Running ngcc on ClusterExecutor'
+if [[ $? != 0 ]]; then exit 1; fi
+
 # Check that running it with logging level error outputs nothing
 ivy-ngcc -l error | grep '.' && exit 1
 
diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts
index aa7cb215f7..4f4d3357ab 100644
--- a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts
+++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts
@@ -63,16 +63,17 @@ export interface DependencyDiagnostics {
 export type PartiallyOrderedEntryPoints = PartiallyOrderedList<EntryPoint>;
 
 /**
- * A list of entry-points, sorted by their dependencies.
+ * A list of entry-points, sorted by their dependencies, and the dependency graph.
  *
  * The `entryPoints` array will be ordered so that no entry point depends upon an entry point that
  * appears later in the array.
  *
- * Some entry points or their dependencies may be have been ignored. These are captured for
+ * Some entry points or their dependencies may have been ignored. These are captured for
  * diagnostic purposes in `invalidEntryPoints` and `ignoredDependencies` respectively.
  */
 export interface SortedEntryPointsInfo extends DependencyDiagnostics {
   entryPoints: PartiallyOrderedEntryPoints;
+  graph: DepGraph<EntryPoint>;
 }
 
 /**
@@ -109,6 +110,7 @@ export class DependencyResolver {
     return {
       entryPoints: (sortedEntryPointNodes as PartiallyOrderedList<string>)
                        .map(path => graph.getNodeData(path)),
+      graph,
       invalidEntryPoints,
       ignoredDependencies,
     };
diff --git a/packages/compiler-cli/ngcc/src/execution/api.ts b/packages/compiler-cli/ngcc/src/execution/api.ts
index 91bb84c30d..bee2e94354 100644
--- a/packages/compiler-cli/ngcc/src/execution/api.ts
+++ b/packages/compiler-cli/ngcc/src/execution/api.ts
@@ -6,7 +6,7 @@
  * found in the LICENSE file at https://angular.io/license
  */
 
-import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
+import {EntryPoint, EntryPointJsonProperty, JsonObject} from '../packages/entry_point';
 import {PartiallyOrderedList} from '../utils';
 
 
@@ -49,7 +49,7 @@ export interface Executor {
 export type PartiallyOrderedTasks = PartiallyOrderedList<Task>;
 
 /** Represents a unit of work: processing a specific format property of an entry-point. */
-export interface Task {
+export interface Task extends JsonObject {
   /** The `EntryPoint` which needs to be processed as part of the task. */
   entryPoint: EntryPoint;
 
diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/api.ts b/packages/compiler-cli/ngcc/src/execution/cluster/api.ts
new file mode 100644
index 0000000000..75ce23e4d0
--- /dev/null
+++ b/packages/compiler-cli/ngcc/src/execution/cluster/api.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
+import {JsonObject} from '../../packages/entry_point';
+import {PackageJsonChange} from '../../writing/package_json_updater';
+import {Task, TaskProcessingOutcome} from '../api';
+
+
+/** A message reporting that an unrecoverable error occurred. */
+export interface ErrorMessage extends JsonObject {
+  type: 'error';
+  error: string;
+}
+
+/** A message requesting the processing of a task. */
+export interface ProcessTaskMessage extends JsonObject {
+  type: 'process-task';
+  task: Task;
+}
+
+/**
+ * A message reporting the result of processing the currently assigned task.
+ *
+ * NOTE: To avoid the communication overhead, the task is not included in the message. Instead, the
+ *       master is responsible for keeping a mapping of workers to their currently assigned tasks.
+ */
+export interface TaskCompletedMessage extends JsonObject {
+  type: 'task-completed';
+  outcome: TaskProcessingOutcome;
+}
+
+/** A message requesting the update of a `package.json` file. */
+export interface UpdatePackageJsonMessage extends JsonObject {
+  type: 'update-package-json';
+  packageJsonPath: AbsoluteFsPath;
+  changes: PackageJsonChange[];
+}
+
+/** The type of messages sent from cluster workers to the cluster master. */
+export type MessageFromWorker = ErrorMessage | TaskCompletedMessage | UpdatePackageJsonMessage;
+
+/** The type of messages sent from the cluster master to cluster workers. */
+export type MessageToWorker = ProcessTaskMessage;
diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts b/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts
new file mode 100644
index 0000000000..a9a4921d08
--- /dev/null
+++ b/packages/compiler-cli/ngcc/src/execution/cluster/executor.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {Logger} from '../../logging/logger';
+import {PackageJsonUpdater} from '../../writing/package_json_updater';
+import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
+
+import {ClusterMaster} from './master';
+import {ClusterWorker} from './worker';
+
+
+/**
+ * An `Executor` that processes tasks in parallel (on multiple processes) and completes
+ * asynchronously.
+ */
+export class ClusterExecutor implements Executor {
+  constructor(
+      private workerCount: number, private logger: Logger,
+      private pkgJsonUpdater: PackageJsonUpdater) {}
+
+  async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
+      Promise<void> {
+    if (cluster.isMaster) {
+      this.logger.debug(
+          `Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
+
+      // This process is the cluster master.
+      const master =
+          new ClusterMaster(this.workerCount, this.logger, this.pkgJsonUpdater, analyzeEntryPoints);
+      return master.run();
+    } else {
+      // This process is a cluster worker.
+      const worker = new ClusterWorker(createCompileFn);
+      return worker.run();
+    }
+  }
+}
diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/master.ts b/packages/compiler-cli/ngcc/src/execution/cluster/master.ts
new file mode 100644
index 0000000000..768962491f
--- /dev/null
+++ b/packages/compiler-cli/ngcc/src/execution/cluster/master.ts
@@ -0,0 +1,253 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {resolve} from '../../../../src/ngtsc/file_system';
+import {Logger} from '../../logging/logger';
+import {PackageJsonUpdater} from '../../writing/package_json_updater';
+import {AnalyzeEntryPointsFn, Task, TaskQueue} from '../api';
+import {onTaskCompleted, stringifyTask} from '../utils';
+
+import {MessageFromWorker, TaskCompletedMessage, UpdatePackageJsonMessage} from './api';
+import {Deferred, sendMessageToWorker} from './utils';
+
+
+/**
+ * The cluster master is responsible for analyzing all entry-points, planning the work that needs to
+ * be done, distributing it to worker-processes and collecting/post-processing the results.
+ */
+export class ClusterMaster {
+  private finishedDeferred = new Deferred<void>();
+  private taskAssignments = new Map<number, Task|null>();
+  private taskQueue: TaskQueue;
+
+  constructor(
+      private workerCount: number, private logger: Logger,
+      private pkgJsonUpdater: PackageJsonUpdater, analyzeEntryPoints: AnalyzeEntryPointsFn) {
+    if (!cluster.isMaster) {
+      throw new Error('Tried to instantiate `ClusterMaster` on a worker process.');
+    }
+
+    this.taskQueue = analyzeEntryPoints();
+  }
+
+  run(): Promise<void> {
+    // Set up listeners for worker events (emitted on `cluster`).
+    cluster.on('online', this.wrapEventHandler(worker => this.onWorkerOnline(worker.id)));
+
+    cluster.on(
+        'message', this.wrapEventHandler((worker, msg) => this.onWorkerMessage(worker.id, msg)));
+
+    cluster.on(
+        'exit',
+        this.wrapEventHandler((worker, code, signal) => this.onWorkerExit(worker, code, signal)));
+
+    // Start the workers.
+    for (let i = 0; i < this.workerCount; i++) {
+      cluster.fork();
+    }
+
+    return this.finishedDeferred.promise.then(() => this.stopWorkers(), err => {
+      this.stopWorkers();
+      return Promise.reject(err);
+    });
+  }
+
+  /** Try to find available (idle) workers and assign them available (non-blocked) tasks. */
+  private maybeDistributeWork(): void {
+    let isWorkerAvailable = false;
+
+    // First, check whether all tasks have been completed.
+    if (this.taskQueue.allTasksCompleted) {
+      return this.finishedDeferred.resolve();
+    }
+
+    // Look for available workers and available tasks to assign to them.
+    for (const [workerId, assignedTask] of Array.from(this.taskAssignments)) {
+      if (assignedTask !== null) {
+        // This worker already has a job; check other workers.
+        continue;
+      } else {
+        // This worker is available.
+        isWorkerAvailable = true;
+      }
+
+      // This worker needs a job. See if any are available.
+      const task = this.taskQueue.getNextTask();
+      if (task === null) {
+        // No suitable work available right now.
+        break;
+      }
+
+      // Process the next task on the worker.
+      this.taskAssignments.set(workerId, task);
+      sendMessageToWorker(workerId, {type: 'process-task', task});
+
+      isWorkerAvailable = false;
+    }
+
+    // If there are no available workers or no available tasks, log (for debugging purposes).
+    if (!isWorkerAvailable) {
+      this.logger.debug(
+          `All ${this.taskAssignments.size} workers are currently busy and cannot take on more ` +
+          'work.');
+    } else {
+      const busyWorkers = Array.from(this.taskAssignments)
+                              .filter(([_workerId, task]) => task !== null)
+                              .map(([workerId]) => workerId);
+      const totalWorkerCount = this.taskAssignments.size;
+      const idleWorkerCount = totalWorkerCount - busyWorkers.length;
+
+      this.logger.debug(
+          `No assignments for ${idleWorkerCount} idle (out of ${totalWorkerCount} total) ` +
+          `workers. Busy workers: ${busyWorkers.join(', ')}`);
+
+      if (busyWorkers.length === 0) {
+        // This is a bug:
+        // All workers are idle (meaning no tasks are in progress) and `taskQueue.allTasksCompleted`
+        // is `false`, but there is still no assignable work.
+        throw new Error(
+            'There are still unprocessed tasks in the queue and no tasks are currently in ' +
+            `progress, yet the queue did not return any available tasks: ${this.taskQueue}`);
+      }
+    }
+  }
+
+  /** Handle a worker's exiting. (Might be intentional or not.) */
+  private onWorkerExit(worker: cluster.Worker, code: number|null, signal: string|null): void {
+    // If the worker's exiting was intentional, nothing to do.
+    if (worker.exitedAfterDisconnect) return;
+
+    // The worker exited unexpectedly: Determine it's status and take an appropriate action.
+    const currentTask = this.taskAssignments.get(worker.id);
+
+    this.logger.warn(
+        `Worker #${worker.id} exited unexpectedly (code: ${code} | signal: ${signal}).\n` +
+        `  Current assignment: ${(currentTask == null) ? '-' : stringifyTask(currentTask)}`);
+
+    if (currentTask == null) {
+      // The crashed worker process was not in the middle of a task:
+      // Just spawn another process.
+      this.logger.debug(`Spawning another worker process to replace #${worker.id}...`);
+      this.taskAssignments.delete(worker.id);
+      cluster.fork();
+    } else {
+      // The crashed worker process was in the middle of a task:
+      // Impossible to know whether we can recover (without ending up with a corrupted entry-point).
+      throw new Error(
+          'Process unexpectedly crashed, while processing format property ' +
+          `${currentTask.formatProperty} for entry-point '${currentTask.entryPoint.path}'.`);
+    }
+  }
+
+  /** Handle a message from a worker. */
+  private onWorkerMessage(workerId: number, msg: MessageFromWorker): void {
+    if (!this.taskAssignments.has(workerId)) {
+      const knownWorkers = Array.from(this.taskAssignments.keys());
+      throw new Error(
+          `Received message from unknown worker #${workerId} (known workers: ` +
+          `${knownWorkers.join(', ')}): ${JSON.stringify(msg)}`);
+    }
+
+    switch (msg.type) {
+      case 'error':
+        throw new Error(`Error on worker #${workerId}: ${msg.error}`);
+      case 'task-completed':
+        return this.onWorkerTaskCompleted(workerId, msg);
+      case 'update-package-json':
+        return this.onWorkerUpdatePackageJson(workerId, msg);
+      default:
+        throw new Error(
+            `Invalid message received from worker #${workerId}: ${JSON.stringify(msg)}`);
+    }
+  }
+
+  /** Handle a worker's coming online. */
+  private onWorkerOnline(workerId: number): void {
+    if (this.taskAssignments.has(workerId)) {
+      throw new Error(`Invariant violated: Worker #${workerId} came online more than once.`);
+    }
+
+    this.taskAssignments.set(workerId, null);
+    this.maybeDistributeWork();
+  }
+
+  /** Handle a worker's having completed their assigned task. */
+  private onWorkerTaskCompleted(workerId: number, msg: TaskCompletedMessage): void {
+    const task = this.taskAssignments.get(workerId) || null;
+
+    if (task === null) {
+      throw new Error(
+          `Expected worker #${workerId} to have a task assigned, while handling message: ` +
+          JSON.stringify(msg));
+    }
+
+    onTaskCompleted(this.pkgJsonUpdater, task, msg.outcome);
+
+    this.taskQueue.markTaskCompleted(task);
+    this.taskAssignments.set(workerId, null);
+    this.maybeDistributeWork();
+  }
+
+  /** Handle a worker's request to update a `package.json` file. */
+  private onWorkerUpdatePackageJson(workerId: number, msg: UpdatePackageJsonMessage): void {
+    const task = this.taskAssignments.get(workerId) || null;
+
+    if (task === null) {
+      throw new Error(
+          `Expected worker #${workerId} to have a task assigned, while handling message: ` +
+          JSON.stringify(msg));
+    }
+
+    const expectedPackageJsonPath = resolve(task.entryPoint.path, 'package.json');
+    const parsedPackageJson = task.entryPoint.packageJson;
+
+    if (expectedPackageJsonPath !== msg.packageJsonPath) {
+      throw new Error(
+          `Received '${msg.type}' message from worker #${workerId} for '${msg.packageJsonPath}', ` +
+          `but was expecting '${expectedPackageJsonPath}' (based on task assignment).`);
+    }
+
+    // NOTE: Although the change in the parsed `package.json` will be reflected in tasks objects
+    //       locally and thus also in future `process-task` messages sent to worker processes, any
+    //       processes already running and processing a task for the same entry-point will not get
+    //       the change.
+    //       Do not rely on having an up-to-date `package.json` representation in worker processes.
+    //       In other words, task processing should only rely on the info that was there when the
+    //       file was initially parsed (during entry-point analysis) and not on the info that might
+    //       be added later (during task processing).
+    this.pkgJsonUpdater.writeChanges(msg.changes, msg.packageJsonPath, parsedPackageJson);
+  }
+
+  /** Stop all workers and stop listening on cluster events. */
+  private stopWorkers(): void {
+    const workers = Object.values(cluster.workers) as cluster.Worker[];
+    this.logger.debug(`Stopping ${workers.length} workers...`);
+
+    cluster.removeAllListeners();
+    workers.forEach(worker => worker.kill());
+  }
+
+  /**
+   * Wrap an event handler to ensure that `finishedDeferred` will be rejected on error (regardless
+   * if the handler completes synchronously or asynchronously).
+   */
+  private wrapEventHandler<Args extends unknown[]>(fn: (...args: Args) => void|Promise<void>):
+      (...args: Args) => Promise<void> {
+    return async(...args: Args) => {
+      try {
+        await fn(...args);
+      } catch (err) {
+        this.finishedDeferred.reject(err);
+      }
+    };
+  }
+}
diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/package_json_updater.ts b/packages/compiler-cli/ngcc/src/execution/cluster/package_json_updater.ts
new file mode 100644
index 0000000000..aae19dd275
--- /dev/null
+++ b/packages/compiler-cli/ngcc/src/execution/cluster/package_json_updater.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
+import {JsonObject} from '../../packages/entry_point';
+import {PackageJsonChange, PackageJsonUpdate, PackageJsonUpdater, applyChange} from '../../writing/package_json_updater';
+
+import {sendMessageToMaster} from './utils';
+
+
+/**
+ * A `PackageJsonUpdater` that can safely handle update operations on multiple processes.
+ */
+export class ClusterPackageJsonUpdater implements PackageJsonUpdater {
+  constructor(private delegate: PackageJsonUpdater) {}
+
+  createUpdate(): PackageJsonUpdate {
+    return new PackageJsonUpdate((...args) => this.writeChanges(...args));
+  }
+
+  writeChanges(
+      changes: PackageJsonChange[], packageJsonPath: AbsoluteFsPath,
+      preExistingParsedJson?: JsonObject): void {
+    if (cluster.isMaster) {
+      // This is the master process:
+      // Actually apply the changes to the file on disk.
+      return this.delegate.writeChanges(changes, packageJsonPath, preExistingParsedJson);
+    }
+
+    // This is a worker process:
+    // Apply the changes in-memory (if necessary) and send a message to the master process.
+    if (preExistingParsedJson) {
+      for (const [propPath, value] of changes) {
+        if (propPath.length === 0) {
+          throw new Error(`Missing property path for writing value to '${packageJsonPath}'.`);
+        }
+
+        applyChange(preExistingParsedJson, propPath, value);
+      }
+    }
+
+    sendMessageToMaster({
+      type: 'update-package-json',
+      packageJsonPath,
+      changes,
+    });
+  }
+}
diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/utils.ts b/packages/compiler-cli/ngcc/src/execution/cluster/utils.ts
new file mode 100644
index 0000000000..351cf58e73
--- /dev/null
+++ b/packages/compiler-cli/ngcc/src/execution/cluster/utils.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {MessageFromWorker, MessageToWorker} from './api';
+
+
+
+/** Expose a `Promise` instance as well as APIs for resolving/rejecting it. */
+export class Deferred<T> {
+  /**
+   * Resolve the associated promise with the specified value.
+   * If the value is a rejection (constructed with `Promise.reject()`), the promise will be rejected
+   * instead.
+   *
+   * @param value The value to resolve the promise with.
+   */
+  resolve !: (value: T) => void;
+
+  /**
+   * Rejects the associated promise with the specified reason.
+   *
+   * @param reason The rejection reason.
+   */
+  reject !: (reason: any) => void;
+
+  /** The `Promise` instance associated with this deferred. */
+  promise = new Promise<T>((resolve, reject) => {
+    this.resolve = resolve;
+    this.reject = reject;
+  });
+}
+
+/**
+ * Send a message to the cluster master.
+ * (This function should be invoked from cluster workers only.)
+ *
+ * @param msg The message to send to the cluster master.
+ */
+export const sendMessageToMaster = (msg: MessageFromWorker): void => {
+  if (cluster.isMaster) {
+    throw new Error('Unable to send message to the master process: Already on the master process.');
+  }
+
+  if (process.send === undefined) {
+    // Theoretically, this should never happen on a worker process.
+    throw new Error('Unable to send message to the master process: Missing `process.send()`.');
+  }
+
+  process.send(msg);
+};
+
+/**
+ * Send a message to a cluster worker.
+ * (This function should be invoked from the cluster master only.)
+ *
+ * @param workerId The ID of the recipient worker.
+ * @param msg The message to send to the worker.
+ */
+export const sendMessageToWorker = (workerId: number, msg: MessageToWorker): void => {
+  if (!cluster.isMaster) {
+    throw new Error('Unable to send message to worker process: Sender is not the master process.');
+  }
+
+  const worker = cluster.workers[workerId];
+
+  if ((worker === undefined) || worker.isDead() || !worker.isConnected()) {
+    throw new Error(
+        'Unable to send message to worker process: Recipient does not exist or has disconnected.');
+  }
+
+  worker.send(msg);
+};
diff --git a/packages/compiler-cli/ngcc/src/execution/cluster/worker.ts b/packages/compiler-cli/ngcc/src/execution/cluster/worker.ts
new file mode 100644
index 0000000000..abfb4a4b3d
--- /dev/null
+++ b/packages/compiler-cli/ngcc/src/execution/cluster/worker.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {CompileFn, CreateCompileFn} from '../api';
+
+import {MessageToWorker} from './api';
+import {sendMessageToMaster} from './utils';
+
+
+/**
+ * A cluster worker is responsible for processing one task (i.e. one format property for a specific
+ * entry-point) at a time and reporting results back to the cluster master.
+ */
+export class ClusterWorker {
+  private compile: CompileFn;
+
+  constructor(createCompileFn: CreateCompileFn) {
+    if (cluster.isMaster) {
+      throw new Error('Tried to instantiate `ClusterWorker` on the master process.');
+    }
+
+    this.compile =
+        createCompileFn((_task, outcome) => sendMessageToMaster({type: 'task-completed', outcome}));
+  }
+
+  run(): Promise<void> {
+    // Listen for `ProcessTaskMessage`s and process tasks.
+    cluster.worker.on('message', (msg: MessageToWorker) => {
+      try {
+        switch (msg.type) {
+          case 'process-task':
+            return this.compile(msg.task);
+          default:
+            throw new Error(
+                `Invalid message received on worker #${cluster.worker.id}: ${JSON.stringify(msg)}`);
+        }
+      } catch (err) {
+        sendMessageToMaster({
+          type: 'error',
+          error: (err instanceof Error) ? (err.stack || err.message) : err,
+        });
+      }
+    });
+
+    // Return a promise that is never resolved.
+    return new Promise(() => undefined);
+  }
+}
diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts
index 27ec5ab8a1..3cea87e265 100644
--- a/packages/compiler-cli/ngcc/src/main.ts
+++ b/packages/compiler-cli/ngcc/src/main.ts
@@ -5,6 +5,11 @@
  * Use of this source code is governed by an MIT-style license that can be
  * found in the LICENSE file at https://angular.io/license
  */
+
+/// <reference types="node" />
+
+import {DepGraph} from 'dependency-graph';
+import * as os from 'os';
 import * as ts from 'typescript';
 
 import {AbsoluteFsPath, FileSystem, absoluteFrom, dirname, getFileSystem, resolve} from '../../src/ngtsc/file_system';
@@ -16,14 +21,17 @@ import {ModuleResolver} from './dependencies/module_resolver';
 import {UmdDependencyHost} from './dependencies/umd_dependency_host';
 import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder';
 import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder';
-import {AnalyzeEntryPointsFn, CreateCompileFn, Executor, PartiallyOrderedTasks, Task, TaskProcessingOutcome} from './execution/api';
+import {AnalyzeEntryPointsFn, CreateCompileFn, Executor, PartiallyOrderedTasks, Task, TaskProcessingOutcome, TaskQueue} from './execution/api';
+import {ClusterExecutor} from './execution/cluster/executor';
+import {ClusterPackageJsonUpdater} from './execution/cluster/package_json_updater';
 import {AsyncSingleProcessExecutor, SingleProcessExecutor} from './execution/single_process_executor';
+import {ParallelTaskQueue} from './execution/task_selection/parallel_task_queue';
 import {SerialTaskQueue} from './execution/task_selection/serial_task_queue';
 import {ConsoleLogger, LogLevel} from './logging/console_logger';
 import {Logger} from './logging/logger';
 import {hasBeenProcessed, markAsProcessed} from './packages/build_marker';
 import {NgccConfiguration} from './packages/configuration';
-import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES, getEntryPointFormat} from './packages/entry_point';
+import {EntryPoint, EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES, getEntryPointFormat} from './packages/entry_point';
 import {makeEntryPointBundle} from './packages/entry_point_bundle';
 import {Transformer} from './packages/transformer';
 import {PathMappings} from './utils';
@@ -32,6 +40,7 @@ import {InPlaceFileWriter} from './writing/in_place_file_writer';
 import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer';
 import {DirectPackageJsonUpdater, PackageJsonUpdater} from './writing/package_json_updater';
 
+
 /**
  * The options to configure the ngcc compiler for synchronous execution.
  */
@@ -100,6 +109,8 @@ export type AsyncNgccOptions = Omit<SyncNgccOptions, 'async'>& {async: true};
  */
 export type NgccOptions = AsyncNgccOptions | SyncNgccOptions;
 
+const EMPTY_GRAPH = new DepGraph<EntryPoint>();
+
 /**
  * This is the main entry-point into ngcc (aNGular Compatibility Compiler).
  *
@@ -115,8 +126,18 @@ export function mainNgcc(
      compileAllFormats = true, createNewEntryPointFormats = false,
      logger = new ConsoleLogger(LogLevel.info), pathMappings, async = false}: NgccOptions): void|
     Promise<void> {
+  // Execute in parallel, if async execution is acceptable and there are more than 1 CPU cores.
+  const inParallel = async && (os.cpus().length > 1);
+
+  // Instantiate common utilities that are always used.
+  // NOTE: Avoid eagerly instantiating anything that might not be used when running sync/async or in
+  //       master/worker process.
   const fileSystem = getFileSystem();
-  const pkgJsonUpdater = new DirectPackageJsonUpdater(fileSystem);
+  // NOTE: To avoid file corruption, ensure that each `ngcc` invocation only creates _one_ instance
+  //       of `PackageJsonUpdater` that actually writes to disk (across all processes).
+  //       This is hard to enforce automatically, when running on multiple processes, so needs to be
+  //       enforced manually.
+  const pkgJsonUpdater = getPackageJsonUpdater(inParallel, fileSystem);
 
   // The function for performing the analysis.
   const analyzeEntryPoints: AnalyzeEntryPointsFn = () => {
@@ -135,7 +156,7 @@ export function mainNgcc(
 
     const absBasePath = absoluteFrom(basePath);
     const config = new NgccConfiguration(fileSystem, dirname(absBasePath));
-    const entryPoints = getEntryPoints(
+    const {entryPoints, graph} = getEntryPoints(
         fileSystem, pkgJsonUpdater, logger, dependencyResolver, config, absBasePath,
         targetEntryPointPath, pathMappings, supportedPropertiesToConsider, compileAllFormats);
 
@@ -176,7 +197,7 @@ export function mainNgcc(
           unprocessableEntryPointPaths.map(path => `\n  - ${path}`).join(''));
     }
 
-    return new SerialTaskQueue(tasks);
+    return getTaskQueue(inParallel, tasks, graph);
   };
 
   // The function for creating the `compile()` function.
@@ -233,7 +254,7 @@ export function mainNgcc(
   };
 
   // The executor for actually planning and getting the work done.
-  const executor = getExecutor(async, logger, pkgJsonUpdater);
+  const executor = getExecutor(async, inParallel, logger, pkgJsonUpdater);
 
   return executor.execute(analyzeEntryPoints, createCompileFn);
 }
@@ -260,6 +281,11 @@ function ensureSupportedProperties(properties: string[]): EntryPointJsonProperty
   return supportedProperties;
 }
 
+function getPackageJsonUpdater(inParallel: boolean, fs: FileSystem): PackageJsonUpdater {
+  const directPkgJsonUpdater = new DirectPackageJsonUpdater(fs);
+  return inParallel ? new ClusterPackageJsonUpdater(directPkgJsonUpdater) : directPkgJsonUpdater;
+}
+
 function getFileWriter(
     fs: FileSystem, pkgJsonUpdater: PackageJsonUpdater,
     createNewEntryPointFormats: boolean): FileWriter {
@@ -267,11 +293,23 @@ function getFileWriter(
                                       new InPlaceFileWriter(fs);
 }
 
-function getExecutor(async: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater): Executor {
-  if (async) {
-    return new AsyncSingleProcessExecutor(logger, pkgJsonUpdater);
+function getTaskQueue(
+    inParallel: boolean, tasks: PartiallyOrderedTasks, graph: DepGraph<EntryPoint>): TaskQueue {
+  return inParallel ? new ParallelTaskQueue(tasks, graph) : new SerialTaskQueue(tasks);
+}
+
+function getExecutor(
+    async: boolean, inParallel: boolean, logger: Logger,
+    pkgJsonUpdater: PackageJsonUpdater): Executor {
+  if (inParallel) {
+    // Execute in parallel (which implies async).
+    // Use up to 8 CPU cores for workers, always reserving one for master.
+    const workerCount = Math.min(8, os.cpus().length - 1);
+    return new ClusterExecutor(workerCount, logger, pkgJsonUpdater);
   } else {
-    return new SingleProcessExecutor(logger, pkgJsonUpdater);
+    // Execute serially, on a single thread (either sync or async).
+    return async ? new AsyncSingleProcessExecutor(logger, pkgJsonUpdater) :
+                   new SingleProcessExecutor(logger, pkgJsonUpdater);
   }
 }
 
@@ -279,14 +317,15 @@ function getEntryPoints(
     fs: FileSystem, pkgJsonUpdater: PackageJsonUpdater, logger: Logger,
     resolver: DependencyResolver, config: NgccConfiguration, basePath: AbsoluteFsPath,
     targetEntryPointPath: string | undefined, pathMappings: PathMappings | undefined,
-    propertiesToConsider: string[], compileAllFormats: boolean): PartiallyOrderedEntryPoints {
-  const {entryPoints, invalidEntryPoints} = (targetEntryPointPath !== undefined) ?
+    propertiesToConsider: string[], compileAllFormats: boolean):
+    {entryPoints: PartiallyOrderedEntryPoints, graph: DepGraph<EntryPoint>} {
+  const {entryPoints, invalidEntryPoints, graph} = (targetEntryPointPath !== undefined) ?
       getTargetedEntryPoints(
           fs, pkgJsonUpdater, logger, resolver, config, basePath, targetEntryPointPath,
           propertiesToConsider, compileAllFormats, pathMappings) :
       getAllEntryPoints(fs, config, logger, resolver, basePath, pathMappings);
   logInvalidEntryPoints(logger, invalidEntryPoints);
-  return entryPoints;
+  return {entryPoints, graph};
 }
 
 function getTargetedEntryPoints(
@@ -302,6 +341,7 @@ function getTargetedEntryPoints(
       entryPoints: [] as unknown as PartiallyOrderedEntryPoints,
       invalidEntryPoints: [],
       ignoredDependencies: [],
+      graph: EMPTY_GRAPH,
     };
   }
   const finder = new TargetedEntryPointFinder(
diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts
index 4fe88198de..a8b35ded49 100644
--- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts
+++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts
@@ -23,7 +23,7 @@ export type EntryPointFormat = 'esm5' | 'esm2015' | 'umd' | 'commonjs';
  * An object containing information about an entry-point, including paths
  * to each of the possible entry-point formats.
  */
-export interface EntryPoint {
+export interface EntryPoint extends JsonObject {
   /** The name of the package (e.g. `@angular/core`). */
   name: string;
   /** The parsed package.json file for this entry-point. */
diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts
index b5645f0aa5..5709512f53 100644
--- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts
+++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts
@@ -5,6 +5,9 @@
  * Use of this source code is governed by an MIT-style license that can be
  * found in the LICENSE file at https://angular.io/license
  */
+
+import {DepGraph} from 'dependency-graph';
+
 import {FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
 import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
 import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver';
@@ -13,6 +16,7 @@ import {ModuleResolver} from '../../src/dependencies/module_resolver';
 import {EntryPoint} from '../../src/packages/entry_point';
 import {MockLogger} from '../helpers/mock_logger';
 
+
 interface DepMap {
   [path: string]: {resolved: string[], missing: string[]};
 }
@@ -162,6 +166,15 @@ runInEachFileSystem(() => {
         ]);
       });
 
+      it('should return the computed dependency graph', () => {
+        spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
+        const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
+
+        expect(result.graph).toEqual(jasmine.any(DepGraph));
+        expect(result.graph.size()).toBe(5);
+        expect(result.graph.dependenciesOf(third.path)).toEqual([fifth.path, fourth.path]);
+      });
+
       it('should only return dependencies of the target, if provided', () => {
         spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies));
         const entryPoints = [fifth, first, fourth, second, third];
diff --git a/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts b/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts
new file mode 100644
index 0000000000..41b8a6ce0b
--- /dev/null
+++ b/packages/compiler-cli/ngcc/test/execution/cluster/executor_spec.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {ClusterExecutor} from '../../../src/execution/cluster/executor';
+import {ClusterMaster} from '../../../src/execution/cluster/master';
+import {ClusterWorker} from '../../../src/execution/cluster/worker';
+import {PackageJsonUpdater} from '../../../src/writing/package_json_updater';
+import {MockLogger} from '../../helpers/mock_logger';
+import {mockProperty} from '../../helpers/spy_utils';
+
+
+describe('ClusterExecutor', () => {
+  const runAsClusterMaster = mockProperty(cluster, 'isMaster');
+  let masterRunSpy: jasmine.Spy;
+  let workerRunSpy: jasmine.Spy;
+  let mockLogger: MockLogger;
+  let executor: ClusterExecutor;
+
+  beforeEach(() => {
+    masterRunSpy = spyOn(ClusterMaster.prototype, 'run');
+    workerRunSpy = spyOn(ClusterWorker.prototype, 'run');
+
+    mockLogger = new MockLogger();
+    executor = new ClusterExecutor(42, mockLogger, null as unknown as PackageJsonUpdater);
+  });
+
+  describe('execute()', () => {
+    describe('(on cluster master)', () => {
+      beforeEach(() => runAsClusterMaster(true));
+
+      it('should log debug info about the executor', () => {
+        const anyFn: () => any = () => undefined;
+        executor.execute(anyFn, anyFn);
+
+        expect(mockLogger.logs.debug).toEqual([
+          ['Running ngcc on ClusterExecutor (using 42 worker processes).'],
+        ]);
+      });
+
+      it('should delegate to `ClusterMaster#run()`', async() => {
+        masterRunSpy.and.returnValue('CusterMaster#run()');
+        const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
+        const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
+
+        expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
+            .toBe('CusterMaster#run()' as any);
+
+        expect(masterRunSpy).toHaveBeenCalledWith();
+        expect(workerRunSpy).not.toHaveBeenCalled();
+
+        expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
+        expect(createCompilerFnSpy).not.toHaveBeenCalled();
+      });
+    });
+
+    describe('(on cluster worker)', () => {
+      beforeEach(() => runAsClusterMaster(false));
+
+      it('should not log debug info about the executor', () => {
+        const anyFn: () => any = () => undefined;
+        executor.execute(anyFn, anyFn);
+
+        expect(mockLogger.logs.debug).toEqual([]);
+      });
+
+      it('should delegate to `ClusterWorker#run()`', async() => {
+        workerRunSpy.and.returnValue('CusterWorker#run()');
+        const analyzeEntryPointsSpy = jasmine.createSpy('analyzeEntryPoints');
+        const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
+
+        expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
+            .toBe('CusterWorker#run()' as any);
+
+        expect(masterRunSpy).not.toHaveBeenCalledWith();
+        expect(workerRunSpy).toHaveBeenCalled();
+
+        expect(analyzeEntryPointsSpy).not.toHaveBeenCalled();
+        expect(createCompilerFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
+      });
+    });
+  });
+});
diff --git a/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts b/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts
new file mode 100644
index 0000000000..c1f743cb0d
--- /dev/null
+++ b/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts
@@ -0,0 +1,198 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+
+import {absoluteFrom as _} from '../../../../src/ngtsc/file_system';
+import {runInEachFileSystem} from '../../../../src/ngtsc/file_system/testing';
+import {ClusterPackageJsonUpdater} from '../../../src/execution/cluster/package_json_updater';
+import {JsonObject} from '../../../src/packages/entry_point';
+import {PackageJsonUpdate, PackageJsonUpdater} from '../../../src/writing/package_json_updater';
+import {mockProperty} from '../../helpers/spy_utils';
+
+
+runInEachFileSystem(() => {
+  describe('ClusterPackageJsonUpdater', () => {
+    const runAsClusterMaster = mockProperty(cluster, 'isMaster');
+    const mockProcessSend = mockProperty(process, 'send');
+    let processSendSpy: jasmine.Spy;
+    let delegate: PackageJsonUpdater;
+    let updater: ClusterPackageJsonUpdater;
+
+    beforeEach(() => {
+      processSendSpy = jasmine.createSpy('process.send');
+      mockProcessSend(processSendSpy);
+
+      delegate = new MockPackageJsonUpdater();
+      updater = new ClusterPackageJsonUpdater(delegate);
+    });
+
+    describe('createUpdate()', () => {
+      [true, false].forEach(
+          isMaster => describe(`(on cluster ${isMaster ? 'master' : 'worker'})`, () => {
+            beforeEach(() => runAsClusterMaster(isMaster));
+
+            it('should return a `PackageJsonUpdate` instance',
+               () => { expect(updater.createUpdate()).toEqual(jasmine.any(PackageJsonUpdate)); });
+
+            it('should wire up the `PackageJsonUpdate` with its `writeChanges()` method', () => {
+              const writeChangesSpy = spyOn(updater, 'writeChanges');
+              const jsonPath = _('/foo/package.json');
+              const update = updater.createUpdate();
+
+              update.addChange(['foo'], 'updated');
+              update.writeChanges(jsonPath);
+
+              expect(writeChangesSpy)
+                  .toHaveBeenCalledWith([[['foo'], 'updated']], jsonPath, undefined);
+            });
+          }));
+    });
+
+    describe('writeChanges()', () => {
+      describe('(on cluster master)', () => {
+        beforeEach(() => runAsClusterMaster(true));
+        afterEach(() => expect(processSendSpy).not.toHaveBeenCalled());
+
+        it('should forward the call to the delegate `PackageJsonUpdater`', () => {
+          const jsonPath = _('/foo/package.json');
+          const parsedJson = {foo: 'bar'};
+
+          updater.createUpdate().addChange(['foo'], 'updated').writeChanges(jsonPath, parsedJson);
+
+          expect(delegate.writeChanges)
+              .toHaveBeenCalledWith([[['foo'], 'updated']], jsonPath, parsedJson);
+        });
+
+        it('should throw, if trying to re-apply an already applied update', () => {
+          const update = updater.createUpdate().addChange(['foo'], 'updated');
+
+          expect(() => update.writeChanges(_('/foo/package.json'))).not.toThrow();
+          expect(() => update.writeChanges(_('/foo/package.json')))
+              .toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
+          expect(() => update.writeChanges(_('/bar/package.json')))
+              .toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
+        });
+      });
+
+      describe('(on cluster worker)', () => {
+        beforeEach(() => runAsClusterMaster(false));
+
+        it('should send an `update-package-json` message to the master process', () => {
+          const jsonPath = _('/foo/package.json');
+
+          const writeToProp = (propPath: string[], parsed?: JsonObject) =>
+              updater.createUpdate().addChange(propPath, 'updated').writeChanges(jsonPath, parsed);
+
+          writeToProp(['foo']);
+          expect(processSendSpy).toHaveBeenCalledWith({
+            type: 'update-package-json',
+            packageJsonPath: jsonPath,
+            changes: [[['foo'], 'updated']],
+          });
+
+          writeToProp(['bar', 'baz', 'qux'], {});
+          expect(processSendSpy).toHaveBeenCalledWith({
+            type: 'update-package-json',
+            packageJsonPath: jsonPath,
+            changes: [[['bar', 'baz', 'qux'], 'updated']],
+          });
+        });
+
+        it('should update an in-memory representation (if provided)', () => {
+          const jsonPath = _('/foo/package.json');
+          const parsedJson: JsonObject = {
+            foo: true,
+            bar: {baz: 'OK'},
+          };
+
+          const update =
+              updater.createUpdate().addChange(['foo'], false).addChange(['bar', 'baz'], 42);
+
+          // Not updated yet.
+          expect(parsedJson).toEqual({
+            foo: true,
+            bar: {baz: 'OK'},
+          });
+
+          update.writeChanges(jsonPath, parsedJson);
+
+          // Updated now.
+          expect(parsedJson).toEqual({
+            foo: false,
+            bar: {baz: 42},
+          });
+        });
+
+        it('should create any missing ancestor objects', () => {
+          const jsonPath = _('/foo/package.json');
+          const parsedJson: JsonObject = {foo: {}};
+
+          updater.createUpdate()
+              .addChange(['foo', 'bar', 'baz', 'qux'], 'updated')
+              .writeChanges(jsonPath, parsedJson);
+
+          expect(parsedJson).toEqual({
+            foo: {
+              bar: {
+                baz: {
+                  qux: 'updated',
+                },
+              },
+            },
+          });
+        });
+
+        it('should throw, if a property-path is empty', () => {
+          const jsonPath = _('/foo/package.json');
+
+          expect(() => updater.createUpdate().addChange([], 'missing').writeChanges(jsonPath, {}))
+              .toThrowError(`Missing property path for writing value to '${jsonPath}'.`);
+        });
+
+        it('should throw, if a property-path points to a non-object intermediate value', () => {
+          const jsonPath = _('/foo/package.json');
+          const parsedJson = {foo: null, bar: 42, baz: {qux: []}};
+
+          const writeToProp = (propPath: string[], parsed?: JsonObject) =>
+              updater.createUpdate().addChange(propPath, 'updated').writeChanges(jsonPath, parsed);
+
+          expect(() => writeToProp(['foo', 'child'], parsedJson))
+              .toThrowError('Property path \'foo.child\' does not point to an object.');
+          expect(() => writeToProp(['bar', 'child'], parsedJson))
+              .toThrowError('Property path \'bar.child\' does not point to an object.');
+          expect(() => writeToProp(['baz', 'qux', 'child'], parsedJson))
+              .toThrowError('Property path \'baz.qux.child\' does not point to an object.');
+
+          // It should not throw, if no parsed representation is provided.
+          // (The error will still be thrown on the master process, but that is out of scope for
+          // this test.)
+          expect(() => writeToProp(['foo', 'child'])).not.toThrow();
+        });
+
+        it('should throw, if trying to re-apply an already applied update', () => {
+          const update = updater.createUpdate().addChange(['foo'], 'updated');
+
+          expect(() => update.writeChanges(_('/foo/package.json'))).not.toThrow();
+          expect(() => update.writeChanges(_('/foo/package.json')))
+              .toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
+          expect(() => update.writeChanges(_('/bar/package.json')))
+              .toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
+        });
+      });
+    });
+
+    // Helpers
+    class MockPackageJsonUpdater implements PackageJsonUpdater {
+      createUpdate = jasmine.createSpy('MockPackageJsonUpdater#createUpdate');
+      writeChanges = jasmine.createSpy('MockPackageJsonUpdater#writeChanges');
+    }
+  });
+});
diff --git a/packages/compiler-cli/ngcc/test/execution/cluster/worker_spec.ts b/packages/compiler-cli/ngcc/test/execution/cluster/worker_spec.ts
new file mode 100644
index 0000000000..b8e12d7cf1
--- /dev/null
+++ b/packages/compiler-cli/ngcc/test/execution/cluster/worker_spec.ts
@@ -0,0 +1,144 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/// <reference types="node" />
+
+import * as cluster from 'cluster';
+import {EventEmitter} from 'events';
+
+import {Task, TaskCompletedCallback, TaskProcessingOutcome} from '../../../src/execution/api';
+import {ClusterWorker} from '../../../src/execution/cluster/worker';
+import {mockProperty} from '../../helpers/spy_utils';
+
+
+describe('ClusterWorker', () => {
+  const runAsClusterMaster = mockProperty(cluster, 'isMaster');
+  const mockProcessSend = mockProperty(process, 'send');
+  let processSendSpy: jasmine.Spy;
+  let compileFnSpy: jasmine.Spy;
+  let createCompileFnSpy: jasmine.Spy;
+
+  beforeEach(() => {
+    compileFnSpy = jasmine.createSpy('compileFn');
+    createCompileFnSpy = jasmine.createSpy('createCompileFn').and.returnValue(compileFnSpy);
+
+    processSendSpy = jasmine.createSpy('process.send');
+    mockProcessSend(processSendSpy);
+  });
+
+  describe('constructor()', () => {
+    describe('(on cluster master)', () => {
+      beforeEach(() => runAsClusterMaster(true));
+
+      it('should throw an error', () => {
+        expect(() => new ClusterWorker(createCompileFnSpy))
+            .toThrowError('Tried to instantiate `ClusterWorker` on the master process.');
+        expect(createCompileFnSpy).not.toHaveBeenCalled();
+      });
+    });
+
+    describe('(on cluster worker)', () => {
+      beforeEach(() => runAsClusterMaster(false));
+
+      it('should create the `compileFn()`', () => {
+        new ClusterWorker(createCompileFnSpy);
+        expect(createCompileFnSpy).toHaveBeenCalledWith(jasmine.any(Function));
+      });
+
+      it('should set up `compileFn()` to send a `task-completed` message to master', () => {
+        new ClusterWorker(createCompileFnSpy);
+        const onTaskCompleted: TaskCompletedCallback = createCompileFnSpy.calls.argsFor(0)[0];
+
+        onTaskCompleted(null as any, TaskProcessingOutcome.AlreadyProcessed);
+        expect(processSendSpy).toHaveBeenCalledTimes(1);
+        expect(processSendSpy).toHaveBeenCalledWith({
+          type: 'task-completed',
+          outcome: TaskProcessingOutcome.AlreadyProcessed,
+        });
+
+        onTaskCompleted(null as any, TaskProcessingOutcome.Processed);
+        expect(processSendSpy).toHaveBeenCalledTimes(2);
+        expect(processSendSpy).toHaveBeenCalledWith({
+          type: 'task-completed',
+          outcome: TaskProcessingOutcome.Processed,
+        });
+      });
+    });
+  });
+
+  describe('run()', () => {
+    describe(
+        '(on cluster master)',
+        () => {/* No tests needed, becasue the constructor would have thrown. */});
+
+    describe('(on cluster worker)', () => {
+      // The `cluster.worker` property is normally `undefined` on the master process and set to the
+      // current `cluster.Worker` on worker processes.
+      const mockClusterWorker = mockProperty(cluster, 'worker');
+      let worker: ClusterWorker;
+
+      beforeEach(() => {
+        runAsClusterMaster(false);
+        mockClusterWorker(Object.assign(new EventEmitter(), {id: 42}) as cluster.Worker);
+
+        worker = new ClusterWorker(createCompileFnSpy);
+      });
+
+      it('should return a promise (that is never resolved)', done => {
+        const promise = worker.run();
+
+        expect(promise).toEqual(jasmine.any(Promise));
+
+        promise.then(
+            () => done.fail('Expected promise not to resolve'),
+            () => done.fail('Expected promise not to reject'));
+
+        // We can't wait forever to verify that the promise is not resolved, but at least verify
+        // that it is not resolved immediately.
+        setTimeout(done, 100);
+      });
+
+      it('should handle `process-task` messages', () => {
+        const mockTask = { foo: 'bar' } as unknown as Task;
+
+        worker.run();
+        cluster.worker.emit('message', {type: 'process-task', task: mockTask});
+
+        expect(compileFnSpy).toHaveBeenCalledWith(mockTask);
+        expect(processSendSpy).not.toHaveBeenCalled();
+      });
+
+      it('should send errors during task processing back to the master process', () => {
+        let err: string|Error;
+        compileFnSpy.and.callFake(() => { throw err; });
+
+        worker.run();
+
+        err = 'Error string.';
+        cluster.worker.emit('message', {type: 'process-task', task: {} as Task});
+        expect(processSendSpy).toHaveBeenCalledWith({type: 'error', error: err});
+
+        err = new Error('Error object.');
+        cluster.worker.emit('message', {type: 'process-task', task: {} as Task});
+        expect(processSendSpy).toHaveBeenCalledWith({type: 'error', error: err.stack});
+      });
+
+      it('should throw, when an unknown message type is received', () => {
+        worker.run();
+        cluster.worker.emit('message', {type: 'unknown', foo: 'bar'});
+
+        expect(compileFnSpy).not.toHaveBeenCalled();
+        expect(processSendSpy).toHaveBeenCalledWith({
+          type: 'error',
+          error: jasmine.stringMatching(
+              'Error: Invalid message received on worker #42: {"type":"unknown","foo":"bar"}'),
+        });
+      });
+    });
+  });
+});
diff --git a/packages/compiler-cli/ngcc/test/helpers/spy_utils.ts b/packages/compiler-cli/ngcc/test/helpers/spy_utils.ts
new file mode 100644
index 0000000000..a173839613
--- /dev/null
+++ b/packages/compiler-cli/ngcc/test/helpers/spy_utils.ts
@@ -0,0 +1,112 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/** An object with helpers for mocking/spying on an object's property. */
+export interface IPropertySpyHelpers<T, P extends keyof T> {
+  /**
+   * A `jasmine.Spy` for `get` operations on the property (i.e. reading the current property value).
+   * (This is useful in case one needs to make assertions against property reads.)
+   */
+  getSpy: jasmine.Spy;
+
+  /**
+   * A `jasmine.Spy` for `set` operations on the property (i.e. setting a new property value).
+   * (This is useful in case one needs to make assertions against property writes.)
+   */
+  setSpy: jasmine.Spy;
+
+  /** Install the getter/setter spies for the property. */
+  installSpies(): void;
+
+  /**
+   * Uninstall the property spies and restore the original value (from before installing the
+   * spies), including the property descriptor.
+   */
+  uninstallSpies(): void;
+
+  /** Update the current value of the mocked property. */
+  setMockValue(value: T[P]): void;
+}
+
+/**
+ * Set up mocking an object's property (using spies) and return a function for updating the mocked
+ * property's value during tests.
+ *
+ * This is, essentially, a wrapper around `spyProperty()` which additionally takes care of
+ * installing the spies before each test (via `beforeEach()`) and uninstalling them after each test
+ * (via `afterEach()`).
+ *
+ * Example usage:
+ *
+ * ```ts
+ * describe('something', () => {
+ *   // Assuming `window.foo` is an object...
+ *   const mockWindowFooBar = mockProperty(window.foo, 'bar');
+ *
+ *   it('should do this', () => {
+ *     mockWindowFooBar('baz');
+ *     expect(window.foo.bar).toBe('baz');
+ *
+ *     mockWindowFooBar('qux');
+ *     expect(window.foo.bar).toBe('qux');
+ *   });
+ * });
+ * ```
+ *
+ * @param ctx The object whose property needs to be spied on.
+ * @param prop The name of the property to spy on.
+ *
+ * @return A function for updating the current value of the mocked property.
+ */
+export const mockProperty =
+    <T, P extends keyof T>(ctx: T, prop: P): IPropertySpyHelpers<T, P>['setMockValue'] => {
+      const {setMockValue, installSpies, uninstallSpies} = spyProperty(ctx, prop);
+
+      beforeEach(installSpies);
+      afterEach(uninstallSpies);
+
+      return setMockValue;
+    };
+
+/**
+ * Return utility functions to help mock and spy on an object's property.
+ *
+ * It supports spying on properties that are either defined on the object instance itself or on its
+ * prototype. It also supports spying on non-writable properties (as long as they are configurable).
+ *
+ * NOTE: Unlike `jasmine`'s spying utilities, spies are not automatically installed/uninstalled, so
+ *       the caller is responsible for manually taking care of that (by calling
+ *       `installSpies()`/`uninstallSpies()` as necessary).
+ *
+ * @param ctx The object whose property needs to be spied on.
+ * @param prop The name of the property to spy on.
+ *
+ * @return An object with helpers for mocking/spying on an object's property.
+ */
+export const spyProperty = <T, P extends keyof T>(ctx: T, prop: P): IPropertySpyHelpers<T, P> => {
+  const originalDescriptor = Object.getOwnPropertyDescriptor(ctx, prop);
+
+  let value = ctx[prop];
+  const setMockValue = (mockValue: typeof value) => value = mockValue;
+  const setSpy = jasmine.createSpy(`set ${prop}`).and.callFake(setMockValue);
+  const getSpy = jasmine.createSpy(`get ${prop}`).and.callFake(() => value);
+
+  const installSpies = () => {
+    value = ctx[prop];
+    Object.defineProperty(ctx, prop, {
+      configurable: true,
+      enumerable: originalDescriptor ? originalDescriptor.enumerable : true,
+      get: getSpy,
+      set: setSpy,
+    });
+  };
+  const uninstallSpies = () =>
+      originalDescriptor ? Object.defineProperty(ctx, prop, originalDescriptor) : delete ctx[prop];
+
+  return {installSpies, uninstallSpies, setMockValue, getSpy, setSpy};
+};
diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
index 70f6a79e8c..e95f9d9f3a 100644
--- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
+++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
@@ -5,6 +5,11 @@
  * Use of this source code is governed by an MIT-style license that can be
  * found in the LICENSE file at https://angular.io/license
  */
+
+/// <reference types="node" />
+
+import * as os from 'os';
+
 import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
 import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
 import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
@@ -15,6 +20,7 @@ import {Transformer} from '../../src/packages/transformer';
 import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater';
 import {MockLogger} from '../helpers/mock_logger';
 
+
 const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
 
 runInEachFileSystem(() => {
@@ -28,6 +34,9 @@ runInEachFileSystem(() => {
       fs = getFileSystem();
       pkgJsonUpdater = new DirectPackageJsonUpdater(fs);
       initMockFileSystem(fs, testFiles);
+
+      // Force single-process execution in unit tests by mocking available CPUs to 1.
+      spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'}]);
     });
 
     it('should run ngcc without errors for esm2015', () => {
@@ -402,7 +411,6 @@ runInEachFileSystem(() => {
           propertiesToConsider: ['module', 'fesm5', 'esm5'],
           compileAllFormats: false,
           logger: new MockLogger(),
-
         });
         // * In the Angular packages fesm5 and module have the same underlying format,
         //   so both are marked as compiled.