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"
},