hyperinstant 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -17
- package/dist/hyperinstant.js +102 -331
- package/dist/hyperinstant.min.js +1 -1
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
# HyperInstant v1.
|
|
1
|
+
# HyperInstant v1.1.1 (Legacy-fast)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- **
|
|
5
|
-
- **
|
|
6
|
-
- **Safer auto-deny** (less over-blocking)
|
|
7
|
-
- **Optional auto-tuning** (OFF by default)
|
|
3
|
+
Same ultra-light behavior as 0.1.0 / 1.1.0, with **practical fixes**:
|
|
4
|
+
- **Same-site support**: allows `www.` / non-`www` and subdomains by default
|
|
5
|
+
- **More reliable events**: uses `pointerover` + capture listeners
|
|
8
6
|
|
|
9
7
|
## CDN
|
|
10
8
|
```html
|
|
11
9
|
<script>
|
|
12
10
|
window.HyperInstant = {
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
strategy: "auto",
|
|
12
|
+
hoverDelay: 65,
|
|
13
|
+
maxPrefetch: 6,
|
|
14
|
+
maxPrerender: 2,
|
|
15
|
+
sameSite: true, // allow www/non-www + subdomains (default true)
|
|
16
|
+
deny: ["/login","/logout","/customer/info","/cart","/shoppingcart","/checkout","addproducttocart"],
|
|
15
17
|
debug: false
|
|
16
18
|
};
|
|
17
19
|
</script>
|
|
18
|
-
<script src="https://unpkg.com/hyperinstant@1.
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## “Very fast” preset (nopCommerce)
|
|
22
|
-
```js
|
|
23
|
-
mode: "aggressive",
|
|
24
|
-
adapter: "nopcommerce",
|
|
25
|
-
hoverDelay: 35,
|
|
26
|
-
enablePrerender: false
|
|
20
|
+
<script src="https://unpkg.com/hyperinstant@1.1.1/dist/hyperinstant.min.js" defer></script>
|
|
27
21
|
```
|
package/dist/hyperinstant.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
/*! HyperInstant v1.
|
|
1
|
+
/*! HyperInstant v1.1.1 (legacy-fast) | MIT | Putia Web */
|
|
2
2
|
(function () {
|
|
3
3
|
if (window.__HYPERINSTANT_LOADED__) return;
|
|
4
4
|
window.__HYPERINSTANT_LOADED__ = true;
|
|
5
5
|
|
|
6
|
-
var
|
|
7
|
-
var VERSION = "1.
|
|
6
|
+
var cfg = window.HyperInstant || {};
|
|
7
|
+
var VERSION = "1.1.1";
|
|
8
|
+
|
|
9
|
+
var strategy = (cfg.strategy || "auto").toLowerCase();
|
|
10
|
+
var hoverDelay = (typeof cfg.hoverDelay === "number") ? cfg.hoverDelay : 65;
|
|
11
|
+
var maxPrefetch = (typeof cfg.maxPrefetch === "number") ? cfg.maxPrefetch : 6;
|
|
12
|
+
var maxPrerender = (typeof cfg.maxPrerender === "number") ? cfg.maxPrerender : 2;
|
|
13
|
+
var deny = Array.isArray(cfg.deny) ? cfg.deny : [];
|
|
14
|
+
var debug = !!cfg.debug;
|
|
15
|
+
var sameSite = (cfg.sameSite === undefined) ? true : !!cfg.sameSite;
|
|
16
|
+
|
|
17
|
+
var prefetched = new Set();
|
|
18
|
+
var prerendered = new Set();
|
|
19
|
+
var inflight = new Set();
|
|
20
|
+
var usedPrefetch = 0;
|
|
21
|
+
var usedPrerender = 0;
|
|
8
22
|
|
|
9
|
-
function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }
|
|
10
|
-
function isSameOrigin(url) {
|
|
11
|
-
try { return new URL(url, location.href).origin === location.origin; }
|
|
12
|
-
catch (e) { return false; }
|
|
13
|
-
}
|
|
14
|
-
function normalizeUrl(url) {
|
|
15
|
-
try { var u = new URL(url, location.href); u.hash = ""; return u.toString(); }
|
|
16
|
-
catch (e) { return ""; }
|
|
17
|
-
}
|
|
18
23
|
function supportsPrerender() {
|
|
19
24
|
try {
|
|
20
25
|
return "relList" in HTMLLinkElement.prototype &&
|
|
@@ -23,367 +28,133 @@
|
|
|
23
28
|
HTMLLinkElement.prototype.relList.supports("prerender");
|
|
24
29
|
} catch (e) { return false; }
|
|
25
30
|
}
|
|
26
|
-
function getConn() {
|
|
27
|
-
var c = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
28
|
-
return {
|
|
29
|
-
saveData: !!(c && c.saveData),
|
|
30
|
-
effectiveType: (c && c.effectiveType) ? String(c.effectiveType) : "unknown",
|
|
31
|
-
downlink: (c && typeof c.downlink === "number") ? c.downlink : null,
|
|
32
|
-
rtt: (c && typeof c.rtt === "number") ? c.rtt : null
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function inNav(a) {
|
|
36
|
-
try { return !!a.closest("nav, header, [role='navigation'], .header, .site-header"); }
|
|
37
|
-
catch (e) { return false; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function applyModeDefaults(cfg) {
|
|
41
|
-
var mode = String(cfg.mode || "safe").toLowerCase();
|
|
42
|
-
var out = Object.assign({}, cfg);
|
|
43
|
-
|
|
44
|
-
// v1.0.2: prerender is intentionally conservative to avoid slowing pages
|
|
45
|
-
if (mode === "aggressive") {
|
|
46
|
-
out.maxPrefetch = out.maxPrefetch ?? 10;
|
|
47
|
-
out.maxPrerender = out.maxPrerender ?? 1;
|
|
48
|
-
out.hoverDelay = out.hoverDelay ?? 35;
|
|
49
|
-
} else if (mode === "balanced") {
|
|
50
|
-
out.maxPrefetch = out.maxPrefetch ?? 6;
|
|
51
|
-
out.maxPrerender = out.maxPrerender ?? 0;
|
|
52
|
-
out.hoverDelay = out.hoverDelay ?? 65;
|
|
53
|
-
} else {
|
|
54
|
-
out.maxPrefetch = out.maxPrefetch ?? 4;
|
|
55
|
-
out.maxPrerender = out.maxPrerender ?? 0;
|
|
56
|
-
out.hoverDelay = out.hoverDelay ?? 75;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
out.adapter = out.adapter ?? "auto";
|
|
60
|
-
out.observe = out.observe ?? "intent";
|
|
61
|
-
out.events = out.events ?? ["mouseover", "touchstart", "mousedown", "focusin"];
|
|
62
|
-
out.deny = out.deny ?? [];
|
|
63
|
-
out.debug = !!out.debug;
|
|
64
|
-
out.debugOverlay = !!out.debugOverlay;
|
|
65
|
-
|
|
66
|
-
// new in 1.0.2
|
|
67
|
-
out.enablePrerender = (out.enablePrerender === undefined) ? true : !!out.enablePrerender;
|
|
68
|
-
out.autoTune = !!out.autoTune; // OFF by default
|
|
69
|
-
out.maxQueue = clamp(out.maxQueue ?? 60, 10, 200);
|
|
70
31
|
|
|
71
|
-
|
|
32
|
+
function normalizeUrl(url) {
|
|
33
|
+
try {
|
|
34
|
+
var u = new URL(url, location.href);
|
|
35
|
+
u.hash = "";
|
|
36
|
+
return u.toString();
|
|
37
|
+
} catch (e) { return ""; }
|
|
72
38
|
}
|
|
73
39
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
var deny = new Set([].concat(cfg.deny || []));
|
|
78
|
-
["/cart", "/checkout", "/account", "/account/login", "/account/register", "/account/logout", "/search",
|
|
79
|
-
"cart/add", "cart/change", "cart/update", "variant=", "sections="].forEach(function (x) { deny.add(x); });
|
|
80
|
-
return Object.assign({}, cfg, { deny: Array.from(deny) });
|
|
40
|
+
function baseHost(hostname) {
|
|
41
|
+
hostname = String(hostname || "").toLowerCase();
|
|
42
|
+
return hostname.replace(/^www\./, "");
|
|
81
43
|
}
|
|
82
44
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
45
|
+
function isAllowedOrigin(url) {
|
|
46
|
+
try {
|
|
47
|
+
var u = new URL(url, location.href);
|
|
48
|
+
if (u.origin === location.origin) return true;
|
|
88
49
|
|
|
89
|
-
|
|
90
|
-
var deny = new Set([].concat(cfg.deny || []));
|
|
91
|
-
["/shoppingcart", "/checkout", "/login", "/register", "/logout", "/customer", "/wishlist", "/compareproducts",
|
|
92
|
-
"/search", "?q=", "addproducttocart", "updateshoppingcart", "updatecart", "deleteitem", "returnurl=", "token=", "ajax", "flyout"].forEach(function (x) { deny.add(x); });
|
|
93
|
-
return Object.assign({}, cfg, { deny: Array.from(deny) });
|
|
94
|
-
}
|
|
50
|
+
if (!sameSite) return false;
|
|
95
51
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
52
|
+
// allow www/non-www and subdomains for same base host
|
|
53
|
+
var here = baseHost(location.hostname);
|
|
54
|
+
var there = baseHost(u.hostname);
|
|
55
|
+
if (there === here) return true;
|
|
56
|
+
if (there.endsWith("." + here)) return true;
|
|
57
|
+
if (here.endsWith("." + there)) return true; // just in case
|
|
102
58
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
59
|
+
return false;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
107
63
|
}
|
|
108
64
|
|
|
109
|
-
|
|
110
|
-
var AUTO_PATH_PREFIX = ["/checkout", "/cart", "/shoppingcart", "/account", "/my-account", "/logout", "/login", "/register"];
|
|
111
|
-
var AUTO_PARAMS = ["add", "remove", "update", "change", "delete", "token", "variant", "sections", "returnurl", "redirect"];
|
|
112
|
-
var AUTO_KEYWORDS = ["add-to-cart", "addtocart", "addproducttocart", "wc-ajax", "admin-ajax", "sections=", "variant="];
|
|
113
|
-
|
|
114
|
-
function shouldDeny(url, cfg) {
|
|
65
|
+
function isDenied(url) {
|
|
115
66
|
if (!url) return true;
|
|
116
|
-
if (!
|
|
67
|
+
if (!isAllowedOrigin(url)) return true;
|
|
117
68
|
|
|
118
69
|
var u = String(url).toLowerCase();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
var r = denyList[i];
|
|
123
|
-
if (r && u.includes(String(r).toLowerCase())) return true;
|
|
70
|
+
for (var i = 0; i < deny.length; i++) {
|
|
71
|
+
var d = deny[i];
|
|
72
|
+
if (d && u.indexOf(String(d).toLowerCase()) !== -1) return true;
|
|
124
73
|
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
var p = new URL(url, location.href).pathname.toLowerCase();
|
|
128
|
-
for (var j = 0; j < AUTO_PATH_PREFIX.length; j++) {
|
|
129
|
-
var ap = AUTO_PATH_PREFIX[j];
|
|
130
|
-
if (p === ap || p.startsWith(ap + "/")) return true;
|
|
131
|
-
}
|
|
132
|
-
} catch (e) { }
|
|
133
|
-
|
|
134
|
-
for (var k = 0; k < AUTO_KEYWORDS.length; k++) if (u.includes(AUTO_KEYWORDS[k])) return true;
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
var uu = new URL(url, location.href);
|
|
138
|
-
var sp = uu.searchParams;
|
|
139
|
-
for (var q = 0; q < AUTO_PARAMS.length; q++) if (sp.has(AUTO_PARAMS[q])) return true;
|
|
140
|
-
} catch (e) { }
|
|
141
|
-
|
|
142
74
|
return false;
|
|
143
75
|
}
|
|
144
76
|
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.prefetched = 0;
|
|
151
|
-
this.prerendered = 0;
|
|
152
|
-
this.clicked = 0;
|
|
153
|
-
this.hit = 0;
|
|
154
|
-
this._loaded = new Set();
|
|
155
|
-
}
|
|
156
|
-
Stats.prototype.isLoaded = function (url) { return this._loaded.has(url); };
|
|
157
|
-
Stats.prototype.markLoaded = function (url, type) {
|
|
158
|
-
if (this._loaded.has(url)) return;
|
|
159
|
-
this._loaded.add(url);
|
|
160
|
-
if (type === "prerender") this.prerendered++; else this.prefetched++;
|
|
161
|
-
};
|
|
162
|
-
Stats.prototype.markClick = function (url) { this.clicked++; if (this._loaded.has(url)) this.hit++; };
|
|
163
|
-
Object.defineProperty(Stats.prototype, "hitRate", { get: function () { var d = this.prefetched + this.prerendered; return d ? (this.hit / d) : 0; } });
|
|
164
|
-
Stats.prototype.snapshot = function () {
|
|
165
|
-
return {
|
|
166
|
-
version: VERSION,
|
|
167
|
-
uptimeSec: Math.round((Date.now() - this.startedAt) / 1000),
|
|
168
|
-
seen: this.seen,
|
|
169
|
-
denied: this.denied,
|
|
170
|
-
enqueued: this.enqueued,
|
|
171
|
-
prefetched: this.prefetched,
|
|
172
|
-
prerendered: this.prerendered,
|
|
173
|
-
clicked: this.clicked,
|
|
174
|
-
hit: this.hit,
|
|
175
|
-
hitRate: Number(this.hitRate.toFixed(3))
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
function createScheduler(cfg, stats) {
|
|
180
|
-
var q = [];
|
|
181
|
-
var qSet = new Set();
|
|
182
|
-
var inFlight = new Set();
|
|
183
|
-
var usedPrefetch = 0;
|
|
184
|
-
var usedPrerender = 0;
|
|
185
|
-
var prerenderSupported = supportsPrerender();
|
|
186
|
-
|
|
187
|
-
function budgets() {
|
|
188
|
-
var mp = cfg.maxPrefetch;
|
|
189
|
-
var mr = cfg.maxPrerender;
|
|
190
|
-
|
|
191
|
-
if (!cfg.enablePrerender || !prerenderSupported) mr = 0;
|
|
192
|
-
|
|
193
|
-
var ci = getConn();
|
|
194
|
-
if (ci.saveData) { mr = 0; mp = Math.min(mp, 2); }
|
|
195
|
-
if (String(ci.effectiveType).includes("2g")) { mr = 0; mp = Math.min(mp, 2); }
|
|
196
|
-
if (String(ci.effectiveType).includes("3g")) { mr = 0; mp = Math.min(mp, 4); }
|
|
197
|
-
if (typeof ci.rtt === "number" && ci.rtt > 350) { mr = 0; mp = Math.min(mp, 3); }
|
|
198
|
-
if (typeof ci.downlink === "number" && ci.downlink < 1.2) { mr = 0; mp = Math.min(mp, 3); }
|
|
199
|
-
|
|
200
|
-
mp = clamp(mp, 0, 12);
|
|
201
|
-
mr = clamp(mr, 0, 3);
|
|
202
|
-
|
|
203
|
-
return { mp: mp, mr: mr };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function enqueue(rawUrl, score, signal) {
|
|
207
|
-
var url = normalizeUrl(rawUrl);
|
|
208
|
-
if (!url) return;
|
|
209
|
-
if (stats.isLoaded(url) || inFlight.has(url) || qSet.has(url)) return;
|
|
210
|
-
|
|
211
|
-
if (q.length >= cfg.maxQueue) {
|
|
212
|
-
var minIdx = 0;
|
|
213
|
-
for (var i = 1; i < q.length; i++) if (q[i].score < q[minIdx].score) minIdx = i;
|
|
214
|
-
var dropped = q.splice(minIdx, 1)[0];
|
|
215
|
-
if (dropped) qSet.delete(dropped.url);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
stats.enqueued++;
|
|
219
|
-
q.push({ url: url, score: score, signal: signal, t: Date.now() });
|
|
220
|
-
qSet.add(url);
|
|
221
|
-
pump();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function pump() {
|
|
225
|
-
var b = budgets();
|
|
226
|
-
|
|
227
|
-
function takeBest() {
|
|
228
|
-
if (!q.length) return null;
|
|
229
|
-
var bestIdx = 0;
|
|
230
|
-
for (var i = 1; i < q.length; i++) if (q[i].score > q[bestIdx].score) bestIdx = i;
|
|
231
|
-
var item = q.splice(bestIdx, 1)[0];
|
|
232
|
-
if (item) qSet.delete(item.url);
|
|
233
|
-
return item;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
while (q.length) {
|
|
237
|
-
var item = takeBest();
|
|
238
|
-
if (!item) break;
|
|
239
|
-
|
|
240
|
-
var url = item.url;
|
|
241
|
-
var score = item.score;
|
|
242
|
-
var signal = item.signal;
|
|
243
|
-
|
|
244
|
-
if (stats.isLoaded(url) || inFlight.has(url)) continue;
|
|
77
|
+
function enqueue(rawUrl) {
|
|
78
|
+
var url = normalizeUrl(rawUrl);
|
|
79
|
+
if (!url) return;
|
|
80
|
+
if (isDenied(url)) return;
|
|
81
|
+
if (prefetched.has(url) || prerendered.has(url) || inflight.has(url)) return;
|
|
245
82
|
|
|
246
|
-
|
|
247
|
-
|
|
83
|
+
var canPrerender = supportsPrerender() && (usedPrerender < maxPrerender);
|
|
84
|
+
var canPrefetch = (usedPrefetch < maxPrefetch);
|
|
248
85
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
86
|
+
var rel = "prefetch";
|
|
87
|
+
if (strategy === "prefetch") rel = "prefetch";
|
|
88
|
+
else if (strategy === "prerender") rel = canPrerender ? "prerender" : "prefetch";
|
|
89
|
+
else rel = canPrerender ? "prerender" : "prefetch";
|
|
252
90
|
|
|
253
|
-
|
|
91
|
+
if (rel === "prefetch" && !canPrefetch) return;
|
|
92
|
+
if (rel === "prerender" && !canPrerender) rel = canPrefetch ? "prefetch" : "";
|
|
93
|
+
if (!rel) return;
|
|
254
94
|
|
|
255
|
-
|
|
256
|
-
if (type === "prerender") usedPrerender++; else usedPrefetch++;
|
|
95
|
+
inflight.add(url);
|
|
257
96
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
link.as = "document";
|
|
263
|
-
link.fetchPriority = doPrerender ? "high" : (score >= 90 ? "auto" : "low");
|
|
97
|
+
var link = document.createElement("link");
|
|
98
|
+
link.rel = rel;
|
|
99
|
+
link.href = url;
|
|
100
|
+
link.as = "document";
|
|
264
101
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
link.onerror = function () { inFlight.delete(u); };
|
|
268
|
-
})(url, type);
|
|
102
|
+
if (rel === "prerender") usedPrerender++;
|
|
103
|
+
else usedPrefetch++;
|
|
269
104
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
105
|
+
(function(u, r){
|
|
106
|
+
link.onload = function () {
|
|
107
|
+
inflight.delete(u);
|
|
108
|
+
if (r === "prerender") prerendered.add(u);
|
|
109
|
+
else prefetched.add(u);
|
|
110
|
+
if (debug) console.log("[HyperInstant]", r, u);
|
|
111
|
+
};
|
|
112
|
+
link.onerror = function () { inflight.delete(u); };
|
|
113
|
+
})(url, rel);
|
|
275
114
|
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function maybeOverlay(cfg, stats) {
|
|
280
|
-
if (!cfg.debug || !cfg.debugOverlay) return;
|
|
281
|
-
var el = document.createElement("div");
|
|
282
|
-
el.style.cssText = "position:fixed;bottom:12px;right:12px;z-index:2147483647;background:rgba(0,0,0,.75);color:#fff;padding:10px 12px;border-radius:10px;font:12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;box-shadow:0 10px 30px rgba(0,0,0,.35);max-width:280px";
|
|
283
|
-
function render() {
|
|
284
|
-
var s = stats.snapshot();
|
|
285
|
-
el.innerHTML =
|
|
286
|
-
"<div style='font-weight:700;margin-bottom:6px'>HyperInstant " + s.version + "</div>" +
|
|
287
|
-
"<div>Loaded: <b>" + (s.prefetched + s.prerendered) + "</b> (P:" + s.prefetched + " R:" + s.prerendered + ")</div>" +
|
|
288
|
-
"<div>Hit-rate: <b>" + Math.round(s.hitRate * 100) + "%</b></div>" +
|
|
289
|
-
"<div>HoverDelay: <b>" + cfg.hoverDelay + "ms</b></div>" +
|
|
290
|
-
"<div style='opacity:.8;margin-top:6px'>Clicks: " + s.clicked + " • Seen: " + s.seen + "</div>";
|
|
291
|
-
}
|
|
292
|
-
render();
|
|
293
|
-
document.addEventListener("DOMContentLoaded", function () { document.body.appendChild(el); });
|
|
294
|
-
setInterval(render, 1200);
|
|
115
|
+
document.head.appendChild(link);
|
|
295
116
|
}
|
|
296
117
|
|
|
297
|
-
|
|
118
|
+
var t = null;
|
|
119
|
+
function getAnchor(target) {
|
|
298
120
|
var a = target && target.closest ? target.closest("a[href]") : null;
|
|
299
121
|
if (!a || !a.href) return null;
|
|
300
|
-
|
|
301
|
-
var rel = String(a.getAttribute("rel") || "").toLowerCase();
|
|
302
|
-
if (rel.includes("nofollow")) return null;
|
|
303
|
-
if (a.getAttribute("data-no-instant") !== null) return null;
|
|
304
|
-
if (a.getAttribute("data-hyperinstant") === "off") return null;
|
|
305
122
|
if (a.target && a.target !== "_self") return null;
|
|
306
|
-
|
|
123
|
+
if (a.getAttribute("data-no-instant") !== null) return null;
|
|
307
124
|
return a;
|
|
308
125
|
}
|
|
309
126
|
|
|
310
|
-
function
|
|
311
|
-
var
|
|
312
|
-
if (
|
|
313
|
-
else if (signal === "touchstart") s = 118;
|
|
314
|
-
else if (signal === "focusin") s = 98;
|
|
315
|
-
else if (signal === "mouseover") s = 82;
|
|
127
|
+
function onHover(e) {
|
|
128
|
+
var a = getAnchor(e.target);
|
|
129
|
+
if (!a) return;
|
|
316
130
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return Math.min(130, s);
|
|
131
|
+
clearTimeout(t);
|
|
132
|
+
t = setTimeout(function () { enqueue(a.href); }, hoverDelay);
|
|
320
133
|
}
|
|
321
134
|
|
|
322
|
-
function
|
|
323
|
-
var
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (total < 16) return;
|
|
327
|
-
var hr = stats.hitRate;
|
|
328
|
-
|
|
329
|
-
if (hr >= 0.40) cfg.hoverDelay = clamp(cfg.hoverDelay - 8, 20, 120);
|
|
330
|
-
else if (hr <= 0.12) cfg.hoverDelay = clamp(cfg.hoverDelay + 10, 25, 180);
|
|
331
|
-
}
|
|
332
|
-
return { onEnqueue: function () { intentCount++; if (cfg.autoTune && intentCount % 25 === 0) maybeTune(); }, onClick: function () { } };
|
|
135
|
+
function onImmediate(e) {
|
|
136
|
+
var a = getAnchor(e.target);
|
|
137
|
+
if (!a) return;
|
|
138
|
+
enqueue(a.href);
|
|
333
139
|
}
|
|
334
140
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!a) return;
|
|
341
|
-
|
|
342
|
-
var url = a.href;
|
|
343
|
-
stats.seen++;
|
|
344
|
-
|
|
345
|
-
if (shouldDeny(url, cfg)) { stats.denied++; return; }
|
|
141
|
+
// More reliable than mouseover in many modern themes
|
|
142
|
+
document.addEventListener("pointerover", onHover, { passive: true, capture: true });
|
|
143
|
+
document.addEventListener("touchstart", onImmediate, { passive: true, capture: true });
|
|
144
|
+
document.addEventListener("mousedown", onImmediate, { passive: true, capture: true });
|
|
145
|
+
document.addEventListener("focusin", onImmediate, { passive: true, capture: true });
|
|
346
146
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
var delay = (sig === "mouseover") ? cfg.hoverDelay : 0;
|
|
352
|
-
|
|
353
|
-
hoverTimer = setTimeout(function () {
|
|
354
|
-
scheduler.enqueue(url, sc, sig);
|
|
355
|
-
tuner.onEnqueue();
|
|
356
|
-
}, delay);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function onClick(e) {
|
|
360
|
-
var a = eligibleLink(e.target);
|
|
361
|
-
if (!a) return;
|
|
362
|
-
stats.markClick(normalizeUrl(a.href));
|
|
363
|
-
tuner.onClick();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
(cfg.events || ["mouseover", "touchstart", "mousedown", "focusin"]).forEach(function (evt) {
|
|
367
|
-
document.addEventListener(evt, onIntent, { passive: true, capture: true });
|
|
368
|
-
});
|
|
369
|
-
document.addEventListener("click", onClick, { passive: true, capture: true });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
var cfg = applyModeDefaults(userCfg);
|
|
373
|
-
cfg = detectAdapter(cfg.adapter)(cfg);
|
|
374
|
-
|
|
375
|
-
var stats = new Stats();
|
|
376
|
-
var tuner = createTuner(cfg, stats);
|
|
377
|
-
var scheduler = createScheduler(cfg, stats);
|
|
147
|
+
window.addEventListener("pageshow", function () {
|
|
148
|
+
usedPrefetch = 0;
|
|
149
|
+
usedPrerender = 0;
|
|
150
|
+
});
|
|
378
151
|
|
|
379
152
|
window.HyperInstant = window.HyperInstant || {};
|
|
380
153
|
window.HyperInstant.version = VERSION;
|
|
381
154
|
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
155
|
+
if (debug) {
|
|
156
|
+
console.log("[HyperInstant] v"+VERSION+" legacy-fast loaded", {
|
|
157
|
+
strategy: strategy, hoverDelay: hoverDelay, maxPrefetch: maxPrefetch, maxPrerender: maxPrerender, sameSite: sameSite
|
|
158
|
+
});
|
|
385
159
|
}
|
|
386
|
-
|
|
387
|
-
start(cfg, scheduler, stats, tuner);
|
|
388
|
-
maybeOverlay(cfg, stats);
|
|
389
160
|
})();
|
package/dist/hyperinstant.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
/*! HyperInstant v1.0.2 | MIT | Putia Web */ (function (){if (window.__HYPERINSTANT_LOADED__) return; window.__HYPERINSTANT_LOADED__ = true; var userCfg = window.HyperInstant ||{}; var VERSION = "1.0.2"; function clamp(n, a, b){return Math.max(a, Math.min(b, n));} function isSameOrigin(url){try{return new URL(url, location.href).origin === location.origin;} catch (e){return false;}} function normalizeUrl(url){try{var u = new URL(url, location.href); u.hash = ""; return u.toString();} catch (e){return "";}} function supportsPrerender(){try{return "relList" in HTMLLinkElement.prototype && HTMLLinkElement.prototype.relList && HTMLLinkElement.prototype.relList.supports && HTMLLinkElement.prototype.relList.supports("prerender");} catch (e){return false;}} function getConn(){var c = navigator.connection || navigator.mozConnection || navigator.webkitConnection; return{saveData: !!(c && c.saveData), effectiveType: (c && c.effectiveType) ? String(c.effectiveType) : "unknown", downlink: (c && typeof c.downlink === "number") ? c.downlink : null, rtt: (c && typeof c.rtt === "number") ? c.rtt : null};} function inNav(a){try{return !!a.closest("nav, header, [role='navigation'], .header, .site-header");} catch (e){return false;}} function applyModeDefaults(cfg){var mode = String(cfg.mode || "safe").toLowerCase(); var out = Object.assign({}, cfg); // v1.0.2: prerender is intentionally conservative to avoid slowing pages if (mode === "aggressive"){out.maxPrefetch = out.maxPrefetch ?? 10; out.maxPrerender = out.maxPrerender ?? 1; out.hoverDelay = out.hoverDelay ?? 35;} else if (mode === "balanced"){out.maxPrefetch = out.maxPrefetch ?? 6; out.maxPrerender = out.maxPrerender ?? 0; out.hoverDelay = out.hoverDelay ?? 65;} else{out.maxPrefetch = out.maxPrefetch ?? 4; out.maxPrerender = out.maxPrerender ?? 0; out.hoverDelay = out.hoverDelay ?? 75;} out.adapter = out.adapter ?? "auto"; out.observe = out.observe ?? "intent"; out.events = out.events ?? ["mouseover", "touchstart", "mousedown", "focusin"]; out.deny = out.deny ?? []; out.debug = !!out.debug; out.debugOverlay = !!out.debugOverlay; // new in 1.0.2 out.enablePrerender = (out.enablePrerender === undefined) ? true : !!out.enablePrerender; out.autoTune = !!out.autoTune; // OFF by default out.maxQueue = clamp(out.maxQueue ?? 60, 10, 200); return out;} function hasMeta(name){return !!document.querySelector('meta[name="' + name + '"]');} function shopifyAdapter(cfg){var deny = new Set([].concat(cfg.deny || [])); ["/cart", "/checkout", "/account", "/account/login", "/account/register", "/account/logout", "/search", "cart/add", "cart/change", "cart/update", "variant=", "sections="].forEach(function (x){deny.add(x);}); return Object.assign({}, cfg,{deny: Array.from(deny)});} function wooAdapter(cfg){var deny = new Set([].concat(cfg.deny || [])); ["/cart", "/checkout", "/my-account", "add-to-cart", "wc-ajax", "remove_item", "update_cart", "apply_coupon"].forEach(function (x){deny.add(x);}); return Object.assign({}, cfg,{deny: Array.from(deny)});} function nopAdapter(cfg){var deny = new Set([].concat(cfg.deny || [])); ["/shoppingcart", "/checkout", "/login", "/register", "/logout", "/customer", "/wishlist", "/compareproducts", "/search", "?q=", "addproducttocart", "updateshoppingcart", "updatecart", "deleteitem", "returnurl=", "token=", "ajax", "flyout"].forEach(function (x){deny.add(x);}); return Object.assign({}, cfg,{deny: Array.from(deny)});} function detectAdapter(name){name = String(name || "auto").toLowerCase(); if (name === "shopify") return shopifyAdapter; if (name === "woocommerce") return wooAdapter; if (name === "nopcommerce") return nopAdapter; if (name === "generic") return function (c){return c;}; try{if (window.Shopify || hasMeta("shopify-digital-wallet") || hasMeta("shopify-checkout-api-token")) return shopifyAdapter;} catch (e){} try{if (window.wc_add_to_cart_params || window.wc_cart_fragments_params || (document.body && String(document.body.className || "").includes("woocommerce"))) return wooAdapter;} catch (e){} try{if (window.nopCommerce || document.querySelector("a[href*='shoppingcart']") || document.querySelector("form[action*='addproducttocart']")) return nopAdapter;} catch (e){} return function (c){return c;};} // Safer auto-deny (avoid broad keywords like "order" that can hit benign pages) var AUTO_PATH_PREFIX = ["/checkout", "/cart", "/shoppingcart", "/account", "/my-account", "/logout", "/login", "/register"]; var AUTO_PARAMS = ["add", "remove", "update", "change", "delete", "token", "variant", "sections", "returnurl", "redirect"]; var AUTO_KEYWORDS = ["add-to-cart", "addtocart", "addproducttocart", "wc-ajax", "admin-ajax", "sections=", "variant="]; function shouldDeny(url, cfg){if (!url) return true; if (!isSameOrigin(url)) return true; var u = String(url).toLowerCase(); var denyList = cfg.deny || []; for (var i = 0; i < denyList.length; i++){var r = denyList[i]; if (r && u.includes(String(r).toLowerCase())) return true;} try{var p = new URL(url, location.href).pathname.toLowerCase(); for (var j = 0; j < AUTO_PATH_PREFIX.length; j++){var ap = AUTO_PATH_PREFIX[j]; if (p === ap || p.startsWith(ap + "/")) return true;}} catch (e){} for (var k = 0; k < AUTO_KEYWORDS.length; k++) if (u.includes(AUTO_KEYWORDS[k])) return true; try{var uu = new URL(url, location.href); var sp = uu.searchParams; for (var q = 0; q < AUTO_PARAMS.length; q++) if (sp.has(AUTO_PARAMS[q])) return true;} catch (e){} return false;} function Stats(){this.startedAt = Date.now(); this.seen = 0; this.denied = 0; this.enqueued = 0; this.prefetched = 0; this.prerendered = 0; this.clicked = 0; this.hit = 0; this._loaded = new Set();} Stats.prototype.isLoaded = function (url){return this._loaded.has(url);}; Stats.prototype.markLoaded = function (url, type){if (this._loaded.has(url)) return; this._loaded.add(url); if (type === "prerender") this.prerendered++; else this.prefetched++;}; Stats.prototype.markClick = function (url){this.clicked++; if (this._loaded.has(url)) this.hit++;}; Object.defineProperty(Stats.prototype, "hitRate",{get: function (){var d = this.prefetched + this.prerendered; return d ? (this.hit / d) : 0;}}); Stats.prototype.snapshot = function (){return{version: VERSION, uptimeSec: Math.round((Date.now() - this.startedAt) / 1000), seen: this.seen, denied: this.denied, enqueued: this.enqueued, prefetched: this.prefetched, prerendered: this.prerendered, clicked: this.clicked, hit: this.hit, hitRate: Number(this.hitRate.toFixed(3))};}; function createScheduler(cfg, stats){var q = []; var qSet = new Set(); var inFlight = new Set(); var usedPrefetch = 0; var usedPrerender = 0; var prerenderSupported = supportsPrerender(); function budgets(){var mp = cfg.maxPrefetch; var mr = cfg.maxPrerender; if (!cfg.enablePrerender || !prerenderSupported) mr = 0; var ci = getConn(); if (ci.saveData){mr = 0; mp = Math.min(mp, 2);} if (String(ci.effectiveType).includes("2g")){mr = 0; mp = Math.min(mp, 2);} if (String(ci.effectiveType).includes("3g")){mr = 0; mp = Math.min(mp, 4);} if (typeof ci.rtt === "number" && ci.rtt > 350){mr = 0; mp = Math.min(mp, 3);} if (typeof ci.downlink === "number" && ci.downlink < 1.2){mr = 0; mp = Math.min(mp, 3);} mp = clamp(mp, 0, 12); mr = clamp(mr, 0, 3); return{mp: mp, mr: mr};} function enqueue(rawUrl, score, signal){var url = normalizeUrl(rawUrl); if (!url) return; if (stats.isLoaded(url) || inFlight.has(url) || qSet.has(url)) return; if (q.length >= cfg.maxQueue){var minIdx = 0; for (var i = 1; i < q.length; i++) if (q[i].score < q[minIdx].score) minIdx = i; var dropped = q.splice(minIdx, 1)[0]; if (dropped) qSet.delete(dropped.url);} stats.enqueued++; q.push({url: url, score: score, signal: signal, t: Date.now()}); qSet.add(url); pump();} function pump(){var b = budgets(); function takeBest(){if (!q.length) return null; var bestIdx = 0; for (var i = 1; i < q.length; i++) if (q[i].score > q[bestIdx].score) bestIdx = i; var item = q.splice(bestIdx, 1)[0]; if (item) qSet.delete(item.url); return item;} while (q.length){var item = takeBest(); if (!item) break; var url = item.url; var score = item.score; var signal = item.signal; if (stats.isLoaded(url) || inFlight.has(url)) continue; var canPrefetch = usedPrefetch < b.mp; var canPrerender = usedPrerender < b.mr; // v1.0.2: prerender only on HIGH confidence signals (not hover) var highIntent = (signal === "mousedown" || signal === "touchstart"); var doPrerender = canPrerender && highIntent && score >= 110; if (!doPrerender && !canPrefetch) break; var type = doPrerender ? "prerender" : "prefetch"; if (type === "prerender") usedPrerender++; else usedPrefetch++; inFlight.add(url); var link = document.createElement("link"); link.rel = type; link.href = url; link.as = "document"; link.fetchPriority = doPrerender ? "high" : (score >= 90 ? "auto" : "low"); (function (u, t){link.onload = function (){inFlight.delete(u); stats.markLoaded(u, t);}; link.onerror = function (){inFlight.delete(u);};})(url, type); document.head.appendChild(link);}} window.addEventListener("pageshow", function (){usedPrefetch = 0; usedPrerender = 0;}); return{enqueue: enqueue};} function maybeOverlay(cfg, stats){if (!cfg.debug || !cfg.debugOverlay) return; var el = document.createElement("div"); el.style.cssText = "position:fixed;bottom:12px;right:12px;z-index:2147483647;background:rgba(0,0,0,.75);color:#fff;padding:10px 12px;border-radius:10px;font:12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial;box-shadow:0 10px 30px rgba(0,0,0,.35);max-width:280px"; function render(){var s = stats.snapshot(); el.innerHTML = "<div style='font-weight:700;margin-bottom:6px'>HyperInstant " + s.version + "</div>" + "<div>Loaded: <b>" + (s.prefetched + s.prerendered) + "</b> (P:" + s.prefetched + " R:" + s.prerendered + ")</div>" + "<div>Hit-rate: <b>" + Math.round(s.hitRate * 100) + "%</b></div>" + "<div>HoverDelay: <b>" + cfg.hoverDelay + "ms</b></div>" + "<div style='opacity:.8;margin-top:6px'>Clicks: " + s.clicked + " • Seen: " + s.seen + "</div>";} render(); document.addEventListener("DOMContentLoaded", function (){document.body.appendChild(el);}); setInterval(render, 1200);} function eligibleLink(target){var a = target && target.closest ? target.closest("a[href]") : null; if (!a || !a.href) return null; var rel = String(a.getAttribute("rel") || "").toLowerCase(); if (rel.includes("nofollow")) return null; if (a.getAttribute("data-no-instant") !== null) return null; if (a.getAttribute("data-hyperinstant") === "off") return null; if (a.target && a.target !== "_self") return null; return a;} function score(signal, a){var s = 60; if (signal === "mousedown") s = 120; else if (signal === "touchstart") s = 118; else if (signal === "focusin") s = 98; else if (signal === "mouseover") s = 82; if (inNav(a)) s += 8; if (String(a.getAttribute("data-hyperinstant-priority") || "").toLowerCase() === "high") s += 12; return Math.min(130, s);} function createTuner(cfg, stats){var intentCount = 0; function maybeTune(){var total = stats.prefetched + stats.prerendered; if (total < 16) return; var hr = stats.hitRate; if (hr >= 0.40) cfg.hoverDelay = clamp(cfg.hoverDelay - 8, 20, 120); else if (hr <= 0.12) cfg.hoverDelay = clamp(cfg.hoverDelay + 10, 25, 180);} return{onEnqueue: function (){intentCount++; if (cfg.autoTune && intentCount % 25 === 0) maybeTune();}, onClick: function (){}};} function start(cfg, scheduler, stats, tuner){var hoverTimer = null; function onIntent(e){var a = eligibleLink(e.target); if (!a) return; var url = a.href; stats.seen++; if (shouldDeny(url, cfg)){stats.denied++; return;} var sig = e.type; var sc = score(sig, a); clearTimeout(hoverTimer); var delay = (sig === "mouseover") ? cfg.hoverDelay : 0; hoverTimer = setTimeout(function (){scheduler.enqueue(url, sc, sig); tuner.onEnqueue();}, delay);} function onClick(e){var a = eligibleLink(e.target); if (!a) return; stats.markClick(normalizeUrl(a.href)); tuner.onClick();} (cfg.events || ["mouseover", "touchstart", "mousedown", "focusin"]).forEach(function (evt){document.addEventListener(evt, onIntent,{passive: true, capture: true});}); document.addEventListener("click", onClick,{passive: true, capture: true});} var cfg = applyModeDefaults(userCfg); cfg = detectAdapter(cfg.adapter)(cfg); var stats = new Stats(); var tuner = createTuner(cfg, stats); var scheduler = createScheduler(cfg, stats); window.HyperInstant = window.HyperInstant ||{}; window.HyperInstant.version = VERSION; if (cfg.debug){window.HyperInstantStats = stats; console.log("[HyperInstant] v" + VERSION + " loaded",{mode: cfg.mode, adapter: cfg.adapter});} start(cfg, scheduler, stats, tuner); maybeOverlay(cfg, stats);})();
|
|
1
|
+
/*! HyperInstant v1.1.1 (legacy-fast) | MIT | Putia Web */ (function (){if (window.__HYPERINSTANT_LOADED__) return; window.__HYPERINSTANT_LOADED__ = true; var cfg = window.HyperInstant ||{}; var VERSION = "1.1.1"; var strategy = (cfg.strategy || "auto").toLowerCase(); var hoverDelay = (typeof cfg.hoverDelay === "number") ? cfg.hoverDelay : 65; var maxPrefetch = (typeof cfg.maxPrefetch === "number") ? cfg.maxPrefetch : 6; var maxPrerender = (typeof cfg.maxPrerender === "number") ? cfg.maxPrerender : 2; var deny = Array.isArray(cfg.deny) ? cfg.deny : []; var debug = !!cfg.debug; var sameSite = (cfg.sameSite === undefined) ? true : !!cfg.sameSite; var prefetched = new Set(); var prerendered = new Set(); var inflight = new Set(); var usedPrefetch = 0; var usedPrerender = 0; function supportsPrerender(){try{return "relList" in HTMLLinkElement.prototype && HTMLLinkElement.prototype.relList && HTMLLinkElement.prototype.relList.supports && HTMLLinkElement.prototype.relList.supports("prerender");} catch (e){return false;}} function normalizeUrl(url){try{var u = new URL(url, location.href); u.hash = ""; return u.toString();} catch (e){return "";}} function baseHost(hostname){hostname = String(hostname || "").toLowerCase(); return hostname.replace(/^www\./, "");} function isAllowedOrigin(url){try{var u = new URL(url, location.href); if (u.origin === location.origin) return true; if (!sameSite) return false; // allow www/non-www and subdomains for same base host var here = baseHost(location.hostname); var there = baseHost(u.hostname); if (there === here) return true; if (there.endsWith("." + here)) return true; if (here.endsWith("." + there)) return true; // just in case return false;} catch (e){return false;}} function isDenied(url){if (!url) return true; if (!isAllowedOrigin(url)) return true; var u = String(url).toLowerCase(); for (var i = 0; i < deny.length; i++){var d = deny[i]; if (d && u.indexOf(String(d).toLowerCase()) !== -1) return true;} return false;} function enqueue(rawUrl){var url = normalizeUrl(rawUrl); if (!url) return; if (isDenied(url)) return; if (prefetched.has(url) || prerendered.has(url) || inflight.has(url)) return; var canPrerender = supportsPrerender() && (usedPrerender < maxPrerender); var canPrefetch = (usedPrefetch < maxPrefetch); var rel = "prefetch"; if (strategy === "prefetch") rel = "prefetch"; else if (strategy === "prerender") rel = canPrerender ? "prerender" : "prefetch"; else rel = canPrerender ? "prerender" : "prefetch"; if (rel === "prefetch" && !canPrefetch) return; if (rel === "prerender" && !canPrerender) rel = canPrefetch ? "prefetch" : ""; if (!rel) return; inflight.add(url); var link = document.createElement("link"); link.rel = rel; link.href = url; link.as = "document"; if (rel === "prerender") usedPrerender++; else usedPrefetch++; (function(u, r){link.onload = function (){inflight.delete(u); if (r === "prerender") prerendered.add(u); else prefetched.add(u); if (debug) console.log("[HyperInstant]", r, u);}; link.onerror = function (){inflight.delete(u);};})(url, rel); document.head.appendChild(link);} var t = null; function getAnchor(target){var a = target && target.closest ? target.closest("a[href]") : null; if (!a || !a.href) return null; if (a.target && a.target !== "_self") return null; if (a.getAttribute("data-no-instant") !== null) return null; return a;} function onHover(e){var a = getAnchor(e.target); if (!a) return; clearTimeout(t); t = setTimeout(function (){enqueue(a.href);}, hoverDelay);} function onImmediate(e){var a = getAnchor(e.target); if (!a) return; enqueue(a.href);} // More reliable than mouseover in many modern themes document.addEventListener("pointerover", onHover,{passive: true, capture: true}); document.addEventListener("touchstart", onImmediate,{passive: true, capture: true}); document.addEventListener("mousedown", onImmediate,{passive: true, capture: true}); document.addEventListener("focusin", onImmediate,{passive: true, capture: true}); window.addEventListener("pageshow", function (){usedPrefetch = 0; usedPrerender = 0;}); window.HyperInstant = window.HyperInstant ||{}; window.HyperInstant.version = VERSION; if (debug){console.log("[HyperInstant] v"+VERSION+" legacy-fast loaded",{strategy: strategy, hoverDelay: hoverDelay, maxPrefetch: maxPrefetch, maxPrerender: maxPrerender, sameSite: sameSite});}})();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyperinstant",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "HyperInstant legacy-fast build (0.1.0 behavior) with same-site support fixes.",
|
|
5
5
|
"main": "dist/hyperinstant.min.js",
|
|
6
6
|
"module": "dist/hyperinstant.js",
|
|
7
7
|
"files": [
|
|
@@ -13,11 +13,7 @@
|
|
|
13
13
|
"prerender",
|
|
14
14
|
"instant-page",
|
|
15
15
|
"web-performance",
|
|
16
|
-
"
|
|
17
|
-
"woocommerce",
|
|
18
|
-
"nopcommerce",
|
|
19
|
-
"adaptive",
|
|
20
|
-
"navigation"
|
|
16
|
+
"legacy-fast"
|
|
21
17
|
],
|
|
22
18
|
"author": "Putia Web",
|
|
23
19
|
"license": "MIT"
|