diff --git a/packages/core/src/hydration/api.ts b/packages/core/src/hydration/api.ts index 6e0d051199a..602748927b9 100644 --- a/packages/core/src/hydration/api.ts +++ b/packages/core/src/hydration/api.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {APP_BOOTSTRAP_LISTENER, ApplicationRef} from '../application_ref'; import {PLATFORM_ID} from '../application_tokens'; import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, makeEnvironmentProviders} from '../di'; import {inject} from '../di/injector_compatibility'; @@ -16,6 +17,7 @@ import {enableApplyRootElementTransformImpl} from '../render3/instructions/share import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template'; import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text'; +import {cleanupDehydratedViews} from './cleanup'; import {IS_HYDRATION_FEATURE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens'; import {enableRetrieveHydrationInfoImpl} from './utils'; import {enableFindMatchingDehydratedViewImpl} from './views'; @@ -129,6 +131,17 @@ export function provideHydrationSupport(): EnvironmentProviders { // environment. On a server, an application is rendered // from scratch, so the host content needs to be empty. useFactory: () => isBrowser(), + }, + { + provide: APP_BOOTSTRAP_LISTENER, + useFactory: () => { + if (isBrowser()) { + const appRef = inject(ApplicationRef); + return () => cleanupDehydratedViews(appRef); + } + return () => {}; // noop + }, + multi: true, } ]); } diff --git a/packages/core/src/hydration/cleanup.ts b/packages/core/src/hydration/cleanup.ts index 59739aa5faa..8b8e4ea4587 100644 --- a/packages/core/src/hydration/cleanup.ts +++ b/packages/core/src/hydration/cleanup.ts @@ -6,15 +6,20 @@ * found in the LICENSE file at https://angular.io/license */ -import {DEHYDRATED_VIEWS, LContainer} from '../render3/interfaces/container'; +import {first} from 'rxjs/operators'; + +import {ApplicationRef} from '../application_ref'; +import {CONTAINER_HEADER_OFFSET, DEHYDRATED_VIEWS, LContainer} from '../render3/interfaces/container'; import {Renderer} from '../render3/interfaces/renderer'; import {RNode} from '../render3/interfaces/renderer_dom'; -import {PARENT, RENDERER} from '../render3/interfaces/view'; +import {isLContainer} from '../render3/interfaces/type_checks'; +import {HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TVIEW} from '../render3/interfaces/view'; import {nativeRemoveNode} from '../render3/node_manipulation'; import {EMPTY_ARRAY} from '../util/empty'; import {validateSiblingNodeExists} from './error_handling'; import {DehydratedContainerView, NUM_ROOT_NODES} from './interfaces'; +import {getComponentLViewForHydration} from './utils'; /** * Removes all dehydrated views from a given LContainer: @@ -53,3 +58,53 @@ function removeDehydratedView(dehydratedView: DehydratedContainerView, renderer: } } } + +/** + * Walks over all views within this LContainer invokes dehydrated views + * cleanup function for each one. + */ +function cleanupLContainer(lContainer: LContainer) { + removeDehydratedViews(lContainer); + for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) { + cleanupLView(lContainer[i] as LView); + } +} + +/** + * Walks over `LContainer`s and components registered within + * this LView and invokes dehydrated views cleanup function for each one. + */ +function cleanupLView(lView: LView) { + const tView = lView[TVIEW]; + for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) { + if (isLContainer(lView[i])) { + const lContainer = lView[i]; + cleanupLContainer(lContainer); + } else if (Array.isArray(lView[i])) { + // This is a component, enter the `cleanupLView` recursively. + cleanupLView(lView[i]); + } + } +} + +/** + * Walks over all views registered within the ApplicationRef and removes + * all dehydrated views from all `LContainer`s along the way. + */ +export function cleanupDehydratedViews(appRef: ApplicationRef) { + // Wait once an app becomes stable and cleanup all views that + // were not claimed during the application bootstrap process. + // The timing is similar to when we kick off serialization on the server. + return appRef.isStable.pipe(first((isStable: boolean) => isStable)).toPromise().then(() => { + const viewRefs = appRef._views; + for (const viewRef of viewRefs) { + const lView = getComponentLViewForHydration(viewRef); + // An `lView` might be `null` if a `ViewRef` represents + // an embedded view (not a component view). + if (lView !== null && lView[HOST] !== null) { + cleanupLView(lView); + ngDevMode && ngDevMode.dehydratedViewsCleanupRuns++; + } + } + }); +} diff --git a/packages/core/src/util/ng_dev_mode.ts b/packages/core/src/util/ng_dev_mode.ts index faf58e6ff45..fea7327022f 100644 --- a/packages/core/src/util/ng_dev_mode.ts +++ b/packages/core/src/util/ng_dev_mode.ts @@ -51,6 +51,7 @@ declare global { hydratedNodes: number; hydratedComponents: number; dehydratedViewsRemoved: number; + dehydratedViewsCleanupRuns: number; } } @@ -83,6 +84,7 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters { hydratedNodes: 0, hydratedComponents: 0, dehydratedViewsRemoved: 0, + dehydratedViewsCleanupRuns: 0, }; // Make sure to refer to ngDevMode as ['ngDevMode'] for closure. diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index b9624eb34a8..a30d82401ac 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -10,6 +10,7 @@ import {CommonModule, DOCUMENT, isPlatformServer, NgComponentOutlet, NgFor, NgIf import {APP_ID, ApplicationRef, Component, ComponentRef, createComponent, destroyPlatform, ElementRef, EnvironmentInjector, getPlatform, inject, Input, PLATFORM_ID, Provider, TemplateRef, Type, ViewChild, ViewContainerRef, ɵgetComponentDef as getComponentDef, ɵprovideHydrationSupport as provideHydrationSupport, ɵsetDocument} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {bootstrapApplication} from '@angular/platform-browser'; +import {first} from 'rxjs/operators'; import {renderApplication} from '../src/utils'; @@ -79,6 +80,15 @@ function stripTransferDataScript(input: string): string { return input.replace(/