refactor(core): make timer-related @defer logic tree-shakable (#52042)

This commit updates `@defer` logic related to handling `after` and `minimum` parameters tree-shakable.

If `after` or `minimum` was used on a `@loading` or `@placeholder` blocks, compiler generates an extra argument for the `ɵɵdefer` instruction. This extra argument is a reference to a function that brings timer-related code.

PR Close #52042
This commit is contained in:
Andrew Kushnir 2023-10-04 22:42:31 -07:00 committed by Andrew Scott
parent 68ba798ae3
commit 2eebd47733
12 changed files with 164 additions and 69 deletions

View file

@ -70,7 +70,7 @@
"defer": {
"uncompressed": {
"runtime": 2689,
"main": 124117,
"main": 121811,
"polyfills": 33807,
"src_app_defer_component_ts": 450
}

View file

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

View file

@ -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();
}
},

View file

@ -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();
}
},

View file

@ -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 {
<cmp-a />
} @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', `

View file

@ -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};

View file

@ -1323,6 +1323,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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);

View file

@ -225,6 +225,7 @@ export {
ɵɵdeferPrefetchOnHover,
ɵɵdeferPrefetchOnInteraction,
ɵɵdeferPrefetchOnViewport,
ɵɵdeferEnableTimerScheduling,
ɵɵtext,
ɵɵtextInterpolate,
ɵɵtextInterpolate1,

View file

@ -143,6 +143,7 @@ export {
ɵɵdeferPrefetchOnHover,
ɵɵdeferPrefetchOnInteraction,
ɵɵdeferPrefetchOnViewport,
ɵɵdeferEnableTimerScheduling,
ɵɵtext,
ɵɵtextInterpolate,

View file

@ -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<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex);
}
if (loadingConfigIndex != null) {
tDetails.loadingBlockConfig =
getConstant<DeferredLoadingBlockConfig>(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<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex) :
null,
loadingBlockConfig: loadingConfigIndex != null ?
getConstant<DeferredLoadingBlockConfig>(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<unknown>) {
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<unknown>) {
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<unknown>, 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.
*

View file

@ -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,

View file

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