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

URL prefetching, Ads preconnect and prefetching and Safari preconnect #805

Merged
merged 1 commit into from
Oct 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ads/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ We will provide the following information to the ad:
More information can be provided in a similar fashion if needed (Please file an issue).

### Minimizing HTTP requests

#### JS reuse across iframes
To allow ads to bundle HTTP requests across multiple ad units on the same page the object `window.context.master` will contain the window object of the iframe being elected master iframe for the current page.

#### Preconnect and prefetch
Add the JS URLs that an ad **always** fetches or always connects to (if you know the origin but not the path) to [_prefetch.js](_prefetch.js).

This triggers prefetch/preconnect when the ad is first seen, so that loads are faster when they come into view.


### Ad markup
Ads are loaded using a the <amp-ad> tag given the type of the ad network and name value pairs of configuration. This is an example for the A9 network:
Expand Down
43 changes: 43 additions & 0 deletions ads/_prefetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Why underscore in file?

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe a bad idea. I kinda wanted this to stand out since it needs to be updated a lot.

* Copyright 2015 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.
*/

/**
* URLs to prefetch for a given ad type.
*
* This MUST be kept in sync with actual implementation.
*
* @const {!Object<string, (string|!Array<string>)>}
*/
export const adPrefetch = {
doubleclick: 'https://www.googletagservices.com/tag/js/gpt.js',
a9: 'https://c.amazon-adsystem.com/aax2/assoc.js',
adsense: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js',
};

/**
* URLs to connect to for a given ad type.
*
* This MUST be kept in sync with actual implementation.
*
* @const {!Object<string, (string|!Array<string>)>}
*/
export const adPreconnect = {
adreactor: 'https://adserver.adreactor.com',
doubleclick: [
'https://partner.googleadservices.com',
'https://securepubads.g.doubleclick.net',
],
};
37 changes: 34 additions & 3 deletions builtins/amp-ad.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {isLayoutSizeDefined} from '../src/layout';
import {setStyles} from '../src/style';
import {loadPromise} from '../src/event-helper';
import {registerElement} from '../src/custom-element';
import {getIframe, listen} from '../src/3p-frame';
import {getIframe, listen, prefetchBootstrap} from '../src/3p-frame';
import {adPrefetch, adPreconnect} from '../ads/_prefetch';


/**
Expand Down Expand Up @@ -111,8 +112,6 @@ export function installAd(win) {

/** @override */
createdCallback() {
this.preconnect.threePFrame();

/** @private {?Element} */
this.iframe_ = null;

Expand All @@ -131,6 +130,7 @@ export function installAd(win) {

/** @override */
buildCallback() {
this.prefetchAd_();
if (this.placeholder_) {
this.placeholder_.classList.add('hidden');
} else {
Expand All @@ -141,6 +141,37 @@ export function installAd(win) {
}
}

/**
* Prefetches and preconnects URLs related to the ad.
* @private
*/
prefetchAd_() {
// We always need the bootstrap.
prefetchBootstrap(this.getWin());
let type = this.element.getAttribute('type');
let prefetch = adPrefetch[type];
let preconnect = adPreconnect[type];
if (typeof prefetch == 'string') {
this.preconnect.prefetch(prefetch);
} else if (prefetch) {
prefetch.forEach(p => {
this.preconnect.prefetch(p);
});
}
if (typeof preconnect == 'string') {
this.preconnect.url(preconnect);
} else if (preconnect) {
preconnect.forEach(p => {
this.preconnect.url(p);
});
}
// If fully qualified src for ad script is specified we prefetch that.
let src = this.element.getAttribute('src');
if (src) {
this.preconnect.prefetch(src);
}
}

/**
* @override
*/
Expand Down
6 changes: 3 additions & 3 deletions extensions/amp-twitter/0.1/amp-twitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/


import {getIframe, listen} from '../../../src/3p-frame';
import {getIframe, listen, prefetchBootstrap} from '../../../src/3p-frame';
import {isLayoutSizeDefined} from '../../../src/layout';
import {loadPromise} from '../../../src/event-helper';

Expand All @@ -26,8 +26,8 @@ class AmpTwitter extends AMP.BaseElement {
// This domain serves the actual tweets as JSONP.
this.preconnect.url('https://syndication.twitter.com');
// Hosts the script that renders tweets.
this.preconnect.url('https://platform.twitter.com');
this.preconnect.threePFrame();
this.preconnect.prefetch('https://platform.twitter.com/widgets.js');
prefetchBootstrap(this.getWin());
}

/** @override */
Expand Down
16 changes: 16 additions & 0 deletions src/3p-frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {getLengthNumeral} from '../src/layout';
import {getService} from './service';
import {documentInfoFor} from './document-info';
import {getMode} from './mode';
import {preconnectFor} from './preconnect';
import {dashToCamelCase} from './string';
import {parseUrl, assertHttpsUrl} from './url';

Expand Down Expand Up @@ -159,6 +160,21 @@ export function addDataAndJsonAttributes_(element, attributes) {
}
}

/**
* Prefetches URLs related to the bootstrap iframe.
* @param {!Window} parentWindow
* @return {string}
*/
export function prefetchBootstrap(window) {
var url = getBootstrapBaseUrl(window);
var preconnect = preconnectFor(window);
preconnect.prefetch(url);
// While the URL may point to a custom domain, this URL will always be
// fetched by it.
preconnect.prefetch(
'https://3p.ampproject.net/$internalRuntimeVersion$/f.js');
}

/**
* Returns the base URL for 3p bootstrap iframes.
* @param {!Window} parentWindow
Expand Down
9 changes: 9 additions & 0 deletions src/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ export class Platform {
return /iPhone|iPad|iPod/i.test(this.win.navigator.userAgent);
}

/**
* Whether the current browser is Safari.
* @return {boolean}
*/
isSafari() {
return /Safari/i.test(this.win.navigator.userAgent) && !this.isChrome();
}

/**
* Whether the current browser a Chrome browser.
* @return {boolean}
*/
isChrome() {
// Also true for MS Edge :)
return /Chrome|CriOS/i.test(this.win.navigator.userAgent);
}
};
Expand Down
88 changes: 80 additions & 8 deletions src/preconnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import {getService} from './service';
import {parseUrl} from './url';
import {timer} from './timer';
import {platformFor} from './platform';


class Preconnect {
Expand All @@ -35,38 +36,109 @@ class Preconnect {
this.head_ = win.document.head;
/** @private @const {!Object<string, boolean>} */
this.origins_ = {};
/** @private @const {!Object<string, boolean>} */
this.urls_ = {};
/** @private @const {!Platform} */
this.platform_ = platformFor(win);
// Mark current origin as preconnected.
this.origins_[parseUrl(win.location.href).origin] = true;
}

/**
* Preconnects to a URL.
* Preconnects to a URL. Always also does a dns-prefetch because
* browser support for that is better.
* @param {string} url
*/
url(url) {
var origin = parseUrl(url).origin;
if (!this.isInterestingUrl_(url)) {
return;
}
const origin = parseUrl(url).origin;
if (this.origins_[origin]) {
return;
}
this.origins_[origin] = true;
var dns = document.createElement('link');
const dns = document.createElement('link');
dns.setAttribute('rel', 'dns-prefetch');
dns.setAttribute('href', origin);
var preconnect = document.createElement('link');
const preconnect = document.createElement('link');
preconnect.setAttribute('rel', 'preconnect');
preconnect.setAttribute('href', origin);
this.head_.appendChild(dns);
this.head_.appendChild(preconnect);

// Remove the tags eventually to free up memory.
timer.delay(() => {
this.head_.removeChild(dns);
this.head_.removeChild(preconnect);
if (dns.parentNode) {
dns.parentNode.removeChild(dns);
}
if (preconnect.parentNode) {
preconnect.parentNode.removeChild(preconnect);
}
}, 10000);

this.preconnectPolyfill_(origin);
}

/**
* Asks the browser to prefetch a URL. Always also does a preconnect
* because browser support for that is better.
* @param {string} url
*/
prefetch(url) {
if (!this.isInterestingUrl_(url)) {
return;
}
if (this.urls_[url]) {
return;
}
this.urls_[url] = true;
this.url(url);
const prefetch = document.createElement('link');
prefetch.setAttribute('rel', 'prefetch');
prefetch.setAttribute('href', url);
this.head_.appendChild(prefetch);
// As opposed to preconnect we do not clean this tag up, because there is
// no expectation as to it having an immediate effect.
}

isInterestingUrl_(url) {
if (url.indexOf('https:') == 0 || url.indexOf('http:') == 0) {
return true;
}
return false;
}

threePFrame() {
this.url('https://3p.ampproject.net');
/**
* Safari does not support preconnecting, but due to its significant
* performance benefits we implement this crude polyfill.
*
* We make an image connection to a "well-known" file on the origin adding
* a random query string to bust the cache (no caching because we do want to
* actually open the connection).
*
* This should get us an open SSL connection to these hosts and significantly
* speed up the next connections.
*
* The actual URL is expected to 404. If you see errors for
* amp_preconnect_polyfill in your DevTools console or server log:
* This is expected and fine to leave as is. Its fine to send a non 404
* response, but please make it small :)
*/
preconnectPolyfill_(origin) {
// Unfortunately there is no way to feature detect whether preconnect is
// supported, so we do this only in Safari, which is the most important
// browser without support for it. This needs to be removed should it
// ever add support.
if (!this.platform_.isSafari()) {
return;
}
const url = origin + '/amp_preconnect_polyfill?' + Math.random();
// We use an XHR without withCredentials(true), so we do not send cookies
// to the host and the host cannot set cookies.
const xhr = new XMLHttpRequest();
xhr.open('HEAD', url, true);
xhr.send();
}
}

Expand Down
14 changes: 14 additions & 0 deletions test/_init_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ it.skipOnFirefox = function(desc, fn) {
it(desc, fn);
};

// Global cleanup of tags added during tests. Cool to add more
// to selector.
afterEach(() => {
var cleanup = document.querySelectorAll('link,meta');
for (var i = 0; i < cleanup.length; i++) {
try {
cleanup[i].parentNode.removeChild(cleanup[i]);
} catch (e) {
// This sometimes fails for unknown reasons.
console./*OK*/log(e);
}
}
});

chai.Assertion.addMethod('attribute', function(attr) {
var obj = this._obj;
var tagName = obj.tagName.toLowerCase();
Expand Down
15 changes: 13 additions & 2 deletions test/functional/test-3p-frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

import {addDataAndJsonAttributes_, getIframe, getBootstrapBaseUrl} from
'../../src/3p-frame';
import {addDataAndJsonAttributes_, getIframe, getBootstrapBaseUrl,
prefetchBootstrap} from '../../src/3p-frame';
import {loadPromise} from '../../src/event-helper';
import {setModeForTesting, getMode} from '../../src/mode';
import {resetServiceForTesting} from '../../src/service';
Expand Down Expand Up @@ -145,4 +145,15 @@ describe('3p-frame', () => {
getBootstrapBaseUrl(window);
}).to.throw(/must not be on the same origin as the/);
});

it('should prefetch bootstrap frame and JS', () => {
prefetchBootstrap(window);
var fetches = document.querySelectorAll(
'link[rel=prefetch]');
expect(fetches).to.have.length(2);
expect(fetches[0].href).to.equal(
'http://ads.localhost:9876/dist.3p/current/frame.max.html');
expect(fetches[1].href).to.equal(
'https://3p.ampproject.net/$internalRuntimeVersion$/f.js');
});
});
Loading