Skip to content

Commit

Permalink
Deferred build API and builder
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Feb 12, 2021
1 parent a365eea commit 06ef0f7
Show file tree
Hide file tree
Showing 19 changed files with 3,011 additions and 338 deletions.
1 change: 1 addition & 0 deletions build-system/global-configs/experiments-const.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"BENTO_AUTO_UPGRADE": false,
"INI_LOAD_INOB": false,
"V2_IMG_VIDEO": false,
"WITHIN_VIEWPORT_INOB": false
}
14 changes: 14 additions & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ const forbiddenTerms = {
],
},
// Service factories that should only be installed once.
'\\.buildInternal': {
message: 'can only be called by the framework',
allowlist: [
'src/service/builder.js',
'src/service/resource.js',
'testing/iframe.js',
],
},
'getBuilderForDoc': {
message: 'can only be used by the runtime',
allowlist: ['src/custom-element.js', 'src/service/builder.js'],
},
'installActionServiceForDoc': {
message: privateServiceFactory,
allowlist: [
Expand Down Expand Up @@ -308,6 +320,7 @@ const forbiddenTerms = {
'src/chunk.js',
'src/element-service.js',
'src/service.js',
'src/service/builder.js',
'src/service/cid-impl.js',
'src/service/origin-experiments-impl.js',
'src/services.js',
Expand Down Expand Up @@ -1398,6 +1411,7 @@ function hasAnyTerms(file) {
/^test-/.test(basename) ||
/^_init_tests/.test(basename) ||
/_test\.js$/.test(basename) ||
/testing\//.test(pathname) ||
/storybook\/[^/]+\.js$/.test(pathname);
if (!isTestFile) {
hasSrcInclusiveTerms = matchTerms(file, forbiddenTermsSrcInclusive);
Expand Down
87 changes: 76 additions & 11 deletions builtins/amp-img.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import {BaseElement} from '../src/base-element';
import {Layout, isLayoutSizeDefined} from '../src/layout';
import {ReadyState} from '../src/ready-state';
import {Services} from '../src/services';
import {dev} from '../src/log';
import {guaranteeSrcForSrcsetUnsupportedBrowsers} from '../src/utils/img';
Expand Down Expand Up @@ -45,11 +46,39 @@ const ATTRIBUTES_TO_PROPAGATE = [
];

export class AmpImg extends BaseElement {
/** @override @nocollapse */
static V2() {
return V2_IMG_VIDEO;
}

/** @override @nocollapse */
static prerenderAllowed() {
return true;
}

/** @override @nocollapse */
static getPreconnects(element) {
const src = element.getAttribute('src');
if (src) {
return [src];
}

// NOTE(@wassgha): since parseSrcset is computationally expensive and can
// not be inside the `buildCallback`, we went with preconnecting to the
// `src` url if it exists or the first srcset url.
const srcset = element.getAttribute('srcset');
if (srcset) {
// We try to find the first url in the srcset
const srcseturl = /\S+/.exec(srcset);
// Connect to the first url if it exists
if (srcseturl) {
return [srcseturl[0]];
}
}

return null;
}

/** @param {!AmpElement} element */
constructor(element) {
super(element);
Expand Down Expand Up @@ -106,6 +135,10 @@ export class AmpImg extends BaseElement {
if (!IS_ESM) {
guaranteeSrcForSrcsetUnsupportedBrowsers(this.img_);
}

if (AmpImg.V2() && !this.img_.complete) {
this.onReadyState(ReadyState.LOADING);
}
}
}

Expand Down Expand Up @@ -255,6 +288,38 @@ export class AmpImg extends BaseElement {
return false;
}

/** @override */
buildCallback() {
if (!AmpImg.V2()) {
return;
}

// A V2 amp-img loads and reloads automatically.
this.onReadyState(ReadyState.LOADING);
this.initialize_();
const img = dev().assertElement(this.img_);
if (img.complete) {
this.onReadyState(ReadyState.COMPLETE);
this.firstLayoutCompleted();
this.hideFallbackImg_();
}
listen(img, 'load', () => {
this.onReadyState(ReadyState.COMPLETE);
this.firstLayoutCompleted();
this.hideFallbackImg_();
});
listen(img, 'error', (reason) => {
this.onReadyState(ReadyState.ERROR, reason);
this.onImgLoadingError_();
});
}

/** @override */
ensureLoaded() {
const img = dev().assertElement(this.img_);
img.loading = 'eager';
}

/** @override */
layoutCallback() {
this.initialize_();
Expand All @@ -270,6 +335,10 @@ export class AmpImg extends BaseElement {

/** @override */
unlayoutCallback() {
if (!AmpImg.V2()) {
return;
}

if (this.unlistenError_) {
this.unlistenError_();
this.unlistenError_ = null;
Expand Down Expand Up @@ -314,10 +383,8 @@ export class AmpImg extends BaseElement {
!this.allowImgLoadFallback_ &&
this.img_.classList.contains('i-amphtml-ghost')
) {
this.getVsync().mutate(() => {
this.img_.classList.remove('i-amphtml-ghost');
this.toggleFallback(false);
});
this.img_.classList.remove('i-amphtml-ghost');
this.toggleFallback(false);
}
}

Expand All @@ -327,13 +394,11 @@ export class AmpImg extends BaseElement {
*/
onImgLoadingError_() {
if (this.allowImgLoadFallback_) {
this.getVsync().mutate(() => {
this.img_.classList.add('i-amphtml-ghost');
this.toggleFallback(true);
// Hide placeholders, as browsers that don't support webp
// Would show the placeholder underneath a transparent fallback
this.togglePlaceholder(false);
});
this.img_.classList.add('i-amphtml-ghost');
this.toggleFallback(true);
// Hide placeholders, as browsers that don't support webp
// Would show the placeholder underneath a transparent fallback
this.togglePlaceholder(false);
this.allowImgLoadFallback_ = false;
}
}
Expand Down
111 changes: 111 additions & 0 deletions src/base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,57 @@ import {isArray, toWin} from './types';
* @implements {BaseElementInterface}
*/
export class BaseElement {
/**
* Whether this element supports V2 protocol, which includes:
* 1. Layout/unlayout are not managed by the runtime, but instead are
* implemented by the element as needed.
* 2. The element will wait until it's fully parsed before building, unless
* it's mutable. See `mutable`.
* 3. The element can defer its build until later. See `deferredBuild`.
* 4. The construction of the element is delayed until build.
*
* Notice, in this mode `layoutCallback`, `pauseCallback`, `onLayoutMeasure`,
* `getLayoutSize`, and other methods are deprecated. The element must
* independently handle each of these states internally.
*
* @return {boolean}
* @nocollapse
*/
static V2() {
return false;
}

/**
* Whether this element supports mutations. A mutable element can be built
* immediately, even before the element has been fully parsed, thus it should
* be able to apply additional markup when it's parsed. Normally, however,
* the element will wait until it's fully parsed before building to save
* resources.
*
* Only used for V2 elements.
*
* @return {boolean}
* @nocollapse
*/
static mutable() {
return false;
}

/**
* Whether this element supports deferred-build mode. In this mode, the
* element's build will be deferred roughly based on the
* `content-visibility: auto` rules.
*
* Only used for V2 elements.
*
* @param {!AmpElement} unusedElement
* @return {boolean}
* @nocollapse
*/
static deferredBuild(unusedElement) {
return true;
}

/**
* Subclasses can override this method to opt-in into being called to
* prerender when document itself is not yet visible (pre-render mode).
Expand Down Expand Up @@ -135,6 +186,36 @@ export class BaseElement {
return {};
}

/**
* This is the element's build priority.
*
* The lower the number, the higher the priority.
*
* The default priority for base elements is LayoutPriority.CONTENT.
*
* @param {!AmpElement} unusedElement
* @return {number}
* @nocollapse
*/
static getBuildPriority(unusedElement) {
return LayoutPriority.CONTENT;
}

/**
* Called by the framework to give the element a chance to preconnect to
* hosts and prefetch resources it is likely to need. May be called
* multiple times because connections can time out.
*
* Returns an array of URLs to be preconnected.
*
* @param {!AmpElement} unusedElement
* @return {?Array<string>}
* @nocollapse
*/
static getPreconnects(unusedElement) {
return null;
}

/** @param {!AmpElement} element */
constructor(element) {
/** @public @const {!Element} */
Expand Down Expand Up @@ -194,6 +275,7 @@ export class BaseElement {
*
* The default priority for base elements is LayoutPriority.CONTENT.
* @return {number}
* TODO(#31915): remove once V2 migration is complete.
*/
getLayoutPriority() {
return LayoutPriority.CONTENT;
Expand Down Expand Up @@ -226,6 +308,7 @@ export class BaseElement {
* mainly affects fixed-position elements that are adjusted to be always
* relative to the document position in the viewport.
* @return {!./layout-rect.LayoutRectDef}
* TODO(#31915): remove once V2 migration is complete.
*/
getLayoutBox() {
return this.element.getLayoutBox();
Expand All @@ -234,6 +317,7 @@ export class BaseElement {
/**
* Returns a previously measured layout size.
* @return {!./layout-rect.LayoutSizeDef}
* TODO(#31915): remove once V2 migration is complete.
*/
getLayoutSize() {
return this.element.getLayoutSize();
Expand Down Expand Up @@ -344,6 +428,7 @@ export class BaseElement {
* hosts and prefetch resources it is likely to need. May be called
* multiple times because connections can time out.
* @param {boolean=} opt_onLayout
* TODO(#31915): remove once V2 migration is complete.
*/
preconnectCallback(opt_onLayout) {
// Subclasses may override.
Expand Down Expand Up @@ -414,6 +499,26 @@ export class BaseElement {
return false;
}

/**
* Ensure that the element is being eagerly loaded.
*
* Only used for V2 elements.
*/
ensureLoaded() {}

/**
* Update the current `readyState`.
*
* Only used for V2 elements.
*
* @param {!./ready-state.ReadyState} state
* @param {*=} opt_failure
* @final
*/
onReadyState(state, opt_failure) {
this.element.onReadyStateInternal(state, opt_failure);
}

/**
* Subclasses can override this method to opt-in into receiving additional
* {@link layoutCallback} calls. Note that this method is not consulted for
Expand All @@ -435,6 +540,7 @@ export class BaseElement {
* {@link isRelayoutNeeded} method.
*
* @return {!Promise}
* TODO(#31915): remove once V2 migration is complete.
*/
layoutCallback() {
return Promise.resolve();
Expand All @@ -457,13 +563,15 @@ export class BaseElement {
* Requests the element to stop its activity when the document goes into
* inactive state. The scope is up to the actual component. Among other
* things the active playback of video or audio content must be stopped.
* TODO(#31915): remove once V2 migration is complete.
*/
pauseCallback() {}

/**
* Requests the element to resume its activity when the document returns from
* an inactive state. The scope is up to the actual component. Among other
* things the active playback of video or audio content may be resumed.
* TODO(#31915): remove once V2 migration is complete.
*/
resumeCallback() {}

Expand All @@ -474,6 +582,7 @@ export class BaseElement {
* {@link layoutCallback} in case document becomes active again.
*
* @return {boolean}
* TODO(#31915): remove once V2 migration is complete.
*/
unlayoutCallback() {
return false;
Expand All @@ -483,6 +592,7 @@ export class BaseElement {
* Subclasses can override this method to opt-in into calling
* {@link unlayoutCallback} when paused.
* @return {boolean}
* TODO(#31915): remove once V2 migration is complete.
*/
unlayoutOnPause() {
return false;
Expand Down Expand Up @@ -947,6 +1057,7 @@ export class BaseElement {
* This may currently not work with extended elements. Please file
* an issue if that is required.
* @public
* TODO(#31915): remove once V2 migration is complete.
*/
onLayoutMeasure() {}

Expand Down
Loading

0 comments on commit 06ef0f7

Please sign in to comment.