diff --git a/modules/benchmarks/src/hydration/BUILD.bazel b/modules/benchmarks/src/hydration/BUILD.bazel
new file mode 100644
index 00000000000000..7acfeade707890
--- /dev/null
+++ b/modules/benchmarks/src/hydration/BUILD.bazel
@@ -0,0 +1,41 @@
+load("//tools:defaults.bzl", "ng_module", "ts_library")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module(
+ name = "shared_lib",
+ srcs = [
+ "init.ts",
+ "table.ts",
+ "util.ts",
+ ],
+ tsconfig = "//modules/benchmarks:tsconfig-build.json",
+ deps = [
+ "//modules/benchmarks/src:util_lib",
+ "//packages/common",
+ "//packages/core",
+ "//packages/platform-browser",
+ ],
+)
+
+ts_library(
+ name = "perf_tests_lib",
+ testonly = 1,
+ srcs = ["hydration.perf-spec.ts"],
+ tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
+ deps = [
+ "@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
+ "@npm//protractor",
+ ],
+)
+
+ts_library(
+ name = "e2e_tests_lib",
+ testonly = 1,
+ srcs = ["hydration.e2e-spec.ts"],
+ tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
+ deps = [
+ "@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
+ "@npm//protractor",
+ ],
+)
diff --git a/modules/benchmarks/src/hydration/README.md b/modules/benchmarks/src/hydration/README.md
new file mode 100644
index 00000000000000..220ee9edd0eea9
--- /dev/null
+++ b/modules/benchmarks/src/hydration/README.md
@@ -0,0 +1,10 @@
+# Hydration benchmark
+
+This folder contains hydration benchmark that tests the process of matching DOM nodes at runtime.
+
+There are 2 folders in this benchmark:
+
+* `baseline` - renders a component without hydration, we use it as a baseline
+* `main` - the same code as the `baseline`, but Angular uses hydration and matches existing DOM nodes instead of creating new ones
+
+The benchmarks are based on `largetable` benchmarks.
diff --git a/modules/benchmarks/src/hydration/baseline/BUILD.bazel b/modules/benchmarks/src/hydration/baseline/BUILD.bazel
new file mode 100644
index 00000000000000..5aad13a0b81f08
--- /dev/null
+++ b/modules/benchmarks/src/hydration/baseline/BUILD.bazel
@@ -0,0 +1,55 @@
+load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
+load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
+load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
+
+package(default_visibility = ["//modules/benchmarks:__subpackages__"])
+
+ng_module(
+ name = "main",
+ srcs = glob(["*.ts"]),
+ tsconfig = "//modules/benchmarks:tsconfig-build.json",
+ deps = [
+ "//modules/benchmarks/src:util_lib",
+ "//modules/benchmarks/src/hydration:shared_lib",
+ "//packages/core",
+ "//packages/platform-browser",
+ ],
+)
+
+app_bundle(
+ name = "bundle",
+ entry_point = ":index.ts",
+ deps = [
+ ":main",
+ "@npm//rxjs",
+ ],
+)
+
+# The script needs to be called `app_bundle` for easier syncing into g3.
+genrule(
+ name = "app_bundle",
+ srcs = [":bundle.debug.min.js"],
+ outs = ["app_bundle.js"],
+ cmd = "cp $< $@",
+)
+
+http_server(
+ name = "prodserver",
+ srcs = ["index.html"],
+ deps = [
+ ":app_bundle",
+ "//packages/zone.js/bundles:zone.umd.js",
+ ],
+)
+
+benchmark_test(
+ name = "perf",
+ server = ":prodserver",
+ deps = ["//modules/benchmarks/src/hydration:perf_tests_lib"],
+)
+
+e2e_test(
+ name = "e2e",
+ server = ":prodserver",
+ deps = ["//modules/benchmarks/src/hydration:e2e_tests_lib"],
+)
diff --git a/modules/benchmarks/src/hydration/baseline/index.html b/modules/benchmarks/src/hydration/baseline/index.html
new file mode 100644
index 00000000000000..a571ee82071f72
--- /dev/null
+++ b/modules/benchmarks/src/hydration/baseline/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+ Params
+
+
+ Hydration Benchmark (baseline)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/benchmarks/src/hydration/baseline/index.ts b/modules/benchmarks/src/hydration/baseline/index.ts
new file mode 100644
index 00000000000000..5b327b7258af70
--- /dev/null
+++ b/modules/benchmarks/src/hydration/baseline/index.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
+
+import {init, syncUrlParamsToForm} from '../init';
+import {AppComponent} from '../table';
+
+syncUrlParamsToForm();
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideProtractorTestingSupport(),
+ ],
+}).then(appRef => init(appRef, false /* insertSsrContent */));
diff --git a/modules/benchmarks/src/hydration/hydration.e2e-spec.ts b/modules/benchmarks/src/hydration/hydration.e2e-spec.ts
new file mode 100644
index 00000000000000..1b2de63fa02005
--- /dev/null
+++ b/modules/benchmarks/src/hydration/hydration.e2e-spec.ts
@@ -0,0 +1,28 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {openBrowser, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
+import {$} from 'protractor';
+
+describe('hydration benchmark', () => {
+ afterEach(verifyNoBrowserErrors);
+
+ it(`should render the table`, async () => {
+ openBrowser({
+ url: '',
+ ignoreBrowserSynchronization: true,
+ params: [{name: 'cols', value: 5}, {name: 'rows', value: 5}],
+ });
+ await $('#createDom').click();
+ expect($('#table').getText()).toContain('0/0');
+ await $('#createDom').click();
+ expect($('#table').getText()).toContain('A/A');
+ await $('#destroyDom').click();
+ expect($('#table').getText() as any).toEqual('');
+ });
+});
diff --git a/modules/benchmarks/src/hydration/hydration.perf-spec.ts b/modules/benchmarks/src/hydration/hydration.perf-spec.ts
new file mode 100644
index 00000000000000..b9860ecf2f0272
--- /dev/null
+++ b/modules/benchmarks/src/hydration/hydration.perf-spec.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {runBenchmark, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
+import {$} from 'protractor';
+
+interface Worker {
+ id: string;
+ prepare?(): void;
+ work(): void;
+}
+
+const CreateWorker: Worker = {
+ id: 'create',
+ prepare: () => $('#prepare').click(),
+ work: () => $('#createDom').click()
+};
+
+const UpdateWorker: Worker = {
+ id: 'update',
+ prepare: () => {
+ $('#prepare').click();
+ $('#createDom').click();
+ },
+ work: () => $('#updateDom').click()
+};
+
+// In order to make sure that we don't change the ids of the benchmarks, we need to
+// determine the current test package name from the Bazel target. This is necessary
+// because previous to the Bazel conversion, the benchmark test ids contained the test
+// name. e.g. "largeTable.ng2_switch.createDestroy". We determine the name of the
+// Bazel package where this test runs from the current test target. The Bazel target
+// looks like: "//modules/benchmarks/src/largetable/{pkg_name}:{target_name}".
+const testPackageName = process.env['BAZEL_TARGET']!.split(':')[0].split('/').pop();
+
+describe('hydration benchmark perf', () => {
+ afterEach(verifyNoBrowserErrors);
+
+ [CreateWorker, UpdateWorker].forEach((worker) => {
+ describe(worker.id, () => {
+ it(`should run benchmark for ${testPackageName}`, async () => {
+ await runTableBenchmark({
+ id: `hydration.${testPackageName}.${worker.id}`,
+ url: '/',
+ ignoreBrowserSynchronization: true,
+ worker,
+ });
+ });
+ });
+ });
+});
+
+function runTableBenchmark(
+ config: {id: string, url: string, ignoreBrowserSynchronization?: boolean, worker: Worker}) {
+ return runBenchmark({
+ id: config.id,
+ url: config.url,
+ ignoreBrowserSynchronization: config.ignoreBrowserSynchronization,
+ params: [{name: 'cols', value: 40}, {name: 'rows', value: 200}],
+ prepare: config.worker.prepare,
+ work: config.worker.work
+ });
+}
diff --git a/modules/benchmarks/src/hydration/init.ts b/modules/benchmarks/src/hydration/init.ts
new file mode 100644
index 00000000000000..3cada325c01da4
--- /dev/null
+++ b/modules/benchmarks/src/hydration/init.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {ApplicationRef, ComponentRef, createComponent, EnvironmentInjector} from '@angular/core';
+
+import {bindAction, profile} from '../util';
+
+import {TableComponent} from './table';
+import {buildTable, emptyTable, initTableUtils, TableCell} from './util';
+
+const DEFAULT_COLS_COUNT = '40';
+const DEFAULT_ROWS_COUNT = '200';
+
+function getUrlParamValue(name: string): string|null {
+ const url = new URL(document.location.href);
+ return url.searchParams.get(name);
+}
+
+export function syncUrlParamsToForm(): {cols: string, rows: string} {
+ let cols = getUrlParamValue('cols') ?? DEFAULT_COLS_COUNT;
+ let rows = getUrlParamValue('rows') ?? DEFAULT_ROWS_COUNT;
+ (document.getElementById('cols') as HTMLInputElement).value = cols;
+ (document.getElementById('rows') as HTMLInputElement).value = rows;
+ return {cols, rows};
+}
+
+export function init(appRef: ApplicationRef, insertSsrContent = true) {
+ let tableComponentRef: ComponentRef;
+ const injector = appRef.injector;
+ const environmentInjector = injector.get(EnvironmentInjector);
+
+ let data: TableCell[][] = [];
+
+ const setInput = (data: TableCell[][]) => {
+ if (tableComponentRef) {
+ tableComponentRef.setInput('data', data);
+ tableComponentRef.changeDetectorRef.detectChanges();
+ }
+ };
+
+ function destroyDom() {
+ setInput(emptyTable);
+ }
+
+ function updateDom() {
+ data = buildTable();
+ setInput(data);
+ }
+
+ function createDom() {
+ const hostElement = document.getElementById('table');
+ tableComponentRef = createComponent(TableComponent, {environmentInjector, hostElement});
+ setInput(data);
+ }
+
+ function prepare() {
+ destroyDom();
+ data = buildTable();
+
+ if (insertSsrContent) {
+ // Prepare DOM structure, similar to what SSR would produce.
+ const hostElement = document.getElementById('table');
+ hostElement.setAttribute('ngh', '0');
+ hostElement.textContent = ''; // clear existing DOM contents
+ hostElement.appendChild(createTableDom(data));
+ }
+ }
+
+ function noop() {}
+
+ initTableUtils();
+
+ bindAction('#prepare', prepare);
+ bindAction('#createDom', createDom);
+ bindAction('#updateDom', updateDom);
+ bindAction('#createDomProfile', profile(createDom, prepare, 'create'));
+ bindAction('#updateDomProfile', profile(updateDom, noop, 'update'));
+}
+
+/**
+ * Creates DOM to represent a table, similar to what'd be generated
+ * during the SSR.
+ */
+function createTableDom(data: TableCell[][]) {
+ const table = document.createElement('table');
+ const tbody = document.createElement('tbody');
+ table.appendChild(tbody);
+ this._renderCells = [];
+ for (let r = 0; r < data.length; r++) {
+ const dataRow = data[r];
+ const tr = document.createElement('tr');
+ // Mark created DOM nodes, so that we can verify that
+ // they were *not* re-created during hydration.
+ (tr as any).__existing = true;
+ tbody.appendChild(tr);
+ const renderRow = [];
+ for (let c = 0; c < dataRow.length; c++) {
+ const dataCell = dataRow[c];
+ const renderCell = document.createElement('td');
+ // Mark created DOM nodes, so that we can verify that
+ // they were *not* re-created during hydration.
+ (renderCell as any).__existing = true;
+ if (r % 2 === 0) {
+ renderCell.style.backgroundColor = 'grey';
+ }
+ tr.appendChild(renderCell);
+ renderRow[c] = renderCell;
+ renderCell.textContent = dataCell.value;
+ }
+ // View container anchor
+ const comment = document.createComment('');
+ tr.appendChild(comment);
+ }
+ // View container anchor
+ const comment = document.createComment('');
+ tbody.appendChild(comment);
+ return table;
+}
diff --git a/modules/benchmarks/src/hydration/main/BUILD.bazel b/modules/benchmarks/src/hydration/main/BUILD.bazel
new file mode 100644
index 00000000000000..5aad13a0b81f08
--- /dev/null
+++ b/modules/benchmarks/src/hydration/main/BUILD.bazel
@@ -0,0 +1,55 @@
+load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
+load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
+load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
+
+package(default_visibility = ["//modules/benchmarks:__subpackages__"])
+
+ng_module(
+ name = "main",
+ srcs = glob(["*.ts"]),
+ tsconfig = "//modules/benchmarks:tsconfig-build.json",
+ deps = [
+ "//modules/benchmarks/src:util_lib",
+ "//modules/benchmarks/src/hydration:shared_lib",
+ "//packages/core",
+ "//packages/platform-browser",
+ ],
+)
+
+app_bundle(
+ name = "bundle",
+ entry_point = ":index.ts",
+ deps = [
+ ":main",
+ "@npm//rxjs",
+ ],
+)
+
+# The script needs to be called `app_bundle` for easier syncing into g3.
+genrule(
+ name = "app_bundle",
+ srcs = [":bundle.debug.min.js"],
+ outs = ["app_bundle.js"],
+ cmd = "cp $< $@",
+)
+
+http_server(
+ name = "prodserver",
+ srcs = ["index.html"],
+ deps = [
+ ":app_bundle",
+ "//packages/zone.js/bundles:zone.umd.js",
+ ],
+)
+
+benchmark_test(
+ name = "perf",
+ server = ":prodserver",
+ deps = ["//modules/benchmarks/src/hydration:perf_tests_lib"],
+)
+
+e2e_test(
+ name = "e2e",
+ server = ":prodserver",
+ deps = ["//modules/benchmarks/src/hydration:e2e_tests_lib"],
+)
diff --git a/modules/benchmarks/src/hydration/main/index.html b/modules/benchmarks/src/hydration/main/index.html
new file mode 100644
index 00000000000000..f0a23b3db4b844
--- /dev/null
+++ b/modules/benchmarks/src/hydration/main/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+ Params
+
+
+ Hydration Benchmark (main)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/benchmarks/src/hydration/main/index.ts b/modules/benchmarks/src/hydration/main/index.ts
new file mode 100644
index 00000000000000..a74454c14f281d
--- /dev/null
+++ b/modules/benchmarks/src/hydration/main/index.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {bootstrapApplication, provideClientHydration, provideProtractorTestingSupport} from '@angular/platform-browser';
+
+import {init, syncUrlParamsToForm} from '../init';
+import {AppComponent, setupTransferState} from '../table';
+
+const params = syncUrlParamsToForm();
+setupTransferState(params.cols, params.rows);
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideClientHydration(),
+ provideProtractorTestingSupport(),
+ ],
+}).then(appRef => init(appRef, true /* insertSsrContent */));
diff --git a/modules/benchmarks/src/hydration/table.ts b/modules/benchmarks/src/hydration/table.ts
new file mode 100644
index 00000000000000..512bcef399c040
--- /dev/null
+++ b/modules/benchmarks/src/hydration/table.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {CommonModule} from '@angular/common';
+import {Component, Input} from '@angular/core';
+import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
+
+import {TableCell} from './util';
+
+let trustedEmptyColor: SafeStyle;
+let trustedGreyColor: SafeStyle;
+
+@Component({
+ standalone: true,
+ selector: 'app',
+ template: ``,
+})
+export class AppComponent {
+ constructor(sanitizer: DomSanitizer) {
+ trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white');
+ trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
+ }
+}
+
+@Component({
+ standalone: true,
+ selector: 'table-cmp',
+ imports: [CommonModule],
+ template: `
+
+
+
+
+ {{cell.value + '!'}}
+ |
+
+
+
+ `,
+})
+export class TableComponent {
+ @Input() data: TableCell[][] = [];
+
+ trackByIndex(index: number, item: any) {
+ return index;
+ }
+
+ getColor(row: number) {
+ return row % 2 ? trustedEmptyColor : trustedGreyColor;
+ }
+}
+
+export function setupTransferState(cols: string, rows: string) {
+ // TODO create a script with correct data!
+ //
+ const script = document.createElement('script');
+ script.id = 'ng-state';
+ script.type = 'application/json';
+ script.textContent =
+ `{"__ɵnghData__":[{"t":{"2":"t0"},"c":{"2":[{"i":"t0","r":1,"t":{"1":"t1"},"c":{"1":[{"i":"t1","r":1,"x":${
+ cols}}]},"x":${rows}}]}}]}`;
+ document.body.insertBefore(script, document.body.firstChild);
+}
diff --git a/modules/benchmarks/src/hydration/util.ts b/modules/benchmarks/src/hydration/util.ts
new file mode 100644
index 00000000000000..a99c9df64a82c3
--- /dev/null
+++ b/modules/benchmarks/src/hydration/util.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright Google LLC 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 {getIntParameter} from '../util';
+
+export class TableCell {
+ constructor(public row: number, public col: number, public value: string) {}
+}
+
+let tableCreateCount: number;
+let maxRow: number;
+let maxCol: number;
+let numberData: TableCell[][];
+let charData: TableCell[][];
+
+export function initTableUtils() {
+ maxRow = getIntParameter('rows');
+ maxCol = getIntParameter('cols');
+ tableCreateCount = 0;
+ numberData = [];
+ charData = [];
+ for (let r = 0; r < maxRow; r++) {
+ const numberRow: TableCell[] = [];
+ numberData.push(numberRow);
+ const charRow: TableCell[] = [];
+ charData.push(charRow);
+ for (let c = 0; c < maxCol; c++) {
+ numberRow.push(new TableCell(r, c, `${c}/${r}`));
+ charRow.push(new TableCell(r, c, `${charValue(c)}/${charValue(r)}`));
+ }
+ }
+}
+
+function charValue(i: number): string {
+ return String.fromCharCode('A'.charCodeAt(0) + (i % 26));
+}
+
+export const emptyTable: TableCell[][] = [];
+
+export function buildTable(): TableCell[][] {
+ tableCreateCount++;
+ return tableCreateCount % 2 ? numberData : charData;
+}