diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json
index 0e0383fbe6d6..774eb7daeb44 100644
--- a/packages/angular/cli/lib/config/schema.json
+++ b/packages/angular/cli/lib/config/schema.json
@@ -882,6 +882,10 @@
               "type": "boolean",
               "default": false,
               "x-deprecated": true
+            },
+            "webWorkerTsConfig": {
+              "type": "string",
+              "description": "TypeScript configuration for Web Worker modules."
             }
           },
           "additionalProperties": false,
diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json
index 96ebc8101aee..64f787b299eb 100644
--- a/packages/angular_devkit/build_angular/package.json
+++ b/packages/angular_devkit/build_angular/package.json
@@ -51,7 +51,8 @@
     "webpack-dev-server": "3.2.1",
     "webpack-merge": "4.2.1",
     "webpack-sources": "1.3.0",
-    "webpack-subresource-integrity": "1.1.0-rc.6"
+    "webpack-subresource-integrity": "1.1.0-rc.6",
+    "worker-plugin": "3.1.0"
   },
   "devDependencies": {
     "@angular/animations": "^8.0.0-beta.10",
diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts
index 993ae9593239..d32668885405 100644
--- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts
+++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts
@@ -54,6 +54,7 @@ export interface BuildOptions {
   namedChunks?: boolean;
   subresourceIntegrity?: boolean;
   serviceWorker?: boolean;
+  webWorkerTsConfig?: string;
   skipAppShell?: boolean;
   statsJson: boolean;
   forkTypeChecker: boolean;
diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts
index 1ec5c1a271bf..11bbaeaa3118 100644
--- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts
+++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts
@@ -13,3 +13,4 @@ export * from './test';
 export * from './typescript';
 export * from './utils';
 export * from './stats';
+export * from './worker';
diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts
index d9a0b8a06748..89bfe5ed7571 100644
--- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts
+++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts
@@ -108,3 +108,24 @@ export function getAotConfig(wco: WebpackConfigOptions, extract = false) {
     plugins: [_createAotPlugin(wco, { tsConfigPath }, true, extract)]
   };
 }
+
+export function getTypescriptWorkerPlugin(wco: WebpackConfigOptions, workerTsConfigPath: string) {
+  const { buildOptions } = wco;
+
+  const pluginOptions: AngularCompilerPluginOptions = {
+    skipCodeGeneration: true,
+    tsConfigPath: workerTsConfigPath,
+    mainPath: undefined,
+    platform: PLATFORM.Browser,
+    sourceMap: buildOptions.sourceMap.scripts,
+    forkTypeChecker: buildOptions.forkTypeChecker,
+    contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'),
+    logger: wco.logger,
+    // Run no transformers.
+    platformTransformers: [],
+    // Don't attempt lazy route discovery.
+    discoverLazyRoutes: false,
+  };
+
+  return new AngularCompilerPlugin(pluginOptions);
+}
diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/worker.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/worker.ts
new file mode 100644
index 000000000000..1ec0a148ed0d
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/worker.ts
@@ -0,0 +1,30 @@
+/**
+ * @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 { resolve } from 'path';
+import { Configuration } from 'webpack';
+import { WebpackConfigOptions } from '../build-options';
+import { getTypescriptWorkerPlugin } from './typescript';
+
+const WorkerPlugin = require('worker-plugin');
+
+
+export function getWorkerConfig(wco: WebpackConfigOptions): Configuration {
+  const { buildOptions } = wco;
+  if (!buildOptions.webWorkerTsConfig) {
+    throw new Error('The `webWorkerTsConfig` must be a string.');
+  }
+
+  const workerTsConfigPath = resolve(wco.root, buildOptions.webWorkerTsConfig);
+
+  return {
+    plugins: [new WorkerPlugin({
+      globalObject: false,
+      plugins: [getTypescriptWorkerPlugin(wco, workerTsConfigPath)],
+    })],
+  };
+}
diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts
index 182e83e1110d..4dd67931ce82 100644
--- a/packages/angular_devkit/build_angular/src/browser/index.ts
+++ b/packages/angular_devkit/build_angular/src/browser/index.ts
@@ -42,6 +42,7 @@ import {
   getNonAotConfig,
   getStatsConfig,
   getStylesConfig,
+  getWorkerConfig,
 } from '../angular-cli-files/models/webpack-configs';
 import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
 import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module';
@@ -161,6 +162,10 @@ export function buildWebpackConfig(
     webpackConfigs.push(typescriptConfigPartial);
   }
 
+  if (wco.buildOptions.webWorkerTsConfig) {
+    webpackConfigs.push(getWorkerConfig(wco));
+  }
+
   const webpackConfig = webpackMerge(webpackConfigs);
 
   if (options.profile || process.env['NG_BUILD_PROFILING']) {
diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json
index d8aeb05a119e..1dbf30b90b01 100644
--- a/packages/angular_devkit/build_angular/src/browser/schema.json
+++ b/packages/angular_devkit/build_angular/src/browser/schema.json
@@ -320,6 +320,10 @@
       "description": "**EXPERIMENTAL** Transform import statements for lazy routes to import factories when using View Engine. Should only be used when switching back and forth between View Engine and Ivy. See https://angular.io/guide/ivy for usage information.",
       "type": "boolean",
       "default": false
+    },
+    "webWorkerTsConfig": {
+      "type": "string",
+      "description": "TypeScript configuration for Web Worker modules."
     }
   },
   "additionalProperties": false,
diff --git a/packages/angular_devkit/build_angular/test/browser/web-worker_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/web-worker_spec_large.ts
new file mode 100644
index 000000000000..7ea911f2f337
--- /dev/null
+++ b/packages/angular_devkit/build_angular/test/browser/web-worker_spec_large.ts
@@ -0,0 +1,182 @@
+/**
+ * @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 { Architect } from '@angular-devkit/architect/src/index2';
+import { TestLogger } from '@angular-devkit/architect/testing';
+import { join, virtualFs } from '@angular-devkit/core';
+import { debounceTime, takeWhile, tap } from 'rxjs/operators';
+import { browserBuild, createArchitect, host, outputPath } from '../utils';
+
+
+describe('Browser Builder Web Worker support', () => {
+  const target = { project: 'app', target: 'build' };
+  let architect: Architect;
+
+  beforeEach(async () => {
+    await host.initialize().toPromise();
+    architect = (await createArchitect(host.root())).architect;
+  });
+  afterEach(async () => host.restore().toPromise());
+
+  const workerFiles: { [k: string]: string } = {
+    'src/app/dep.ts': `export const foo = 'bar';`,
+    'src/app/app.worker.ts': `
+      import { foo } from './dep';
+      console.log('hello from worker');
+      addEventListener('message', ({ data }) => {
+        console.log('worker got message:', data);
+        if (data === 'hello') {
+          postMessage(foo);
+        }
+      });
+    `,
+    'src/main.ts': `
+      import { enableProdMode } from '@angular/core';
+      import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+      import { AppModule } from './app/app.module';
+      import { environment } from './environments/environment';
+      if (environment.production) { enableProdMode(); }
+      platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err));
+
+      const worker = new Worker('./app/app.worker', { type: 'module' });
+      worker.onmessage = ({ data }) => {
+        console.log('page got message:', data);
+      };
+      worker.postMessage('hello');
+    `,
+    // Make a new tsconfig for the *.worker.ts files.
+    // The final place for this tsconfig must take into consideration editor tooling, unit
+    // tests, and integration with other build targets.
+    './src/tsconfig.worker.json': `
+      {
+        "extends": "../tsconfig.json",
+        "compilerOptions": {
+          "outDir": "../out-tsc/worker",
+          "lib": [
+            "es2018",
+            "webworker"
+          ],
+          "types": []
+        },
+        "include": [
+          "**/*.worker.ts",
+        ]
+      }`,
+    // Alter the app tsconfig to not include *.worker.ts files.
+    './src/tsconfig.app.json': `
+      {
+        "extends": "../tsconfig.json",
+        "compilerOptions": {
+          "outDir": "../out-tsc/worker",
+          "types": []
+        },
+        "exclude": [
+          "test.ts",
+          "**/*.spec.ts",
+          "**/*.worker.ts",
+          "app/dep.ts",
+        ]
+      }`,
+  };
+
+  it('bundles TS worker', async () => {
+    host.writeMultipleFiles(workerFiles);
+    const logger = new TestLogger('worker-warnings');
+    const overrides = { webWorkerTsConfig: 'src/tsconfig.worker.json' };
+    await browserBuild(architect, host, target, overrides, { logger });
+
+    // Worker bundle contains worker code.
+    const workerContent = virtualFs.fileBufferToString(
+      host.scopedSync().read(join(outputPath, '0.worker.js')));
+    expect(workerContent).toContain('hello from worker');
+    expect(workerContent).toContain('bar');
+
+    // Main bundle references worker.
+    const mainContent = virtualFs.fileBufferToString(
+      host.scopedSync().read(join(outputPath, 'main.js')));
+    expect(mainContent).toContain('0.worker.js');
+    expect(logger.includes('WARNING')).toBe(false, 'Should show no warnings.');
+  });
+
+  it('minimizes and hashes worker', async () => {
+    host.writeMultipleFiles(workerFiles);
+    const overrides = {
+      webWorkerTsConfig: 'src/tsconfig.worker.json',
+      outputHashing: 'all',
+      optimization: true,
+    };
+    await browserBuild(architect, host, target, overrides);
+
+    // Worker bundle should have hash and minified code.
+    const workerBundle = host.fileMatchExists(outputPath, /0\.[0-9a-f]{20}\.worker\.js/) as string;
+    expect(workerBundle).toBeTruthy('workerBundle should exist');
+    const workerContent = virtualFs.fileBufferToString(
+      host.scopedSync().read(join(outputPath, workerBundle)));
+    expect(workerContent).toContain('hello from worker');
+    expect(workerContent).toContain('bar');
+    expect(workerContent).toContain('"hello"===t&&postMessage');
+
+    // Main bundle should reference hashed worker bundle.
+    const mainBundle = host.fileMatchExists(outputPath, /main\.[0-9a-f]{20}\.js/) as string;
+    expect(mainBundle).toBeTruthy('mainBundle should exist');
+    const mainContent = virtualFs.fileBufferToString(
+      host.scopedSync().read(join(outputPath, mainBundle)));
+    expect(mainContent).toContain(workerBundle);
+  });
+
+  it('rebuilds TS worker', async () => {
+    host.writeMultipleFiles(workerFiles);
+    const overrides = {
+      webWorkerTsConfig: 'src/tsconfig.worker.json',
+      watch: true,
+    };
+
+    let buildCount = 0;
+    let phase = 1;
+    const workerPath = join(outputPath, '0.worker.js');
+    let workerContent = '';
+
+    const run = await architect.scheduleTarget(target, overrides);
+    await run.output.pipe(
+      // Wait for files to be written to disk.
+      debounceTime(1000),
+      tap((buildEvent) => expect(buildEvent.success).toBe(true, 'build should succeed')),
+      tap(() => {
+        buildCount++;
+        switch (phase) {
+          case 1:
+            // Original worker content should be there.
+            workerContent = virtualFs.fileBufferToString(host.scopedSync().read(workerPath));
+            expect(workerContent).toContain('bar');
+            // Change content of worker dependency.
+            host.writeMultipleFiles({ 'src/app/dep.ts': `export const foo = 'baz';` });
+            phase = 2;
+            break;
+
+          case 2:
+            workerContent = virtualFs.fileBufferToString(host.scopedSync().read(workerPath));
+            // TODO(filipesilva): Should this change? Check with Jason Miller.
+            // The worker changes name with each rebuild. But sometimes it also changes from 0 to
+            // 1 and then back to 0. It's hard to know where the updated content is, but it should
+            // be in one of these two.
+            const anotherWorkerPath = join(outputPath, '1.worker.js');
+            const anotherWorkerContent = virtualFs.fileBufferToString(
+              host.scopedSync().read(anotherWorkerPath));
+            // Worker content should have changed.
+            expect(
+              workerContent.includes('baz') || anotherWorkerContent.includes('baz'),
+            ).toBeTruthy('Worker bundle did not contain updated content.');
+            phase = 3;
+            break;
+        }
+      }),
+      takeWhile(() => phase < 3),
+    ).toPromise();
+    await run.stop();
+  });
+});
diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts
index 6dda7de68b0f..fec4676bbeb6 100644
--- a/packages/ngtools/webpack/src/angular_compiler_plugin.ts
+++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts
@@ -596,7 +596,7 @@ export class AngularCompilerPlugin {
 
   // Registration hook for webpack plugin.
   // tslint:disable-next-line:no-big-function
-  apply(compiler: Compiler & { watchMode?: boolean }) {
+  apply(compiler: Compiler & { watchMode?: boolean, parentCompilation?: compilation.Compilation }) {
     // The below is require by NGCC processor
     // since we need to know which fields we need to process
     compiler.hooks.environment.tap('angular-compiler', () => {
@@ -610,8 +610,14 @@ export class AngularCompilerPlugin {
     // cleanup if not watching
     compiler.hooks.thisCompilation.tap('angular-compiler', compilation => {
       compilation.hooks.finishModules.tap('angular-compiler', () => {
+        let rootCompiler = compiler;
+        while (rootCompiler.parentCompilation) {
+          // tslint:disable-next-line:no-any
+          rootCompiler = compiler.parentCompilation as any;
+        }
+
         // only present for webpack 4.23.0+, assume true otherwise
-        const watchMode = compiler.watchMode === undefined ? true : compiler.watchMode;
+        const watchMode = rootCompiler.watchMode === undefined ? true : rootCompiler.watchMode;
         if (!watchMode) {
           this._program = null;
           this._transformers = [];
@@ -859,6 +865,24 @@ export class AngularCompilerPlugin {
       throw new Error('An @ngtools/webpack plugin already exist for this compilation.');
     }
 
+    // If there is no compiler host at this point, it means that the environment hook did not run.
+    // This happens in child compilations that inherit the parent compilation file system.
+    // Node: child compilations also do not run most webpack compiler hooks, including almost all
+    // we use here. The child compiler will always run as if it was the first build.
+    if (this._compilerHost === undefined) {
+      const inputFs = compilation.compiler.inputFileSystem as VirtualFileSystemDecorator;
+      if (!inputFs.getWebpackCompilerHost) {
+        throw new Error('AngularCompilerPlugin is running in a child compilation, but could' +
+          'not find a WebpackCompilerHost in the parent compilation.');
+      }
+
+      // Use the existing WebpackCompilerHost to ensure builds and rebuilds work.
+      this._compilerHost = createCompilerHost({
+        options: this._compilerOptions,
+        tsHost: inputFs.getWebpackCompilerHost(),
+      }) as CompilerHost & WebpackCompilerHost;
+    }
+
     // Set a private variable for this plugin instance.
     // tslint:disable-next-line:no-any
     (compilation as any)._ngToolsWebpackPluginInstance = this;
diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts
index 853d13378072..3f4aaf3b6d99 100644
--- a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts
+++ b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts
@@ -21,6 +21,10 @@ export class VirtualFileSystemDecorator implements InputFileSystem {
     private _webpackCompilerHost: WebpackCompilerHost,
   ) { }
 
+  getWebpackCompilerHost() {
+    return this._webpackCompilerHost;
+  }
+
   getVirtualFilesPaths() {
     return this._webpackCompilerHost.getNgFactoryPaths();
   }
diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json
index 682667e73f02..9145af5e4840 100644
--- a/packages/schematics/angular/collection.json
+++ b/packages/schematics/angular/collection.json
@@ -100,6 +100,12 @@
       "factory": "./library",
       "schema": "./library/schema.json",
       "description": "Generate a library project for Angular."
+    },
+    "webWorker": {
+      "aliases": ["web-worker", "worker"],
+      "factory": "./web-worker",
+      "schema": "./web-worker/schema.json",
+      "description": "Create a Web Worker ."
     }
   }
 }
diff --git a/packages/schematics/angular/utility/workspace-models.ts b/packages/schematics/angular/utility/workspace-models.ts
index 3d226000317b..9c8eebdcf78f 100644
--- a/packages/schematics/angular/utility/workspace-models.ts
+++ b/packages/schematics/angular/utility/workspace-models.ts
@@ -61,6 +61,7 @@ export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions {
         maximumError?: string;
     }[];
     es5BrowserSupport?: boolean;
+    webWorkerTsConfig?: string;
 }
 
 export interface ServeBuilderOptions {
diff --git a/packages/schematics/angular/web-worker/files/project-tsconfig/tsconfig.json.template b/packages/schematics/angular/web-worker/files/project-tsconfig/tsconfig.json.template
new file mode 100644
index 000000000000..b170f6d1d0e5
--- /dev/null
+++ b/packages/schematics/angular/web-worker/files/project-tsconfig/tsconfig.json.template
@@ -0,0 +1,10 @@
+{
+  "extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json",
+  "compilerOptions": {
+    "lib": [
+      "es2018",
+      "dom",
+      "webworker"
+    ],
+  }
+}
diff --git a/packages/schematics/angular/web-worker/files/worker-tsconfig/tsconfig.worker.json.template b/packages/schematics/angular/web-worker/files/worker-tsconfig/tsconfig.worker.json.template
new file mode 100644
index 000000000000..33b92e255762
--- /dev/null
+++ b/packages/schematics/angular/web-worker/files/worker-tsconfig/tsconfig.worker.json.template
@@ -0,0 +1,14 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/worker",
+    "lib": [
+      "es2018",
+      "webworker"
+    ],
+    "types": []
+  },
+  "include": [
+    "**/*.worker.ts"
+  ]
+}
diff --git a/packages/schematics/angular/web-worker/files/worker/__name@dasherize__.worker.ts.template b/packages/schematics/angular/web-worker/files/worker/__name@dasherize__.worker.ts.template
new file mode 100644
index 000000000000..b88e1b10b6ce
--- /dev/null
+++ b/packages/schematics/angular/web-worker/files/worker/__name@dasherize__.worker.ts.template
@@ -0,0 +1,4 @@
+addEventListener('message', ({ data }) => {
+  const response = `worker response to ${data}`;
+  postMessage(response);
+});
\ No newline at end of file
diff --git a/packages/schematics/angular/web-worker/index.ts b/packages/schematics/angular/web-worker/index.ts
new file mode 100644
index 000000000000..352c8d889de5
--- /dev/null
+++ b/packages/schematics/angular/web-worker/index.ts
@@ -0,0 +1,210 @@
+/**
+ * @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 { JsonParseMode, parseJsonAst, strings, tags } from '@angular-devkit/core';
+import {
+  Rule, SchematicContext, SchematicsException, Tree,
+  apply, applyTemplates, chain, mergeWith, move, noop, url,
+} from '@angular-devkit/schematics';
+import { getWorkspace, updateWorkspace } from '../utility/config';
+import { appendValueInAstArray, findPropertyInAstObject } from '../utility/json-utils';
+import { parseName } from '../utility/parse-name';
+import { buildDefaultPath, getProject } from '../utility/project';
+import { getProjectTargets } from '../utility/project-targets';
+import {
+  BrowserBuilderOptions,
+  BrowserBuilderTarget,
+  WorkspaceSchema,
+} from '../utility/workspace-models';
+import { Schema as WebWorkerOptions } from './schema';
+
+function getProjectConfiguration(
+  workspace: WorkspaceSchema,
+  options: WebWorkerOptions,
+): BrowserBuilderOptions {
+  if (!options.target) {
+    throw new SchematicsException('Option (target) is required.');
+  }
+
+  const projectTargets = getProjectTargets(workspace, options.project);
+  if (!projectTargets[options.target]) {
+    throw new Error(`Target is not defined for this project.`);
+  }
+
+  const target = projectTargets[options.target] as BrowserBuilderTarget;
+
+  return target.options;
+}
+
+function addConfig(options: WebWorkerOptions, root: string): Rule {
+  return (host: Tree, context: SchematicContext) => {
+    context.logger.debug('updating project configuration.');
+    const workspace = getWorkspace(host);
+    const config = getProjectConfiguration(workspace, options);
+
+    if (config.webWorkerTsConfig) {
+      // Don't do anything if the configuration is already there.
+      return;
+    }
+
+    const tsConfigRules = [];
+
+    // Add tsconfig.worker.json.
+    const relativePathToWorkspaceRoot = root.split('/').map(x => '..').join('/');
+    tsConfigRules.push(mergeWith(apply(url('./files/worker-tsconfig'), [
+      applyTemplates({ ...options, relativePathToWorkspaceRoot }),
+      move(root),
+    ])));
+
+    // Add build-angular config flag.
+    config.webWorkerTsConfig = `${root.endsWith('/') ? root : root + '/'}tsconfig.worker.json`;
+
+    // Add project tsconfig.json.
+    // The project level tsconfig.json with webworker lib is for editor support since
+    // the dom and webworker libs are mutually exclusive.
+    // Note: this schematic does not change other tsconfigs to use the project-level tsconfig.
+    const projectTsConfigPath = `${root}/tsconfig.json`;
+    if (host.exists(projectTsConfigPath)) {
+      // If the file already exists, alter it.
+      const buffer = host.read(projectTsConfigPath);
+      if (buffer) {
+        const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
+        if (tsCfgAst.kind != 'object') {
+          throw new SchematicsException('Invalid tsconfig. Was expecting an object');
+        }
+        const optsAstNode = findPropertyInAstObject(tsCfgAst, 'compilerOptions');
+        if (optsAstNode && optsAstNode.kind != 'object') {
+          throw new SchematicsException(
+            'Invalid tsconfig "compilerOptions" property; Was expecting an object.');
+        }
+        const libAstNode = findPropertyInAstObject(tsCfgAst, 'lib');
+        if (libAstNode && libAstNode.kind != 'array') {
+          throw new SchematicsException('Invalid tsconfig "lib" property; expected an array.');
+        }
+        const newLibProp = 'webworker';
+        if (libAstNode && !libAstNode.value.includes(newLibProp)) {
+          const recorder = host.beginUpdate(projectTsConfigPath);
+          appendValueInAstArray(recorder, libAstNode, newLibProp);
+          host.commitUpdate(recorder);
+        }
+      }
+    } else {
+      // Otherwise create it.
+      tsConfigRules.push(mergeWith(apply(url('./files/project-tsconfig'), [
+        applyTemplates({ ...options, relativePathToWorkspaceRoot }),
+        move(root),
+      ])));
+    }
+
+    // Add worker glob exclusion to tsconfig.app.json.
+    const workerGlob = '**/*.worker.ts';
+    const tsConfigPath = config.tsConfig;
+    const buffer = host.read(tsConfigPath);
+    if (buffer) {
+      const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
+      if (tsCfgAst.kind != 'object') {
+        throw new SchematicsException('Invalid tsconfig. Was expecting an object');
+      }
+      const filesAstNode = findPropertyInAstObject(tsCfgAst, 'exclude');
+      if (filesAstNode && filesAstNode.kind != 'array') {
+        throw new SchematicsException('Invalid tsconfig "exclude" property; expected an array.');
+      }
+
+      if (filesAstNode && filesAstNode.value.indexOf(workerGlob) == -1) {
+        const recorder = host.beginUpdate(tsConfigPath);
+        appendValueInAstArray(recorder, filesAstNode, workerGlob);
+        host.commitUpdate(recorder);
+      }
+    }
+
+    return chain([
+      // Add tsconfigs.
+      ...tsConfigRules,
+      // Add workspace configuration.
+      updateWorkspace(workspace),
+    ]);
+  };
+}
+
+function addSnippet(options: WebWorkerOptions): Rule {
+  return (host: Tree, context: SchematicContext) => {
+    context.logger.debug('Updating appmodule');
+
+    if (options.path === undefined) {
+      return;
+    }
+
+    const siblingModules = host.getDir(options.path).subfiles
+      // Find all files that start with the same name, are ts files, and aren't spec files.
+      .filter(f => f.startsWith(options.name) && f.endsWith('.ts') && !f.endsWith('spec.ts'))
+      // Sort alphabetically for consistency.
+      .sort();
+
+    if (siblingModules.length === 0) {
+      // No module to add in.
+      return;
+    }
+
+    const siblingModulePath = `${options.path}/${siblingModules[0]}`;
+    const workerCreationSnippet = tags.stripIndent`
+      if (typeof Worker !== 'undefined') {
+        // Create a new
+        const worker = new Worker('./${options.name}.worker', { type: 'module' });
+        worker.onmessage = ({ data }) => {
+          console.log('page got message: $\{data\}');
+        };
+        worker.postMessage('hello');
+      } else {
+        // Web Workers are not supported in this environment.
+        // You should add a fallback so that your program still executes correctly.
+      }
+    `;
+
+    // Append the worker creation snippet.
+    const originalContent = host.read(siblingModulePath);
+    host.overwrite(siblingModulePath, originalContent + '\n' + workerCreationSnippet);
+
+    return host;
+  };
+}
+
+export default function (options: WebWorkerOptions): Rule {
+  return (host: Tree, context: SchematicContext) => {
+    const project = getProject(host, options.project);
+    if (!options.project) {
+      throw new SchematicsException('Option "project" is required.');
+    }
+    if (!project) {
+      throw new SchematicsException(`Invalid project name (${options.project})`);
+    }
+    if (project.projectType !== 'application') {
+      throw new SchematicsException(`Web Worker requires a project type of "application".`);
+    }
+
+    if (options.path === undefined) {
+      options.path = buildDefaultPath(project);
+    }
+    const parsedPath = parseName(options.path, options.name);
+    options.name = parsedPath.name;
+    options.path = parsedPath.path;
+    const root = project.root || project.sourceRoot || '';
+
+    const templateSource = apply(url('./files/worker'), [
+      applyTemplates({ ...options, ...strings }),
+      move(parsedPath.path),
+    ]);
+
+    return chain([
+      // Add project configuration.
+      addConfig(options, root),
+      // Create the worker in a sibling module.
+      options.snippet ? addSnippet(options) : noop(),
+      // Add the worker.
+      mergeWith(templateSource),
+    ]);
+  };
+}
diff --git a/packages/schematics/angular/web-worker/index_spec.ts b/packages/schematics/angular/web-worker/index_spec.ts
new file mode 100644
index 000000000000..940d890d2f97
--- /dev/null
+++ b/packages/schematics/angular/web-worker/index_spec.ts
@@ -0,0 +1,85 @@
+/**
+ * @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
+import { Schema as ApplicationOptions } from '../application/schema';
+import { Schema as WorkspaceOptions } from '../workspace/schema';
+import { Schema as WebWorkerOptions } from './schema';
+
+
+describe('Service Worker Schematic', () => {
+  const schematicRunner = new SchematicTestRunner(
+    '@schematics/angular',
+    require.resolve('../collection.json'),
+  );
+  const defaultOptions: WebWorkerOptions = {
+    project: 'bar',
+    target: 'build',
+    name: 'app',
+    // path: 'src/app',
+    snippet: true,
+  };
+
+  let appTree: UnitTestTree;
+
+  const workspaceOptions: WorkspaceOptions = {
+    name: 'workspace',
+    newProjectRoot: 'projects',
+    version: '8.0.0',
+  };
+
+  const appOptions: ApplicationOptions = {
+    name: 'bar',
+    inlineStyle: false,
+    inlineTemplate: false,
+    routing: false,
+    skipTests: false,
+    skipPackageJson: false,
+  };
+
+  beforeEach(() => {
+    appTree = schematicRunner.runSchematic('workspace', workspaceOptions);
+    appTree = schematicRunner.runSchematic('application', appOptions, appTree);
+  });
+
+  it('should put the worker file in the project root', () => {
+    const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
+    const path = '/projects/bar/src/app/app.worker.ts';
+    expect(tree.exists(path)).toEqual(true);
+  });
+
+  it('should put a new tsconfig.json file in the project root', () => {
+    const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
+    const path = '/projects/bar/tsconfig.json';
+    expect(tree.exists(path)).toEqual(true);
+  });
+
+  it('should put the tsconfig.worker.json file in the project root', () => {
+    const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
+    const path = '/projects/bar/tsconfig.worker.json';
+    expect(tree.exists(path)).toEqual(true);
+  });
+
+  it('should add the webWorkerTsConfig option to workspace', () => {
+    const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
+    const { projects } = JSON.parse(tree.readContent('/angular.json'));
+    expect(projects.bar.architect.build.options.webWorkerTsConfig)
+      .toBe('projects/bar/tsconfig.worker.json');
+  });
+
+  it('should add exclusions to tsconfig.app.json', () => {
+    const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
+    const { exclude } = JSON.parse(tree.readContent('/projects/bar/tsconfig.app.json'));
+    expect(exclude).toContain('**/*.worker.ts');
+  });
+
+  it('should add snippet to sibling file', () => {
+    const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
+    const appComponent = tree.readContent('/projects/bar/src/app/app.component.ts');
+    expect(appComponent).toContain(`new Worker('./${defaultOptions.name}.worker`);
+  });
+});
diff --git a/packages/schematics/angular/web-worker/schema.json b/packages/schematics/angular/web-worker/schema.json
new file mode 100644
index 000000000000..43ac37caa5f7
--- /dev/null
+++ b/packages/schematics/angular/web-worker/schema.json
@@ -0,0 +1,45 @@
+{
+  "$schema": "http://json-schema.org/schema",
+  "id": "SchematicsAngularWebWorker",
+  "title": "Angular Web Worker Options Schema",
+  "type": "object",
+  "description": "Pass this schematic to the \"run\" command to create a Web Worker",
+  "properties": {
+    "path": {
+      "type": "string",
+      "format": "path",
+      "description": "The path at which to create the worker file, relative to the current workspace.",
+      "visible": false
+    },    
+    "project": {
+      "type": "string",
+      "description": "The name of the project.",
+      "$default": {
+        "$source": "projectName"
+      }
+    },
+    "target": {
+      "type": "string",
+      "description": "The target to apply service worker to.",
+      "default": "build"
+    },
+    "name": {
+      "type": "string",
+      "description": "The name of the worker.",
+      "$default": {
+        "$source": "argv",
+        "index": 0
+      },
+      "x-prompt": "What name would you like to use for the worker?"
+    },
+    "snippet": {
+      "type": "boolean",
+      "default": true,
+      "description": "Add a worker creation snippet in a sibling file of the same name."
+    }    
+  },
+  "required": [
+    "name",
+    "project"
+  ]
+}
diff --git a/tests/legacy-cli/e2e/tests/build/worker.ts b/tests/legacy-cli/e2e/tests/build/worker.ts
new file mode 100644
index 000000000000..7c9398687af6
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/build/worker.ts
@@ -0,0 +1,45 @@
+/**
+ * @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 { join } from 'path';
+import { expectFileToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs';
+import { ng } from '../../utils/process';
+
+
+export default async function () {
+
+  const workerPath = join('src', 'app', 'app.worker.ts');
+  const snippetPath = join('src', 'app', 'app.component.ts');
+  const projectTsConfig = join('src', 'tsconfig.json');
+  const workerTsConfig = join('src', 'tsconfig.worker.json');
+
+  await ng('generate', 'web-worker', 'app');
+  await expectFileToExist(workerPath);
+  await expectFileToExist(projectTsConfig);
+  await expectFileToExist(workerTsConfig);
+  await expectFileToMatch(snippetPath, `new Worker('./app.worker', { type: 'module' })`);
+
+  // console.warn has to be used because chrome only captures warnings and errors by default
+  // https://github.com/angular/protractor/issues/2207
+  await replaceInFile('src/app/app.component.ts', 'console.log', 'console.warn');
+
+  await writeFile('e2e/app.e2e-spec.ts', `
+    import { AppPage } from './app.po';
+    import { browser, logging } from 'protractor';
+    describe('worker bundle', () => {
+      it('should log worker messages', async () => {
+        const page = new AppPage();;
+        page.navigateTo();
+        const logs = await browser.manage().logs().get(logging.Type.BROWSER);
+        expect(logs.length).toEqual(1);
+        expect(logs[0].message).toContain('page got message: worker response to hello');
+      });
+    });
+  `);
+
+  await ng('e2e');
+}
diff --git a/yarn.lock b/yarn.lock
index a188e29a6b5b..0bf1108371b4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9890,6 +9890,13 @@ worker-farm@^1.5.2:
   dependencies:
     errno "~0.1.7"
 
+worker-plugin@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-3.1.0.tgz#6311778f3514a87c273510ee3f809cc3fe161e6f"
+  integrity sha512-iQ9KTTmmN5fhfc2KMR7CcDblvcrg1QQ4pXymqZ3cRZF8L0890YLBcEqlIsGPdxoFwghyN8RA1pCEhCKuTF4Lkw==
+  dependencies:
+    loader-utils "^1.1.0"
+
 wrap-ansi@^2.0.0:
   version "2.1.0"
   resolved "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"