From 72a6a671397d3abf2d877e135617af493ed07f92 Mon Sep 17 00:00:00 2001 From: Mohammad Khatib Date: Wed, 2 Mar 2016 11:08:11 -0800 Subject: [PATCH] Allow using preloading over prefetching. --- builtins/amp-ad.js | 4 +- extensions/amp-facebook/0.1/amp-facebook.js | 3 +- extensions/amp-twitter/0.1/amp-twitter.js | 3 +- src/3p-frame.js | 4 +- src/preconnect.js | 26 +- test/functional/test-3p-frame.js | 2 + test/functional/test-preconnect.js | 350 +++++++++++++------- 7 files changed, 272 insertions(+), 120 deletions(-) diff --git a/builtins/amp-ad.js b/builtins/amp-ad.js index 7b0b69f67473..248b3bfb304d 100644 --- a/builtins/amp-ad.js +++ b/builtins/amp-ad.js @@ -122,10 +122,10 @@ export function installAd(win) { const prefetch = adPrefetch[type]; const preconnect = adPreconnect[type]; if (typeof prefetch == 'string') { - this.preconnect.prefetch(prefetch); + this.preconnect.prefetch(prefetch, 'script'); } else if (prefetch) { prefetch.forEach(p => { - this.preconnect.prefetch(p); + this.preconnect.prefetch(p, 'script'); }); } if (typeof preconnect == 'string') { diff --git a/extensions/amp-facebook/0.1/amp-facebook.js b/extensions/amp-facebook/0.1/amp-facebook.js index c74dbcaba995..723826b0e7c0 100644 --- a/extensions/amp-facebook/0.1/amp-facebook.js +++ b/extensions/amp-facebook/0.1/amp-facebook.js @@ -26,7 +26,8 @@ class AmpFacebook extends AMP.BaseElement { preconnectCallback(onLayout) { this.preconnect.url('https://facebook.com', onLayout); // Hosts the facebook SDK. - this.preconnect.prefetch('https://connect.facebook.net/en_US/sdk.js'); + this.preconnect.prefetch( + 'https://connect.facebook.net/en_US/sdk.js', 'script'); prefetchBootstrap(this.getWin()); } diff --git a/extensions/amp-twitter/0.1/amp-twitter.js b/extensions/amp-twitter/0.1/amp-twitter.js index bb46b054f3e7..67fbda98a7e3 100644 --- a/extensions/amp-twitter/0.1/amp-twitter.js +++ b/extensions/amp-twitter/0.1/amp-twitter.js @@ -29,7 +29,8 @@ class AmpTwitter extends AMP.BaseElement { // All images this.preconnect.url('https://pbs.twimg.com', onLayout); // Hosts the script that renders tweets. - this.preconnect.prefetch('https://platform.twitter.com/widgets.js'); + this.preconnect.prefetch( + 'https://platform.twitter.com/widgets.js', 'script'); prefetchBootstrap(this.getWin()); } diff --git a/src/3p-frame.js b/src/3p-frame.js index 1740ada9af47..bfbe613d25b6 100644 --- a/src/3p-frame.js +++ b/src/3p-frame.js @@ -156,11 +156,11 @@ export function addDataAndJsonAttributes_(element, attributes) { export function prefetchBootstrap(window) { const url = getBootstrapBaseUrl(window); const preconnect = preconnectFor(window); - preconnect.prefetch(url); + preconnect.prefetch(url, 'document'); // 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'); + 'https://3p.ampproject.net/$internalRuntimeVersion$/f.js', 'script'); } /** diff --git a/src/preconnect.js b/src/preconnect.js index cfe6707af952..0b033384cac7 100644 --- a/src/preconnect.js +++ b/src/preconnect.js @@ -28,7 +28,7 @@ import {platformFor} from './platform'; const ACTIVE_CONNECTION_TIMEOUT_MS = 180 * 1000; const PRECONNECT_TIMEOUT_MS = 10 * 1000; -class Preconnect { +export class Preconnect { /** * @param {!Window} win @@ -51,6 +51,9 @@ class Preconnect { this.platform_ = platformFor(win); // Mark current origin as preconnected. this.origins_[parseUrl(win.location.href).origin] = true; + + /** @private {boolean} */ + this.preloadSupported_ = this.isPreloadSupported_(); } /** @@ -109,20 +112,26 @@ class Preconnect { /** * Asks the browser to prefetch a URL. Always also does a preconnect * because browser support for that is better. + * * @param {string} url + * @param {string=} opt_preloadAs */ - prefetch(url) { + prefetch(url, opt_preloadAs) { if (!this.isInterestingUrl_(url)) { return; } if (this.urls_[url]) { return; } + const command = this.preloadSupported_ ? 'preload' : 'prefetch'; this.urls_[url] = true; this.url(url, /* opt_alsoConnecting */ true); const prefetch = document.createElement('link'); - prefetch.setAttribute('rel', 'prefetch'); + prefetch.setAttribute('rel', command); prefetch.setAttribute('href', url); + if (opt_preloadAs) { + prefetch.setAttribute('as', opt_preloadAs); + } 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. @@ -135,6 +144,17 @@ class Preconnect { return false; } + /** @private */ + isPreloadSupported_() { + const tokenList = document.createElement('link').relList; + if (!tokenList || !tokenList.supports) { + this.preloadSupported_ = false; + return this.preloadSupported_; + } + this.preloadSupported_ = tokenList.supports('preload'); + return this.preloadSupported_; + } + /** * Safari does not support preconnecting, but due to its significant * performance benefits we implement this crude polyfill. diff --git a/test/functional/test-3p-frame.js b/test/functional/test-3p-frame.js index 44b16fb3d5ca..17120e40ddc8 100644 --- a/test/functional/test-3p-frame.js +++ b/test/functional/test-3p-frame.js @@ -183,8 +183,10 @@ describe('3p-frame', () => { expect(fetches).to.have.length(2); expect(fetches[0].href).to.equal( 'http://ads.localhost:9876/dist.3p/current/frame.max.html'); + expect(fetches[0].getAttribute('as')).to.equal('document'); expect(fetches[1].href).to.equal( 'https://3p.ampproject.net/$internalRuntimeVersion$/f.js'); + expect(fetches[1].getAttribute('as')).to.equal('script'); }); it('should make sub domains (unique)', () => { diff --git a/test/functional/test-preconnect.js b/test/functional/test-preconnect.js index 3aa97e4ae170..e90029ed4fd0 100644 --- a/test/functional/test-preconnect.js +++ b/test/functional/test-preconnect.js @@ -14,7 +14,8 @@ * limitations under the License. */ -import {preconnectFor} from '../../src/preconnect'; +import {createIframePromise} from '../../testing/iframe'; +import {preconnectFor, Preconnect} from '../../src/preconnect'; import * as sinon from 'sinon'; describe('preconnect', () => { @@ -22,12 +23,34 @@ describe('preconnect', () => { let sandbox; let clock; let preconnect; + let preloadSupported; + let isSafari; // Factored out to make our linter happy since we don't allow // bare javascript URLs. const javascriptUrlPrefix = 'javascript'; + function getPreconnectIframe() { + return createIframePromise().then(iframe => { + if (preloadSupported !== undefined) { + sandbox.stub(Preconnect.prototype, 'isPreloadSupported_', () => { + return preloadSupported; + }); + } + preconnect = preconnectFor(iframe.win); + if (isSafari !== undefined) { + sandbox.stub(preconnect.platform_, 'isSafari', () => { + return isSafari; + }); + } + return iframe; + }); + } beforeEach(() => { + isSafari = undefined; + // Default mock to not support preload - override in cases to test for + // preload support. + preloadSupported = false; sandbox = sinon.sandbox.create(); clock = sandbox.useFakeTimers(); preconnect = preconnectFor(window); @@ -39,133 +62,238 @@ describe('preconnect', () => { }); it('should preconnect', () => { - sandbox.stub(preconnect.platform_, 'isSafari', () => false); - const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); - preconnect.url('https://a.preconnect.com/foo/bar'); - preconnect.url('https://a.preconnect.com/other'); - preconnect.url(javascriptUrlPrefix + ':alert()'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelector('link[rel=dns-prefetch]').href) - .to.equal('https://a.preconnect.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - expect(document.querySelector('link[rel=preconnect]').href) - .to.equal('https://a.preconnect.com/'); - expect(document.querySelectorAll('link[rel=prefetch]')) - .to.have.length(0); - expect(open.callCount).to.equal(0); + isSafari = false; + return getPreconnectIframe().then(iframe => { + const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); + preconnect.url('https://a.preconnect.com/foo/bar'); + preconnect.url('https://a.preconnect.com/other'); + preconnect.url(javascriptUrlPrefix + ':alert()'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=prefetch]')) + .to.have.length(0); + expect(open.callCount).to.equal(0); + }); }); it('should preconnect with polyfill', () => { - sandbox.stub(preconnect.platform_, 'isSafari', () => true); - const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); - const send = sandbox.spy(XMLHttpRequest.prototype, 'send'); - preconnect.url('https://s.preconnect.com/foo/bar'); - preconnect.url('https://s.preconnect.com/other'); - preconnect.url(javascriptUrlPrefix + ':alert()'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelector('link[rel=dns-prefetch]').href) - .to.equal('https://s.preconnect.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - expect(document.querySelector('link[rel=preconnect]').href) - .to.equal('https://s.preconnect.com/'); - expect(document.querySelectorAll('link[rel=prefetch]')) - .to.have.length(0); - expect(open.callCount).to.equal(1); - expect(send.callCount).to.equal(1); - expect(open.args[0][1]).to.include( - 'https://s.preconnect.com/amp_preconnect_polyfill_404_or' + - '_other_error_expected._Do_not_worry_about_it'); + isSafari = true; + return getPreconnectIframe().then(iframe => { + const open = sandbox.spy(XMLHttpRequest.prototype, 'open'); + const send = sandbox.spy(XMLHttpRequest.prototype, 'send'); + preconnect.url('https://s.preconnect.com/foo/bar'); + preconnect.url('https://s.preconnect.com/other'); + preconnect.url(javascriptUrlPrefix + ':alert()'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://s.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://s.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=prefetch]')) + .to.have.length(0); + expect(open.callCount).to.equal(1); + expect(send.callCount).to.equal(1); + expect(open.args[0][1]).to.include( + 'https://s.preconnect.com/amp_preconnect_polyfill_404_or' + + '_other_error_expected._Do_not_worry_about_it'); + }); }); it('should cleanup', () => { - preconnect.url('https://c.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(9000); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(1000); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(0); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://c.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(9000); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(1000); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(0); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + }); }); it('should preconnect to 2 different origins', () => { - preconnect.url('https://d.preconnect.com/foo/bar'); - // Different origin - preconnect.url('https://e.preconnect.com/other'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(2); - expect(document.querySelectorAll('link[rel=dns-prefetch]')[0].href) - .to.equal('https://d.preconnect.com/'); - expect(document.querySelectorAll('link[rel=dns-prefetch]')[1].href) - .to.equal('https://e.preconnect.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(2); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://d.preconnect.com/foo/bar'); + // Different origin + preconnect.url('https://e.preconnect.com/other'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(2); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')[0].href) + .to.equal('https://d.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')[1].href) + .to.equal('https://e.preconnect.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(2); + }); }); it('should timeout preconnects', () => { - preconnect.url('https://x.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(9000); - preconnect.url('https://x.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(1000); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); - // After timeout preconnect creates a new tag. - preconnect.url('https://x.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://x.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(9000); + preconnect.url('https://x.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(1000); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + // After timeout preconnect creates a new tag. + preconnect.url('https://x.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + }); }); it('should timeout preconnects longer with active connect', () => { - preconnect.url('https://y.preconnect.com/foo/bar', - /* opt_alsoConnecting */ true); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - clock.tick(10000); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); - preconnect.url('https://y.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(0); - clock.tick(180 * 1000); - preconnect.url('https://y.preconnect.com/foo/bar'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); + return getPreconnectIframe().then(iframe => { + preconnect.url('https://y.preconnect.com/foo/bar', + /* opt_alsoConnecting */ true); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + clock.tick(10000); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + preconnect.url('https://y.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(0); + clock.tick(180 * 1000); + preconnect.url('https://y.preconnect.com/foo/bar'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + }); }); it('should prefetch', () => { - preconnect.prefetch('https://a.prefetch.com/foo/bar'); - preconnect.prefetch('https://a.prefetch.com/foo/bar'); - preconnect.prefetch('https://a.prefetch.com/other'); - preconnect.prefetch(javascriptUrlPrefix + ':alert()'); - // Also preconnects. - expect(document.querySelectorAll('link[rel=dns-prefetch]')) - .to.have.length(1); - expect(document.querySelector('link[rel=dns-prefetch]').href) - .to.equal('https://a.prefetch.com/'); - expect(document.querySelectorAll('link[rel=preconnect]')) - .to.have.length(1); - expect(document.querySelector('link[rel=preconnect]').href) - .to.equal('https://a.prefetch.com/'); - // Actual prefetch - const fetches = document.querySelectorAll( - 'link[rel=prefetch]'); - expect(fetches).to.have.length(2); - expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); - expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch]'); + expect(fetches).to.have.length(2); + expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + }); + }); + + it('should add links (prefetch or preload)', () => { + // Don't stub preload support allow the test to run through the browser + // default regardless of support or not. + preloadSupported = undefined; + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar', 'script'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other', 'style'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch],link[rel=preload]'); + expect(fetches).to.have.length(2); + expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(fetches[0].getAttribute('as')).to.equal('script'); + expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + expect(fetches[1].getAttribute('as')).to.equal('style'); + }); + }); + + it('should prefetch when preload is not supported', () => { + preloadSupported = false; + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar', 'script'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other', 'style'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + + const preloads = iframe.doc.querySelectorAll( + 'link[rel=preload]'); + expect(preloads).to.have.length(0); + + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch]'); + expect(fetches).to.have.length(2); + expect(fetches[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(fetches[0].getAttribute('as')).to.equal('script'); + expect(fetches[1].href).to.equal('https://a.prefetch.com/other'); + expect(fetches[1].getAttribute('as')).to.equal('style'); + }); + }); + + it('should preload when supported', () => { + preloadSupported = true; + return getPreconnectIframe().then(iframe => { + preconnect.prefetch('https://a.prefetch.com/foo/bar', 'script'); + preconnect.prefetch('https://a.prefetch.com/foo/bar'); + preconnect.prefetch('https://a.prefetch.com/other', 'style'); + preconnect.prefetch(javascriptUrlPrefix + ':alert()'); + // Also preconnects. + expect(iframe.doc.querySelectorAll('link[rel=dns-prefetch]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=dns-prefetch]').href) + .to.equal('https://a.prefetch.com/'); + expect(iframe.doc.querySelectorAll('link[rel=preconnect]')) + .to.have.length(1); + expect(iframe.doc.querySelector('link[rel=preconnect]').href) + .to.equal('https://a.prefetch.com/'); + // Actual prefetch + const fetches = iframe.doc.querySelectorAll( + 'link[rel=prefetch]'); + expect(fetches).to.have.length(0); + const preloads = iframe.doc.querySelectorAll( + 'link[rel=preload]'); + expect(preloads).to.have.length(2); + expect(preloads[0].href).to.equal('https://a.prefetch.com/foo/bar'); + expect(preloads[0].getAttribute('as')).to.equal('script'); + expect(preloads[1].href).to.equal('https://a.prefetch.com/other'); + expect(preloads[1].getAttribute('as')).to.equal('style'); + }); }); });