From 81ae5af961cb7cdba156d995c23eed76ce1c6d5d Mon Sep 17 00:00:00 2001 From: Dima Voytenko Date: Fri, 15 Jan 2021 11:25:14 -0800 Subject: [PATCH] Workspace: deferred build mode --- builtins/amp-img.js | 2 +- examples/loader-bento.amp.html | 94 +++++++ examples/loader.amp.html | 97 +++++++ extensions/amp-iframe/0.1/amp-iframe.js | 88 +++++- extensions/amp-video/0.1/amp-video.js | 33 +++ src/custom-element.js | 32 +++ src/layout.js | 2 +- src/service/mutator-impl.js | 16 ++ src/service/owners-impl.js | 4 + src/service/resizer-v2.js | 339 ++++++++++++++++++++++++ src/service/scheduler.js | 22 ++ 11 files changed, 726 insertions(+), 3 deletions(-) create mode 100644 examples/loader-bento.amp.html create mode 100644 examples/loader.amp.html create mode 100644 src/service/resizer-v2.js diff --git a/builtins/amp-img.js b/builtins/amp-img.js index f36d705b0951..7291b2743b27 100644 --- a/builtins/amp-img.js +++ b/builtins/amp-img.js @@ -48,7 +48,7 @@ const ATTRIBUTES_TO_PROPAGATE = [ export class AmpImg extends BaseElement { /** @override @nocollapse */ static V1() { - return V1_IMG_DEFERRED_BUILD; + return true; } /** @override @nocollapse */ diff --git a/examples/loader-bento.amp.html b/examples/loader-bento.amp.html new file mode 100644 index 000000000000..8781b0fe5c98 --- /dev/null +++ b/examples/loader-bento.amp.html @@ -0,0 +1,94 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + + + + + + + + +
scroll
+ + diff --git a/examples/loader.amp.html b/examples/loader.amp.html new file mode 100644 index 000000000000..24a723b8b130 --- /dev/null +++ b/examples/loader.amp.html @@ -0,0 +1,97 @@ + + + + + Lorem Ipsum | PublisherName + + + + + + + + + + + + + + + + + + + + + + + + + + +
scroll
+ + diff --git a/extensions/amp-iframe/0.1/amp-iframe.js b/extensions/amp-iframe/0.1/amp-iframe.js index 3813010e55f8..59c1c3262e1e 100644 --- a/extensions/amp-iframe/0.1/amp-iframe.js +++ b/extensions/amp-iframe/0.1/amp-iframe.js @@ -65,6 +65,11 @@ let count = 0; let trackingIframeTimeout = 5000; export class AmpIframe extends AMP.BaseElement { + /** @override @nocollapse */ + static V1() { + return true; + } + /** @param {!AmpElement} element */ constructor(element) { super(element); @@ -305,6 +310,30 @@ export class AmpIframe extends AMP.BaseElement { this.container_ = makeIOsScrollable(this.element); this.registerIframeMessaging_(); + + if (AmpIframe.V1()) { + this.layoutCallback(); + } + } + + /** @override */ + ensureLoaded() { + if (!this.iframe_) { + this.layoutCallback(); + } + } + + /** @override */ + attachedCallback() { + observeDisplay(this.element, this.onDisplay_); + if (AmpIframe.V1()) { + this.setReadyState('loading'); + } + } + + /** @override */ + detachedCallback() { + unobserveDisplay(this.element, this.onDisplay_); } /** @override */ @@ -405,6 +434,19 @@ export class AmpIframe extends AMP.BaseElement { const iframe = this.element.ownerDocument.createElement('iframe'); + if (AmpIframe.V1()) { + this.setReadyState('loading'); + if (iframe.readyState == 'complete') { + this.setReadyState('complete'); + } + listen(iframe, 'load', () => { + this.setReadyState('complete'); + }); + listen(iframe, 'error', (reason) => { + this.setReadyState('error', reason); + }); + } + this.iframe_ = /** @type {HTMLIFrameElement} */ (iframe); this.applyFillContent(iframe); @@ -482,6 +524,12 @@ export class AmpIframe extends AMP.BaseElement { this.container_.appendChild(iframe); + // DO NOT SUBMIT: might be worth to just fork amp-iframe completely for V2. + if (!AmpIframe.V1()) { + this.isDisplayed_ = false; + observeDisplay(this.element, this.onDisplay_); + } + return this.loadPromise(iframe).then(() => { // On iOS the iframe at times fails to render inside the `overflow:auto` // container. To avoid this problem, we set the `overflow:auto` property @@ -572,6 +620,9 @@ export class AmpIframe extends AMP.BaseElement { * @override **/ unlayoutCallback() { + if (!AmpIframe.V1()) { + unobserveDisplay(this.element, this.onDisplay_); + } if (this.unlistenPym_) { this.unlistenPym_(); this.unlistenPym_ = null; @@ -628,6 +679,38 @@ export class AmpIframe extends AMP.BaseElement { return true; } + /** + * @param {boolean} isDisplayed + * @private + */ + onDisplay_(isDisplayed) { + console.log( + 'amp-iframe: onDisplay_:', + isDisplayed, + this.isDisplayed_, + !!this.iframe_ + ); + if (isDisplayed === this.isDisplayed_) { + return; + } + this.isDisplayed_ = isDisplayed; + + if (AmpIframe.V1()) { + const iframeExists = !!this.iframe_; + if (isDisplayed !== iframeExists) { + if (isDisplayed) { + this.layoutCallback(); + } else { + this.unlayoutCallback(); + } + } + } else { + if (!isDisplayed && this.iframe_) { + this.getVsync().mutate(() => this.unload()); + } + } + } + /** * Makes the iframe visible. * @private @@ -718,6 +801,7 @@ export class AmpIframe extends AMP.BaseElement { if (newHeight !== undefined || newWidth !== undefined) { this.attemptChangeSize(newHeight, newWidth).then( () => { + console.log('amp-iframe: updateSize_: approved'); if (newHeight !== undefined) { this.element.setAttribute('height', newHeight); } @@ -730,7 +814,9 @@ export class AmpIframe extends AMP.BaseElement { newWidth ); }, - () => {} + () => { + console.log('amp-iframe: updateSize_: denied'); + } ); } else { this.user().error( diff --git a/extensions/amp-video/0.1/amp-video.js b/extensions/amp-video/0.1/amp-video.js index 0d412216db41..967469b84354 100644 --- a/extensions/amp-video/0.1/amp-video.js +++ b/extensions/amp-video/0.1/amp-video.js @@ -85,6 +85,11 @@ const ATTRS_TO_PROPAGATE = ATTRS_TO_PROPAGATE_ON_BUILD.concat( * @implements {../../../src/video-interface.VideoInterface} */ export class AmpVideo extends AMP.BaseElement { + /** @override @nocollapse */ + static V1() { + return true; + } + /** * AMP Cache may selectively cache certain video sources (based on various * heuristics such as video type, extensions, etc...). @@ -264,6 +269,30 @@ export class AmpVideo extends AMP.BaseElement { installVideoManagerForDoc(element); Services.videoManagerForDoc(element).register(this); + + if (AmpVideo.V1()) { + this.setReadyState('loading'); + + const video = devAssert(this.video_); + + if (video.readyState > 0) { + this.setReadyState('complete'); + } + listen(video, 'loadedmetadata', () => { + this.setReadyState('complete'); + }); + listen(video, 'error', (reason) => { + this.setReadyState('error', reason); + }); + + this.layoutCallback(); + } + } + + /** @override */ + ensureLoaded() { + const video = dev().assertElement(this.video_); + video.loading = 'eager'; } /** @@ -339,6 +368,10 @@ export class AmpVideo extends AMP.BaseElement { // TODO(@aghassemi, 10756) Either make metadata observable or submit // an event indicating metadata changed (in case metadata changes // while the video is playing). + + if (AmpVideo.V1() && this.video_.readyState === 0) { + this.setReadyState('loading'); + } } /** @override */ diff --git a/src/custom-element.js b/src/custom-element.js index 86845bece2ba..9390fe94ced2 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -224,6 +224,16 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { Ctor = nonStructThis['implementationClassForTesting']; } + console.log( + 'CustomElement: new: ', + this.id, + Ctor ? Ctor.name : 'n/a', + 'V2:', + Ctor ? Ctor.V1() : 'n/a', + 'deferredBuild:', + Ctor ? Ctor.deferredBuild(this) : 'n/a' + ); + /** @private {?(typeof ../base-element.BaseElement)} */ this.implClass_ = Ctor === ElementStub ? null : Ctor || null; @@ -349,6 +359,13 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { * @final @package */ upgrade(newImplClass) { + console.log( + 'CustomElement: upgraded: ', + this.id, + newImplClass.name, + newImplClass.V1(), + newImplClass.deferredBuild(this) + ); if (this.isInTemplate_) { return; } @@ -491,6 +508,8 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { return this.buildingPromise_; } + console.log('CustomElement: buildInternal:', this.id); + this.setReadyStateInternal(ReadyState.BUILDING); // Create the instance. @@ -845,6 +864,13 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { return; } + console.log( + 'CustomElement: setReadyStateInternal:', + this.id, + state, + opt_failure + ); + this.readyState_ = state; if (!this.V1()) { @@ -1130,6 +1156,7 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { * @final */ connectedCallback() { + console.log('CustomElement: connectedCallback:', this.id); if (!isTemplateTagSupported() && this.isInTemplate_ === undefined) { this.isInTemplate_ = !!dom.closestAncestorElementBySelector( this, @@ -1293,6 +1320,7 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { 'Implementation must not be a stub' ); + console.log('CustomElement: create: ', this.id, Ctor.name); const impl = new Ctor(this); // The `upgradeCallback` only allows redirect once for the top-level @@ -1591,6 +1619,9 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { * TODO(#31915): remove once V1 migration is complete. */ layoutCallback(signal) { + console.log('CustomElement: layoutCallback:', this.id); + devAssert(!this.V1(), 'not allowed for V2'); + assertNotTemplate(this); devAssert(this.isBuilt(), 'Must be built to receive viewport events'); // A lot of tests call layoutCallback manually, and don't pass a signal. @@ -2085,6 +2116,7 @@ function createBaseCustomElementClass(win, elementConnectedCallback) { * @package @final */ overflowCallback(overflown, requestedHeight, requestedWidth) { + console.log('CustomElement: overflowCallback:', overflown); this.getOverflowElement(); if (!this.overflowElement_) { if (overflown && this.warnOnMissingOverflow) { diff --git a/src/layout.js b/src/layout.js index 9adc36aa2de7..5a8c9251bab2 100644 --- a/src/layout.js +++ b/src/layout.js @@ -322,7 +322,7 @@ export function isLoadingAllowed(element) { */ export function isIframeVideoPlayerComponent(tagName) { if (tagName == 'AMP-VIDEO') { - return false; + return true; // DO NOT SUBMIT: why was this `false` before? } return videoPlayerTagNameRe.test(tagName); } diff --git a/src/service/mutator-impl.js b/src/service/mutator-impl.js index e0b2a6a3d1ee..b109716e38f5 100644 --- a/src/service/mutator-impl.js +++ b/src/service/mutator-impl.js @@ -16,6 +16,7 @@ import {FocusHistory} from '../focus-history'; import {MutatorInterface} from './mutator-interface'; +import {ResizerV2} from './resizer-v2'; import {Resource} from './resource'; import {Services} from '../services'; import {areMarginsChanged} from '../layout-rect'; @@ -58,6 +59,9 @@ export class MutatorImpl { this.activeHistory_.onFocus((element) => { this.checkPendingChangeSize_(element); }); + + /** @private @const */ + this.resizer_ = new ResizerV2(ampdoc); } /** @override */ @@ -321,6 +325,18 @@ export class MutatorImpl { force, opt_callback ) { + // DO NOT SUBMIT: testing + this.resizer_.tryChangeSize( + resource.element, + newHeight, + newWidth, + newMargins, + event, + force, + () => {}, + () => {} + ); + if (resource.hasBeenMeasured() && !newMargins) { this.completeScheduleChangeSize_( resource, diff --git a/src/service/owners-impl.js b/src/service/owners-impl.js index 960e49545cc8..62dce691681f 100644 --- a/src/service/owners-impl.js +++ b/src/service/owners-impl.js @@ -163,6 +163,10 @@ export class OwnersImpl { */ scheduleLayoutOrPreloadForSubresources_(parentResource, layout, subElements) { this.findResourcesInElements_(parentResource, subElements, (resource) => { + console.log( + 'OwnersImpl: measureAndTryScheduleLayout_: ', + resource.debugid + ); resource.element.ensureLoaded(parentResource.getLayoutPriority()); }); } diff --git a/src/service/resizer-v2.js b/src/service/resizer-v2.js new file mode 100644 index 000000000000..73194afe4463 --- /dev/null +++ b/src/service/resizer-v2.js @@ -0,0 +1,339 @@ +/** + * Copyright 2021 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Deferred} from '../utils/promise'; +import {FocusHistory} from '../focus-history'; +import {Services} from '../services'; +import {computedStyle} from '../style'; + +const FOCUS_HISTORY_TIMEOUT = 1000 * 60; // 1min + +/** @implements {!Disposable} */ +export class ResizerV2 { + /** + * @param {!./ampdoc-impl.AmpDoc} ampdoc + */ + constructor(ampdoc) { + /** @private @const {!./ampdoc-impl.AmpDoc} */ + this.ampdoc_ = ampdoc; + + const {win} = ampdoc; + + /** @private {!WeakMap} */ + this.measuredElements_ = new WeakMap(); + + /** @private @const {!IntersectionObserver} */ + this.measuringObserver_ = new win.IntersectionObserver( + (records) => { + for (let i = 0; i < records.length; i++) { + const record = records[i]; + const element = record.target; + const deferred = this.measuredElements_.get(element); + if (deferred) { + deferred.resolve(record); + this.measuringObserver_.unobserve(element); + this.measuredElements_.delete(element); + } + } + }, + {root: win.document} + ); + + /** @private @const {function(!Element):!Promise} */ + this.measure_ = (element) => { + let deferred = this.measuredElements_.get(element); + if (!deferred) { + deferred = new Deferred(); + this.measuredElements_.set(element, deferred); + this.measuringObserver_.observe(element); + } + return deferred.promise; + }; + + /** @private @const {!./viewport/viewport-interface.ViewportInterface} */ + this.viewport_ = Services.viewportForDoc(ampdoc); + + /** @private @const {!FocusHistory} */ + this.activeHistory_ = new FocusHistory(win, FOCUS_HISTORY_TIMEOUT); + + /** @private {?number} */ + this.initialContentHeight_ = null; + } + + /** @override */ + dispose() { + this.measuringObserver_.disconnect(); + this.measuredElements_ = null; + } + + /* QQQ + someCaller() { + return new Promise((resolve, reject) => { + tryChangeSize_(a, b, c, + (newHeight, newWidth, newMargins) => { + resource.changeSize(newHeight, newWidth, newMargins); + // QQQQ: notify resources + }, + (approved, newHeight, newWidth, newMargins) => { + resource.overflowCallback( + \* overflown *\ !approved, + newHeight, + newWidth, + newMargins + ); + if (approved) { + resolve(); + } else { + reject(new Error('...')); + } + }); + }); + } + */ + + /** + * @param {!Element} element + * @param {number|undefined} newHeight + * @param {number|undefined} newWidth + * @param {!../layout-rect.LayoutMarginsChangeDef|undefined} newMargins + * @param {?Event} event + * @param {function(number|undefined, number|undefined, !../layout-rect.LayoutMarginsChangeDef|undefined)} onChange + * @param {function(boolean, number|undefined, number|undefined, !../layout-rect.LayoutMarginsChangeDef|undefined)} onComplete + */ + tryChangeSize( + element, + newHeight, + newWidth, + newMargins, + event, + onChange, + onComplete + ) { + this.measure_(element).then((intersection) => + this.tryChangeSize_( + element, + intersection, + newHeight, + newWidth, + newMargins, + event, + onChange, + onComplete + ) + ); + } + + /** + * @param {!Element} element + * @param {!IntersectionObserverEntry} intersection + * @param {number|undefined} newHeight + * @param {number|undefined} newWidth + * @param {!../layout-rect.LayoutMarginsChangeDef|undefined} newMargins + * @param {?Event} event + * @param {function(number|undefined, number|undefined, !../layout-rect.LayoutMarginsChangeDef|undefined)} onChange + * @param {function(boolean, number|undefined, number|undefined, !../layout-rect.LayoutMarginsChangeDef|undefined)} onComplete + * @private + */ + tryChangeSize_( + element, + intersection, + newHeight, + newWidth, + newMargins, + event, + onChange, + onComplete + ) { + const {boundingClientRect, rootBounds} = intersection; + + // The element might no longer be attached to DOM. If so, it can be + // resized w/o problems. + if (!rootBounds) { + onChange(newHeight, newWidth, newMargins); + onComplete(true); + return; + } + + // Measuring from the InOb callback is mostly free. + const topOffset = rootBounds.height / 10; + const bottomOffset = rootBounds.height / 10; + const scrollTop = this.viewport_.getScrollTop(); + + const {top, bottom, height, width} = boundingClientRect; + let topUnchangedBoundary = top; + let bottomDisplacedBoundary = bottom; + let topMarginDiff = 0; + let bottomMarginDiff = 0; + let leftMarginDiff = 0; + let rightMarginDiff = 0; + + if (newMargins) { + const margins = computeMargins(this.ampdoc_.win, element); + if (newMargins.top != undefined) { + topMarginDiff = newMargins.top - margins.top; + } + if (newMargins.bottom != undefined) { + bottomMarginDiff = newMargins.bottom - margins.bottom; + } + if (newMargins.left != undefined) { + leftMarginDiff = newMargins.left - margins.left; + } + if (newMargins.right != undefined) { + rightMarginDiff = newMargins.right - margins.right; + } + if (topMarginDiff) { + topUnchangedBoundary -= margins.top; + } + if (bottomMarginDiff) { + // The lowest boundary of the element that would appear to be + // resized as a result of this size change. If the bottom margin is + // being changed then it is the bottom edge of the margin box, + // otherwise it is the bottom edge of the layout box as set above. + bottomDisplacedBoundary += margins.bottom; + } + } + const heightDiff = newHeight == null ? 0 : newHeight - height; + const widthDiff = newWidth == null ? 0 : newWidth - width; + + const changed = !( + heightDiff == 0 && + topMarginDiff == 0 && + bottomMarginDiff == 0 && + widthDiff == 0 && + leftMarginDiff == 0 && + rightMarginDiff == 0 + ); + let allow = false; + let adjScrollHeight = false; + if (!changed) { + // 1. Nothing to resize. + allow = true; + } else if (!this.ampdoc_.isVisible()) { + // 2. An immediate execution requested or the document is hidden. + allow = true; + } else if ( + this.activeHistory_.hasDescendantsOf(element) || + (event && event.userActivation && event.userActivation.hasBeenActive) + ) { + // 3. Active elements are immediately resized. The assumption is that + // the resize is triggered by the user action or soon after. + allow = true; + } else if ( + topUnchangedBoundary >= rootBounds.height - bottomOffset || + (topMarginDiff == 0 && + bottom + Math.min(heightDiff, 0) >= rootBounds.height - bottomOffset) + ) { + // 4. Elements under viewport are resized immediately, but only if + // an element's boundary is not changed above the viewport after + // resize. + allow = true; + } else if ( + scrollTop > 1 && + bottomDisplacedBoundary <= topOffset && + (heightDiff >= 0 || scrollTop + heightDiff >= 0) + ) { + // 5. Elements above the viewport can only be resized if we are able + // to compensate the height change by setting scrollTop and only if + // the page has already been scrolled by some amount (1px due to iOS). + // Otherwise the scrolling might move important things like the menu + // bar out of the viewport at initial page load. + allow = true; + adjScrollHeight = true; + } else if (this.isNearBottom_(bottom + scrollTop)) { + // 6. Elements close to the bottom of the document (not viewport) + // are resized immediately. + allow = true; + } else if (heightDiff < 0 || topMarginDiff < 0 || bottomMarginDiff < 0) { + // 7. The new height (or one of the margins) is smaller than the + // current one. + } else if (heightDiff == 0 && widthDiff != 0) { + // 8. Element is in viewport, but this is a width-only expansion. + // Check whether this should be reflow-free, in which case, + // schedule a size change. + const parent = element.parentElement; + if (parent) { + const parentWidth = parent./*OK*/ offsetWidth; + let cumulativeWidth = widthDiff; + for (let i = 0; i < parent.childElementCount; i++) { + cumulativeWidth += parent.children[i]./*OK*/ offsetWidth; + if (cumulativeWidth > parentWidth) { + break; + } + } + allow = cumulativeWidth <= parentWidth; + } + } + + if (allow && changed) { + if (adjScrollHeight) { + // The browser is normally fully sync'd in a InOb callback, thus reads + // would not be blocking. + const scrollHeight = this.viewport_.getScrollHeight(); + + onChange(newHeight, newWidth, newMargins); + + // This measurement is definitely blocking, but we have to do it sync + // to avoid scroll jumps causing FOUC. + const newScrollHeight = this.viewport_.getScrollHeight(); + if (newScrollHeight != scrollHeight) { + this.viewport_.setScrollTop( + scrollTop + (newScrollHeight - scrollHeight) + ); + } + } else { + onChange(newHeight, newWidth, newMargins); + } + } + + onComplete(allow, newHeight, newWidth, newMargins); + } + + /** + * @param {number} bottom + * @return {boolean} + * @private + */ + isNearBottom_(bottom) { + const contentHeight = this.viewport_.getContentHeight(); + if (this.initialContentHeight_ == null) { + this.initialContentHeight_ = contentHeight; + } + const minContentHeight = Math.min( + contentHeight, + this.initialContentHeight_ + ); + const threshold = Math.max( + minContentHeight * 0.85, + minContentHeight - 1000 + ); + return bottom >= threshold; + } +} + +/** + * @param {!Window} win + * @param {!Element} element + * @return {!../layout-rect.LayoutMarginsChangeDef} + */ +function computeMargins(win, element) { + const style = computedStyle(win, element); + return { + top: parseInt(style.marginTop, 10) || 0, + right: parseInt(style.marginRight, 10) || 0, + bottom: parseInt(style.marginBottom, 10) || 0, + left: parseInt(style.marginLeft, 10) || 0, + }; +} diff --git a/src/service/scheduler.js b/src/service/scheduler.js index 139ef0dca7d2..7d5e607869c1 100644 --- a/src/service/scheduler.js +++ b/src/service/scheduler.js @@ -74,6 +74,12 @@ export class Scheduler { * @param {!AmpElement} target */ scheduleAsap(target) { + console.log( + 'Builder: scheduleAsap:', + target.id, + 'mutable:', + target.mutable() + ); this.targets_.set(target, {asap: true, isIntersecting: false}); this.waitParsing_(target); } @@ -111,6 +117,8 @@ export class Scheduler { return; } + console.log('Builder: unschedule:', target.id); + this.targets_.delete(target); this.observer_.unobserve(target); @@ -220,6 +228,14 @@ export class Scheduler { if (parsingTargets) { for (let i = 0; i < parsingTargets.length; i++) { const target = parsingTargets[i]; + console.log( + 'Builder: parse complete?:', + target.id, + 'docReady:', + documentReady, + 'hasNext:', + hasNextNodeInDocumentOrder(target, this.ampdoc_.getRootNode()) + ); if ( documentReady || hasNextNodeInDocumentOrder(target, this.ampdoc_.getRootNode()) @@ -279,6 +295,12 @@ export class Scheduler { vs == VisibilityState.HIDDEN || // Prerender can only proceed when allowed. (vs == VisibilityState.PRERENDER && target.prerenderAllowed())); + console.log('Builder: build?:', target.id, toBuild, { + parsed, + isIntersecting, + dovVis: this.ampdoc_.getVisibilityState(), + prerender: target.prerenderAllowed(), + }); if (!toBuild) { return; }