Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Auto-detect images to lightbox under experiment #20393

Merged

Conversation

alanorozco
Copy link
Member

@alanorozco alanorozco commented Jan 17, 2019

Partial for #20395.

Introduces amp-auto-lightbox experiment and extension for field testing.

@alanorozco alanorozco force-pushed the amp-lightbox-gallery-detection branch 2 times, most recently from 902c98c to a056f4f Compare January 17, 2019 02:01
@alanorozco alanorozco force-pushed the amp-lightbox-gallery-detection branch from 23f0231 to c5b8cf4 Compare January 17, 2019 04:21
@alanorozco
Copy link
Member Author

alanorozco commented Jan 17, 2019

@rsimha for bundle size approval.

This gates amp-auto-lightbox under an experiment. Delta should become smaller once experiment guard is removed.

Copy link
Contributor

@rsimha rsimha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bundle size increase LGTM.

@alanorozco
Copy link
Member Author

/to @jridgewell or @choumx for integration bundle size check

@@ -50,6 +52,15 @@ export class AmpImg extends BaseElement {

/** @private {?UnlistenDef} */
this.unlistenError_ = null;

/** @private {function()} */
this.detectIfLightboxable_ = once(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may makes sense to be outside if amp-img since it does not belong to a single image instance (although once handles that). Also we want a good to exclude this in different runtimes.

I think something like
installAutoLightbox() in runtime.js and then calling it from amp.js (and not from amp-shadow.js, amp-inabox) is better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Ideally installAutoLightbox gets called in a chuck created with very low-priority inside amp.js)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we can check to see if there is a single amp-img however before install it)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. (/cc @choumx)


/** @private {function()} */
this.detectIfLightboxable_ = once(() => {
if (!isExperimentOn(this.win, 'amp-lightbox-gallery-detection')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's stick with amp-auto-lightbox as everywhere else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

export const VIEWPORT_AREA_RATIO = 0.4;

const ACTIONABLE_ANCESTORS = 'a[href], amp-selector, amp-script, amp-story';
const TAP_ACTION_REGEX = /(;|\s|^)tap\:/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actionService has a hasAction(element, eventName) that can tell you this. not sure which is faster though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's pretty similar performance since they both traverse up the tree, but using hasAction is more readable, changed.

* @return {boolean}
*/
static meetsAll(element) {
return Criteria.meetsPlaceholderCriteria(element) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the order here sort of matters for performance, it should be a combination of (cost * likeliness to exclude the image) so we can optimally short circuit. Honestly I have no idea what order is the best. (Imagine if you could tell JS that these checks are side-effect free and V8 would optimize the order for you!)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can roll that ourselves, basically by making each criteria async and failing as soon as one of them fails. For now I think this is a premature optimization.

/** Factor of renderArea vs viewportArea to lightbox. */
export const VIEWPORT_AREA_RATIO = 0.4;

const ACTIONABLE_ANCESTORS = 'a[href], amp-selector, amp-script, amp-story';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amp-selector is fine being inside a amp-selector > [option] match isn't.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

*/
static getAllImages(doc) {
const imgs = toArray(doc.querySelectorAll('amp-img'))
.filter(img => !img.hasAttribute(LIGHTBOXABLE_ATTR));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the idea was if lightbox is present even on a single one, we opt out completely. So maybe move this to another high-level rule similar to the sizing and placeholder?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That check is already in place in isEnabledForDoc.


const promises = imgs.map(whenLoadedOrNull);

return Promise.all(promises).then(imagesOrNull =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think this would work. IF we wait for all images to load first, the ones below 3 viewport won't be loaded and we don't lightbox anything. It would actually be amazing to just do the lightboxable check as images finish loading, provided a decent spacing and works on demand. (one problem is lightbox gallery wound't pick up late addition, but there can be a simple API to tell it to add a new image to its list). For now, I guess just don't wait for load event and add a TODO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, done.

}

iterateCursor(lightboxable, element => {
element.setAttribute(LIGHTBOXABLE_ATTR, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you wanna generate a unique "group" key here as set it as the value of lightbox=<group> otherwise they all end up in the "default" group.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@alanorozco alanorozco force-pushed the amp-lightbox-gallery-detection branch 4 times, most recently from e729016 to 4cae170 Compare January 20, 2019 20:19
@alanorozco alanorozco force-pushed the amp-lightbox-gallery-detection branch from 4cae170 to 447f155 Compare January 20, 2019 20:25
src/amp.js Outdated
@@ -121,6 +122,7 @@ if (shouldMainBootstrapRun) {

maybeValidate(self);
makeBodyVisible(self.document);
detectAutoLightbox(ampdoc);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should happen in your extension's initialization process, not here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be a path to the extension from the runtime, this function only checks for existence of amp-img and then includes amp-auto-lightbox. This is essentially a runtime feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this shouldn't be happening here. If the publisher wants auto-lightbox treatment, they should be including amp-auto-lightbox.js.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is supposed to be backported to documents that do not include the extension. /cc @aghassemi

For internal details see go/amp-auto-lightbox. Public I2I TBD.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@choumx @jridgewell: the whole feature is lightboxing images without publisher's involvements (because they don't do it and end user suffers as the result). We will be running experiments and monitoring perf metrics as this progresses. Initially we will be starting with a limited environment to enable this feature (likely just AMP pages in SERP viewer that are of type Article). This PR is purely to put it behind an experiment flag so we can enable manually and start looking at pages to refine the heuristics.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I also assume tree shaking is smart enough not to include all of auto-lightbox.js here in v0. detectAutoLightbox simply checks for couple of conditions and then dynamically loads another extension that would take care of the rest of heuristics)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we considered fetching the extension binary at runtime for experiment-enabled users? Or injecting the extension script via cache transform?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we considered fetching the extension binary at runtime for experiment-enabled users?

That's exactly what detectLightbox does unless I'm missing something.

Or injecting the extension script via cache transform?

That would be a nice improvement over this, I don't see how they're mutually exclusive?

return;
}
chunk(ampdoc, () => {
if (!ampdoc.getRootNode().querySelector('amp-img')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every page has an amp-img.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, might be an unnecessary optimization. Removed.

src/amp.js Outdated
@@ -121,6 +122,7 @@ if (shouldMainBootstrapRun) {

maybeValidate(self);
makeBodyVisible(self.document);
detectAutoLightbox(ampdoc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this shouldn't be happening here. If the publisher wants auto-lightbox treatment, they should be including amp-auto-lightbox.js.

*/
static getAllImages(doc) {
const imgs = toArray(doc.querySelectorAll('amp-img')).filter(img =>
!img.hasAttribute(LIGHTBOXABLE_ATTR) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add these as part of the query selector.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

!img.hasAttribute(VISITED_ATTR));

imgs.forEach(img => {
img.setAttribute(VISITED_ATTR, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VISITED_ATTR is a public attribute (data-amp-auto-lightbox-visited), so it must be done in a mutate phase.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @return {!Promise<?Element>}
*/
function whenLoadedOrNull(el) {
return new Promise(resolve => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return new Promise(resolve => {
return el.signals.whenSignal(CommonSignals.LOAD_END).then(() => el, () => null)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

const maybeApplyAll = () => Scanner.getAllImages(ampdoc.win.document)
.map(imgPromise => imgPromise.then(imgOrNull => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map here is useless, the return map is filled with all undefined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signal is used in tests. For the execution path it's not necessary to go through the result of the promise, but it's useful to yield to all the promises returned here in tests.

const scriptTags = ampdoc.getHeadNode().querySelectorAll('script');

const currentScriptTag = find(scriptTags,
t => t.getAttribute('custom-element') == REQUIRED_EXTENSION);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handled by src/service/extensions-impl.js

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not sure why it's not provided as a public API. I don't know what the best way to expose this may be, since there is some extension holder logic I'm not particularly familiar with.

export const VIEWPORT_AREA_RATIO = 0.3;

const ACTIONABLE_ANCESTORS =
'a[href], amp-selector > [option], amp-script, amp-story';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

option does not need to be direct child of amp-selector, so amp-selector [option] (my bad from previous comment)

/** Factor of renderArea vs viewportArea to lightbox. */
export const VIEWPORT_AREA_RATIO = 0.3;

const ACTIONABLE_ANCESTORS =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another fun one to add button ( <button type="submit" value="OK"> <amp-img src="https://via.placeholder.com/150" width=20 height=20> </button> )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Test added as well.

.then(
() =>
Mutation.mutate(img, () => {
img.setAttribute(VISITED_ATTR, '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when can we endup scanning the same tree twice? (worth adding a comment) also for the name of VISITED_ATTR, let's do i-amphtml- instead of data- as prefix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

function usesLightboxExplicitly(ampdoc) {
const scriptTags = ampdoc.getHeadNode().querySelectorAll('script');

const currentScriptTag = find(scriptTags,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not querySelectorAll(`script [custom-element=${REQUIRED_EXTENSION}]`) ?;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

*/
export function isEnabledForDoc(ampdoc) {
const {win} = ampdoc;
if (!Services.viewerForDoc(ampdoc).isEmbedded() &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&& viewer.isTrustedViewer() (will limit to SERP)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@aghassemi aghassemi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alanorozco left a few comments, nothing major. Thanks!

}
chunk(ampdoc, () => {
Services.extensionsFor(win)
.installExtensionForDoc(ampdoc, 'amp-auto-lightbox');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Duplicated string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to define to a constant, but isExperimentOn only takes literals per lint rules.

/**
* @param {!./service/ampdoc-impl.AmpDoc} ampdoc
*/
export function detectAutoLightbox(ampdoc) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: installAutoLightboxExtension

@alanorozco alanorozco merged commit fc4eb84 into ampproject:master Jan 23, 2019
@alanorozco alanorozco deleted the amp-lightbox-gallery-detection branch January 23, 2019 03:29
noranazmy pushed a commit to noranazmy/amphtml that referenced this pull request Mar 22, 2019
Partial for ampproject#20395.

Introduces `amp-auto-lightbox` experiment and extension for field testing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants