diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 96d1bae7af3..d22d102a4d6 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -70,7 +70,7 @@ "defer": { "uncompressed": { "runtime": 2689, - "main": 124117, + "main": 121811, "polyfills": 33807, "src_app_defer_component_ts": 450 } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/TEST_CASES.json index eda79c9cc20..3cbed511bcc 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/TEST_CASES.json @@ -50,7 +50,8 @@ ], "failureMessage": "Incorrect template" } - ] + ], + "skipForTemplatePipeline": true }, { "description": "should generate a deferred block with loading block parameters", @@ -67,7 +68,8 @@ ], "failureMessage": "Incorrect template" } - ] + ], + "skipForTemplatePipeline": true }, { "description": "should generate a deferred block with local dependencies", diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_loading_params_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_loading_params_template.js index 30d47fc8109..e6b7c1be589 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_loading_params_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_loading_params_template.js @@ -16,7 +16,7 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ template: function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtemplate(0, MyApp_Defer_0_Template, 1, 0)(1, MyApp_DeferLoading_1_Template, 1, 0); - $r3$.ɵɵdefer(2, 0, null, 1, null, null, 0); + $r3$.ɵɵdefer(2, 0, null, 1, null, null, 0, null, $r3$.ɵɵdeferEnableTimerScheduling); $r3$.ɵɵdeferOnIdle(); } }, diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_placeholder_params_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_placeholder_params_template.js index a49c72a6865..a62cb24b371 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_placeholder_params_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_with_placeholder_params_template.js @@ -16,7 +16,7 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ template: function MyApp_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtemplate(0, MyApp_Defer_0_Template, 1, 0)(1, MyApp_DeferPlaceholder_1_Template, 1, 0); - $r3$.ɵɵdefer(2, 0, null, null, 1, null, null, 0); + $r3$.ɵɵdefer(2, 0, null, null, 1, null, null, 0, $r3$.ɵɵdeferEnableTimerScheduling); $r3$.ɵɵdeferOnIdle(); } }, diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index b3e8efe930f..595fea60b93 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -8854,6 +8854,47 @@ function allTests(os: string) { expect(jsContents).not.toContain('import { CmpA }'); }); + it('should include timer scheduler function when ' + + '`after` or `minimum` parameters are used', + () => { + env.write('cmp-a.ts', ` + import { Component } from '@angular/core'; + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + `); + + env.write('/test.ts', ` + import { Component } from '@angular/core'; + import { CmpA } from './cmp-a'; + + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA], + template: \` + @defer { + + } @loading (after 500ms; minimum 300ms) { + Loading... + } + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toContain( + 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)'); + }); + describe('imports', () => { it('should retain regular imports when symbol is eagerly referenced', () => { env.write('cmp-a.ts', ` diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 1917cb6af48..e985a1d8a02 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -160,6 +160,8 @@ export class Identifiers { o.ExternalReference = {name: 'ɵɵdeferPrefetchOnInteraction', moduleName: CORE}; static deferPrefetchOnViewport: o.ExternalReference = {name: 'ɵɵdeferPrefetchOnViewport', moduleName: CORE}; + static deferEnableTimerScheduling: + o.ExternalReference = {name: 'ɵɵdeferEnableTimerScheduling', moduleName: CORE}; static conditional: o.ExternalReference = {name: 'ɵɵconditional', moduleName: CORE}; static repeater: o.ExternalReference = {name: 'ɵɵrepeater', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index e090863094b..831c56b053f 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -1323,6 +1323,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver o.literal(errorIndex), loadingConsts?.length ? this.addToConsts(o.literalArr(loadingConsts)) : o.TYPED_NULL_EXPR, placeholderConsts ? this.addToConsts(placeholderConsts) : o.TYPED_NULL_EXPR, + (loadingConsts?.length || placeholderConsts) ? + o.importExpr(R3.deferEnableTimerScheduling) : + o.TYPED_NULL_EXPR, ])); this.createDeferTriggerInstructions(deferredIndex, triggers, metadata, false); diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 332ed8c21af..151b85702b6 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -225,6 +225,7 @@ export { ɵɵdeferPrefetchOnHover, ɵɵdeferPrefetchOnInteraction, ɵɵdeferPrefetchOnViewport, + ɵɵdeferEnableTimerScheduling, ɵɵtext, ɵɵtextInterpolate, ɵɵtextInterpolate1, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 662972f1fdb..a36dcb2612c 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -143,6 +143,7 @@ export { ɵɵdeferPrefetchOnHover, ɵɵdeferPrefetchOnInteraction, ɵɵdeferPrefetchOnViewport, + ɵɵdeferEnableTimerScheduling, ɵɵtext, ɵɵtextInterpolate, diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts index 7ab0da68410..e7750e4c857 100644 --- a/packages/core/src/render3/instructions/defer.ts +++ b/packages/core/src/render3/instructions/defer.ts @@ -45,6 +45,38 @@ function shouldTriggerDeferBlock(injector: Injector): boolean { return isPlatformBrowser(injector); } +/** + * Reference to the timer-based scheduler implementation of defer block state + * rendering method. It's used to make timer-based scheduling tree-shakable. + * If `minimum` or `after` parameters are used, compiler generates an extra + * argument for the `ɵɵdefer` instruction, which references a timer-based + * implementation. + */ +let applyDeferBlockStateWithSchedulingImpl: (typeof applyDeferBlockState)|null = null; + +/** + * Enables timer-related scheduling if `after` or `minimum` parameters are setup + * on the `@loading` or `@placeholder` blocks. + */ +export function ɵɵdeferEnableTimerScheduling( + tView: TView, tDetails: TDeferBlockDetails, placeholderConfigIndex?: number|null, + loadingConfigIndex?: number|null) { + const tViewConsts = tView.consts; + if (placeholderConfigIndex != null) { + tDetails.placeholderBlockConfig = + getConstant(tViewConsts, placeholderConfigIndex); + } + if (loadingConfigIndex != null) { + tDetails.loadingBlockConfig = + getConstant(tViewConsts, loadingConfigIndex); + } + + // Enable implementation that supports timer-based scheduling. + if (applyDeferBlockStateWithSchedulingImpl === null) { + applyDeferBlockStateWithSchedulingImpl = applyDeferBlockStateWithScheduling; + } +} + /** * Creates runtime data structures for defer blocks. * @@ -58,6 +90,8 @@ function shouldTriggerDeferBlock(injector: Injector): boolean { * block. * @param placeholderConfigIndex Index in the constants array of the configuration of the * placeholder block. + * @param enableTimerScheduling Function that enables timer-related scheduling if `after` + * or `minimum` parameters are setup on the `@loading` or `@placeholder` blocks. * * @codeGenApi */ @@ -65,32 +99,28 @@ export function ɵɵdefer( index: number, primaryTmplIndex: number, dependencyResolverFn?: DependencyResolverFn|null, loadingTmplIndex?: number|null, placeholderTmplIndex?: number|null, errorTmplIndex?: number|null, loadingConfigIndex?: number|null, - placeholderConfigIndex?: number|null) { + placeholderConfigIndex?: number|null, + enableTimerScheduling?: typeof ɵɵdeferEnableTimerScheduling) { const lView = getLView(); const tView = getTView(); - const tViewConsts = tView.consts; const adjustedIndex = index + HEADER_OFFSET; ɵɵtemplate(index, null, 0, 0); if (tView.firstCreatePass) { - const deferBlockConfig: TDeferBlockDetails = { + const tDetails: TDeferBlockDetails = { primaryTmplIndex, loadingTmplIndex: loadingTmplIndex ?? null, placeholderTmplIndex: placeholderTmplIndex ?? null, errorTmplIndex: errorTmplIndex ?? null, - placeholderBlockConfig: placeholderConfigIndex != null ? - getConstant(tViewConsts, placeholderConfigIndex) : - null, - loadingBlockConfig: loadingConfigIndex != null ? - getConstant(tViewConsts, loadingConfigIndex) : - null, + placeholderBlockConfig: null, + loadingBlockConfig: null, dependencyResolverFn: dependencyResolverFn ?? null, loadingState: DeferDependenciesLoadingState.NOT_STARTED, loadingPromise: null, }; - - setTDeferBlockDetails(tView, adjustedIndex, deferBlockConfig); + enableTimerScheduling?.(tView, tDetails, placeholderConfigIndex, loadingConfigIndex); + setTDeferBlockDetails(tView, adjustedIndex, tDetails); } const tNode = getCurrentTNode()!; @@ -657,16 +687,68 @@ export function renderDeferBlockState( ngDevMode && assertTNodeForLView(tNode, hostLView); const lDetails = getLDeferBlockDetails(hostLView, tNode); - const tDetails = getTDeferBlockDetails(hostTView, tNode); ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined'); - const now = Date.now(); const currentState = lDetails[DEFER_BLOCK_STATE]; - if (!isValidStateChange(currentState, newState) || - !isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState)) - return; + if (isValidStateChange(currentState, newState) && + isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState)) { + const tDetails = getTDeferBlockDetails(hostTView, tNode); + const needsScheduling = getLoadingBlockAfter(tDetails) !== null || + getMinimumDurationForState(tDetails, DeferBlockState.Loading) !== null || + getMinimumDurationForState(tDetails, DeferBlockState.Placeholder); + + if (ngDevMode && needsScheduling) { + assertDefined( + applyDeferBlockStateWithSchedulingImpl, 'Expected scheduling function to be defined'); + } + + const applyStateFn = + needsScheduling ? applyDeferBlockStateWithSchedulingImpl! : applyDeferBlockState; + applyStateFn(newState, lDetails, lContainer, tNode, hostLView); + } +} + +/** + * Applies changes to the DOM to reflect a given state. + */ +function applyDeferBlockState( + newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode, + hostLView: LView) { + const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode); + + if (stateTmplIndex !== null) { + lDetails[DEFER_BLOCK_STATE] = newState; + const hostTView = hostLView[TVIEW]; + const adjustedIndex = stateTmplIndex + HEADER_OFFSET; + const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode; + + // There is only 1 view that can be present in an LContainer that + // represents a defer block, so always refer to the first one. + const viewIndex = 0; + + removeLViewFromLContainer(lContainer, viewIndex); + const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId); + const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView}); + addLViewToLContainer( + lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView)); + markViewDirty(embeddedLView); + } +} + +/** + * Extends the `applyDeferBlockState` with timer-based scheduling. + * This function becomes available on a page if there are defer blocks + * that use `after` or `minimum` parameters in the `@loading` or + * `@placeholder` blocks. + */ +function applyDeferBlockStateWithScheduling( + newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode, + hostLView: LView) { + const now = Date.now(); + const hostTView = hostLView[TVIEW]; + const tDetails = getTDeferBlockDetails(hostTView, tNode); if (lDetails[STATE_IS_FROZEN_UNTIL] === null || lDetails[STATE_IS_FROZEN_UNTIL] <= now) { lDetails[STATE_IS_FROZEN_UNTIL] = null; @@ -690,7 +772,7 @@ export function renderDeferBlockState( lDetails[NEXT_DEFER_BLOCK_STATE] = null; } - applyDeferBlockStateToDom(newState, lDetails, lContainer, hostLView, tNode); + applyDeferBlockState(newState, lDetails, lContainer, tNode, hostLView); const duration = getMinimumDurationForState(tDetails, newState); if (duration !== null) { @@ -720,8 +802,6 @@ function scheduleDeferBlockUpdate( renderDeferBlockState(nextState, tNode, lContainer); } }; - // TODO: this needs refactoring to make `TimerScheduler` that is used inside - // of the `scheduleTimerTrigger` function tree-shakable. return scheduleTimerTrigger(timeout, callback, hostLView, true); } @@ -739,33 +819,6 @@ function isValidStateChange( return currentState < newState; } -/** - * Applies changes to the DOM to reflect a given state. - */ -function applyDeferBlockStateToDom( - newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, - hostLView: LView, tNode: TNode) { - const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode); - - if (stateTmplIndex !== null) { - lDetails[DEFER_BLOCK_STATE] = newState; - const hostTView = hostLView[TVIEW]; - const adjustedIndex = stateTmplIndex + HEADER_OFFSET; - const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode; - - // There is only 1 view that can be present in an LContainer that - // represents a defer block, so always refer to the first one. - const viewIndex = 0; - - removeLViewFromLContainer(lContainer, viewIndex); - const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId); - const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView}); - addLViewToLContainer( - lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView)); - markViewDirty(embeddedLView); - } -} - /** * Trigger prefetching of dependencies for a defer block. * diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index aadd936c34d..a13b45e681f 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -154,6 +154,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵdeferPrefetchOnHover': r3.ɵɵdeferPrefetchOnHover, 'ɵɵdeferPrefetchOnInteraction': r3.ɵɵdeferPrefetchOnInteraction, 'ɵɵdeferPrefetchOnViewport': r3.ɵɵdeferPrefetchOnViewport, + 'ɵɵdeferEnableTimerScheduling': r3.ɵɵdeferEnableTimerScheduling, 'ɵɵrepeater': r3.ɵɵrepeater, 'ɵɵrepeaterCreate': r3.ɵɵrepeaterCreate, 'ɵɵrepeaterTrackByIndex': r3.ɵɵrepeaterTrackByIndex, diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index 7850d6507cb..8f0f0330c36 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -233,9 +233,6 @@ { "name": "KeyEventsPlugin" }, - { - "name": "LOADING_AFTER_CLEANUP_FN" - }, { "name": "LOADING_AFTER_SLOT" }, @@ -428,9 +425,6 @@ { "name": "SOURCE" }, - { - "name": "STATE_IS_FROZEN_UNTIL" - }, { "name": "SVG_NAMESPACE" }, @@ -491,9 +485,6 @@ { "name": "TYPE" }, - { - "name": "TimerScheduler" - }, { "name": "USE_VALUE" }, @@ -611,6 +602,12 @@ { "name": "appendChild" }, + { + "name": "applyDeferBlockState" + }, + { + "name": "applyDeferBlockStateWithSchedulingImpl" + }, { "name": "applyNodes" }, @@ -623,9 +620,6 @@ { "name": "applyView" }, - { - "name": "arraySplice" - }, { "name": "assertConsumerNode" }, @@ -890,6 +884,9 @@ { "name": "getLViewParent" }, + { + "name": "getMinimumDurationForState" + }, { "name": "getNativeByTNode" }, @@ -2258,9 +2255,6 @@ { "name": "scheduleArray" }, - { - "name": "scheduleDeferBlockUpdate" - }, { "name": "searchTokensOnInjector" }, @@ -2321,9 +2315,6 @@ { "name": "shouldSearchParent" }, - { - "name": "storeLViewOnDestroy" - }, { "name": "stringify" },