forked from ampproject/amphtml
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpreconnect.js
185 lines (172 loc) · 6.14 KB
/
preconnect.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/**
* 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.
*/
/**
* @fileoverview Provides a services to preconnect to a url to warm up the
* connection before the real request can be made.
*/
import {getService} from './service';
import {parseUrl} from './url';
import {timer} from './timer';
import {platformFor} from './platform';
const ACTIVE_CONNECTION_TIMEOUT_MS = 180 * 1000;
const PRECONNECT_TIMEOUT_MS = 10 * 1000;
class Preconnect {
/**
* @param {!Window} win
*/
constructor(win) {
/** @private @const {!Element} */
this.head_ = win.document.head;
/**
* Origin we've preconnected to and when that connection
* expires as a timestamp in MS.
* @private @const {!Object<string, number>}
*/
this.origins_ = {};
/**
* Urls we've prefetched.
* @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. Always also does a dns-prefetch because
* browser support for that is better.
* @param {string} url
* @param {boolean=} opt_alsoConnecting Set this flag if you also just
* did or are about to connect to this host. This is for the case
* where preconnect is issued immediate before or after actual connect
* and preconnect is used to flatten a deep HTTP request chain.
* E.g. when you preconnect to a host that an embed will connect to
* when it is more fully rendered, you already know that the connection
* will be used very soon.
*/
url(url, opt_alsoConnecting) {
if (!this.isInterestingUrl_(url)) {
return;
}
const origin = parseUrl(url).origin;
const now = timer.now();
const lastPreconnectTimeout = this.origins_[origin];
if (lastPreconnectTimeout && now < lastPreconnectTimeout) {
if (opt_alsoConnecting) {
this.origins_[origin] = now + ACTIVE_CONNECTION_TIMEOUT_MS ;
}
return;
}
// If we are about to use the connection, don't re-preconnect for
// 180 seconds.
const timeout = opt_alsoConnecting
? ACTIVE_CONNECTION_TIMEOUT_MS
: PRECONNECT_TIMEOUT_MS;
this.origins_[origin] = now + timeout;
const dns = document.createElement('link');
dns.setAttribute('rel', 'dns-prefetch');
dns.setAttribute('href', origin);
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(() => {
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, /* opt_alsoConnecting */ true);
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;
}
/**
* 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;
}
// Don't attempt to preconnect for ACTIVE_CONNECTION_TIMEOUT_MS since
// we effectively create an active connection.
// TODO(@cramforce): Confirm actual http2 timeout in Safari.
this.origins_[origin] = timer.now() + ACTIVE_CONNECTION_TIMEOUT_MS;
const url = origin +
'/amp_preconnect_polyfill_404_or_other_error_expected.' +
'_Do_not_worry_about_it?' + 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();
}
}
/**
* @param {!Window} window
* @return {!Preconnect}
*/
export function preconnectFor(window) {
return getService(window, 'preconnect', () => {
return new Preconnect(window);
});
};