react-fastload 0.1.0
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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/index.cjs +1348 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +562 -0
- package/dist/index.d.ts +562 -0
- package/dist/index.js +1330 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React5 = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var React5__default = /*#__PURE__*/_interopDefault(React5);
|
|
9
|
+
|
|
10
|
+
// src/components/FastLoadProvider.tsx
|
|
11
|
+
|
|
12
|
+
// src/core/PriorityEngine.ts
|
|
13
|
+
var PRIORITY_ORDER = {
|
|
14
|
+
CRITICAL: 0,
|
|
15
|
+
HIGH: 1,
|
|
16
|
+
NORMAL: 2,
|
|
17
|
+
LOW: 3,
|
|
18
|
+
IDLE: 4
|
|
19
|
+
};
|
|
20
|
+
var PriorityEngine = class {
|
|
21
|
+
/**
|
|
22
|
+
* Lower score = loaded sooner. CRITICAL always sorts first regardless
|
|
23
|
+
* of other signals, per the "never delay explicitly critical resources"
|
|
24
|
+
* requirement.
|
|
25
|
+
*/
|
|
26
|
+
score(resource, ctx) {
|
|
27
|
+
if (resource.priority === "CRITICAL") return -Infinity;
|
|
28
|
+
let score = PRIORITY_ORDER[resource.priority] * 1e3;
|
|
29
|
+
const distance = resource.viewportDistance;
|
|
30
|
+
if (typeof distance === "number") {
|
|
31
|
+
score += Math.max(distance, 0) / 10;
|
|
32
|
+
} else {
|
|
33
|
+
score += 500;
|
|
34
|
+
}
|
|
35
|
+
if (this.isConstrainedConnection(ctx.connection)) {
|
|
36
|
+
if (resource.priority === "LOW" || resource.priority === "IDLE") {
|
|
37
|
+
score += 2e3;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return score;
|
|
41
|
+
}
|
|
42
|
+
compare(a, b, ctx) {
|
|
43
|
+
return this.score(a, ctx) - this.score(b, ctx);
|
|
44
|
+
}
|
|
45
|
+
isConstrainedConnection(connection) {
|
|
46
|
+
if (connection.saveData) return true;
|
|
47
|
+
if (!connection.supported) return false;
|
|
48
|
+
if (connection.effectiveType === "slow-2g" || connection.effectiveType === "2g") {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (typeof connection.downlink === "number" && connection.downlink > 0 && connection.downlink < 1.5) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a resource sitting at `distance` px from the viewport should become eligible. */
|
|
57
|
+
isWithinPreloadZone(distance, preloadDistance) {
|
|
58
|
+
if (typeof distance !== "number") return false;
|
|
59
|
+
return distance <= preloadDistance;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/core/ResourceRegistry.ts
|
|
64
|
+
var ResourceRegistry = class {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.resources = /* @__PURE__ */ new Map();
|
|
67
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
68
|
+
}
|
|
69
|
+
register(input) {
|
|
70
|
+
const existing = this.resources.get(input.id);
|
|
71
|
+
if (existing) {
|
|
72
|
+
return existing;
|
|
73
|
+
}
|
|
74
|
+
const record = {
|
|
75
|
+
id: input.id,
|
|
76
|
+
type: input.type,
|
|
77
|
+
priority: input.priority,
|
|
78
|
+
strategy: input.strategy,
|
|
79
|
+
state: "idle",
|
|
80
|
+
estimatedSize: input.estimatedSize,
|
|
81
|
+
meta: input.meta,
|
|
82
|
+
deferred: false,
|
|
83
|
+
prefetched: false,
|
|
84
|
+
abortController: typeof AbortController !== "undefined" ? new AbortController() : void 0,
|
|
85
|
+
timestamps: {
|
|
86
|
+
registeredAt: now()
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
this.resources.set(input.id, record);
|
|
90
|
+
this.emit();
|
|
91
|
+
return record;
|
|
92
|
+
}
|
|
93
|
+
unregister(id) {
|
|
94
|
+
if (this.resources.delete(id)) {
|
|
95
|
+
this.emit();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
get(id) {
|
|
99
|
+
return this.resources.get(id);
|
|
100
|
+
}
|
|
101
|
+
has(id) {
|
|
102
|
+
return this.resources.has(id);
|
|
103
|
+
}
|
|
104
|
+
all() {
|
|
105
|
+
return Array.from(this.resources.values());
|
|
106
|
+
}
|
|
107
|
+
byState(state) {
|
|
108
|
+
return this.all().filter((r) => r.state === state);
|
|
109
|
+
}
|
|
110
|
+
update(id, patch) {
|
|
111
|
+
const record = this.resources.get(id);
|
|
112
|
+
if (!record) return void 0;
|
|
113
|
+
const updated = {
|
|
114
|
+
...record,
|
|
115
|
+
...patch,
|
|
116
|
+
timestamps: { ...record.timestamps, ...patch.timestamps }
|
|
117
|
+
};
|
|
118
|
+
this.resources.set(id, updated);
|
|
119
|
+
this.emit();
|
|
120
|
+
return updated;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Returns the CURRENT record after the transition — not the stale
|
|
124
|
+
* pre-call reference. This matters: a caller that does
|
|
125
|
+
* `const r = registry.setState(id, "eligible")` must see `r.state ===
|
|
126
|
+
* "eligible"`, not whatever `r` looked like before this call, since
|
|
127
|
+
* `update()` replaces the Map entry with a new object rather than
|
|
128
|
+
* mutating the old one in place. (This exact mismatch was a real bug
|
|
129
|
+
* in LoadManager.register(), caught by a clean-install runtime smoke
|
|
130
|
+
* test — see CHANGELOG.) Returns `undefined` only if `id` isn't
|
|
131
|
+
* registered at all.
|
|
132
|
+
*/
|
|
133
|
+
setState(id, state) {
|
|
134
|
+
const record = this.resources.get(id);
|
|
135
|
+
if (!record) return void 0;
|
|
136
|
+
if (record.state === state) return record;
|
|
137
|
+
const timestamps = { ...record.timestamps };
|
|
138
|
+
const t = now();
|
|
139
|
+
if (state === "eligible" && !timestamps.eligibleAt) timestamps.eligibleAt = t;
|
|
140
|
+
if (state === "loading" && !timestamps.loadStartedAt) timestamps.loadStartedAt = t;
|
|
141
|
+
if ((state === "loaded" || state === "error") && !timestamps.loadEndedAt) {
|
|
142
|
+
timestamps.loadEndedAt = t;
|
|
143
|
+
}
|
|
144
|
+
return this.update(id, { state, timestamps });
|
|
145
|
+
}
|
|
146
|
+
setViewportDistance(id, distance) {
|
|
147
|
+
this.update(id, { viewportDistance: distance });
|
|
148
|
+
}
|
|
149
|
+
markDeferred(id) {
|
|
150
|
+
this.update(id, { deferred: true });
|
|
151
|
+
}
|
|
152
|
+
markPrefetched(id) {
|
|
153
|
+
this.update(id, { prefetched: true });
|
|
154
|
+
}
|
|
155
|
+
subscribe(listener) {
|
|
156
|
+
this.listeners.add(listener);
|
|
157
|
+
return () => this.listeners.delete(listener);
|
|
158
|
+
}
|
|
159
|
+
clear() {
|
|
160
|
+
this.resources.clear();
|
|
161
|
+
this.emit();
|
|
162
|
+
}
|
|
163
|
+
/** Aggregate counters used by the metrics layer. Pure derived data, no side effects. */
|
|
164
|
+
summary() {
|
|
165
|
+
var _a;
|
|
166
|
+
const all = this.all();
|
|
167
|
+
let deferredRequests = 0;
|
|
168
|
+
let bytesDeferred = 0;
|
|
169
|
+
let imagesDeferred = 0;
|
|
170
|
+
let videosDeferred = 0;
|
|
171
|
+
let audiosDeferred = 0;
|
|
172
|
+
let componentsDeferred = 0;
|
|
173
|
+
for (const r of all) {
|
|
174
|
+
if (r.deferred) {
|
|
175
|
+
deferredRequests += 1;
|
|
176
|
+
bytesDeferred += (_a = r.estimatedSize) != null ? _a : 0;
|
|
177
|
+
if (r.type === "image") imagesDeferred += 1;
|
|
178
|
+
if (r.type === "video") videosDeferred += 1;
|
|
179
|
+
if (r.type === "audio") audiosDeferred += 1;
|
|
180
|
+
if (r.type === "component") componentsDeferred += 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
totalRequests: all.length,
|
|
185
|
+
initialRequests: all.filter((r) => !r.deferred).length,
|
|
186
|
+
deferredRequests,
|
|
187
|
+
deferredResources: deferredRequests,
|
|
188
|
+
bytesDeferred,
|
|
189
|
+
imagesDeferred,
|
|
190
|
+
videosDeferred,
|
|
191
|
+
audiosDeferred,
|
|
192
|
+
componentsDeferred
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
emit() {
|
|
196
|
+
const snapshot = this.all();
|
|
197
|
+
for (const listener of this.listeners) listener(snapshot);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function now() {
|
|
201
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/core/Scheduler.ts
|
|
205
|
+
var Scheduler = class {
|
|
206
|
+
constructor(registry, priorityEngine, options = {}) {
|
|
207
|
+
this.registry = registry;
|
|
208
|
+
this.priorityEngine = priorityEngine;
|
|
209
|
+
this.activeCount = 0;
|
|
210
|
+
this.loaders = /* @__PURE__ */ new Map();
|
|
211
|
+
this.decisions = [];
|
|
212
|
+
this.maxDecisionLog = 200;
|
|
213
|
+
this.scheduled = false;
|
|
214
|
+
var _a;
|
|
215
|
+
this.concurrency = (_a = options.concurrency) != null ? _a : 4;
|
|
216
|
+
this.onDecision = options.onDecision;
|
|
217
|
+
}
|
|
218
|
+
/** Register the function used to actually load a given resource id (called once per resource). */
|
|
219
|
+
registerLoader(resourceId, loader) {
|
|
220
|
+
this.loaders.set(resourceId, loader);
|
|
221
|
+
}
|
|
222
|
+
unregisterLoader(resourceId) {
|
|
223
|
+
this.loaders.delete(resourceId);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Request a scheduling pass. Coalesced into a microtask so multiple
|
|
227
|
+
* rapid registry updates (e.g. many IntersectionObserver callbacks in
|
|
228
|
+
* one frame) only trigger a single pass.
|
|
229
|
+
*/
|
|
230
|
+
requestPass(ctx) {
|
|
231
|
+
if (this.scheduled) return;
|
|
232
|
+
this.scheduled = true;
|
|
233
|
+
queueMicrotask(() => {
|
|
234
|
+
this.scheduled = false;
|
|
235
|
+
this.runPass({ ctx });
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Synchronous scheduling pass, exposed directly for tests and advanced use. */
|
|
239
|
+
runPass({ ctx }) {
|
|
240
|
+
const eligible = this.registry.byState("eligible").slice().sort((a, b) => this.priorityEngine.compare(a, b, ctx));
|
|
241
|
+
let freeSlots = this.concurrency - this.activeCount;
|
|
242
|
+
if (freeSlots <= 0) return;
|
|
243
|
+
for (const resource of eligible) {
|
|
244
|
+
if (freeSlots <= 0) break;
|
|
245
|
+
const loader = this.loaders.get(resource.id);
|
|
246
|
+
if (!loader) continue;
|
|
247
|
+
freeSlots -= 1;
|
|
248
|
+
this.dispatch(resource, loader);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
dispatch(resource, loader) {
|
|
252
|
+
var _a;
|
|
253
|
+
this.activeCount += 1;
|
|
254
|
+
this.registry.setState(resource.id, "loading");
|
|
255
|
+
this.record({
|
|
256
|
+
resourceId: resource.id,
|
|
257
|
+
action: "load",
|
|
258
|
+
reason: `priority=${resource.priority} distance=${(_a = resource.viewportDistance) != null ? _a : "unknown"}`,
|
|
259
|
+
timestamp: nowMs()
|
|
260
|
+
});
|
|
261
|
+
loader(resource).then(() => {
|
|
262
|
+
this.registry.setState(resource.id, "loaded");
|
|
263
|
+
}).catch(() => {
|
|
264
|
+
this.registry.setState(resource.id, "error");
|
|
265
|
+
}).finally(() => {
|
|
266
|
+
this.activeCount -= 1;
|
|
267
|
+
this.unregisterLoader(resource.id);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** Explicitly defer a resource (used by observers when it leaves the preload zone again, or on slow connections). */
|
|
271
|
+
defer(resourceId, reason) {
|
|
272
|
+
this.registry.markDeferred(resourceId);
|
|
273
|
+
this.record({ resourceId, action: "defer", reason, timestamp: nowMs() });
|
|
274
|
+
}
|
|
275
|
+
prefetch(resourceId, reason) {
|
|
276
|
+
this.registry.markPrefetched(resourceId);
|
|
277
|
+
this.record({ resourceId, action: "prefetch", reason, timestamp: nowMs() });
|
|
278
|
+
}
|
|
279
|
+
getDecisionLog() {
|
|
280
|
+
return this.decisions.slice();
|
|
281
|
+
}
|
|
282
|
+
record(decision) {
|
|
283
|
+
var _a;
|
|
284
|
+
this.decisions.push(decision);
|
|
285
|
+
if (this.decisions.length > this.maxDecisionLog) {
|
|
286
|
+
this.decisions.shift();
|
|
287
|
+
}
|
|
288
|
+
(_a = this.onDecision) == null ? void 0 : _a.call(this, decision);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function nowMs() {
|
|
292
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/utils/browserSupport.ts
|
|
296
|
+
var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
297
|
+
function supportsIntersectionObserver() {
|
|
298
|
+
return isBrowser && typeof window.IntersectionObserver === "function";
|
|
299
|
+
}
|
|
300
|
+
function supportsPerformanceObserver() {
|
|
301
|
+
return isBrowser && typeof PerformanceObserver !== "undefined";
|
|
302
|
+
}
|
|
303
|
+
function supportsNavigationTiming() {
|
|
304
|
+
return isBrowser && typeof performance !== "undefined" && typeof performance.getEntriesByType === "function";
|
|
305
|
+
}
|
|
306
|
+
function supportsNetworkInformation() {
|
|
307
|
+
return isBrowser && "connection" in navigator;
|
|
308
|
+
}
|
|
309
|
+
function supportsFetchPriority() {
|
|
310
|
+
if (!isBrowser) return false;
|
|
311
|
+
const img = document.createElement("img");
|
|
312
|
+
return "fetchPriority" in img;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/observers/ViewportObserver.ts
|
|
316
|
+
var ViewportObserver = class {
|
|
317
|
+
constructor(preloadDistance = 1e3) {
|
|
318
|
+
this.callbacks = /* @__PURE__ */ new Map();
|
|
319
|
+
this.preloadDistance = preloadDistance;
|
|
320
|
+
if (supportsIntersectionObserver()) {
|
|
321
|
+
this.observer = new IntersectionObserver(
|
|
322
|
+
(entries) => this.handleEntries(entries),
|
|
323
|
+
{
|
|
324
|
+
root: null,
|
|
325
|
+
rootMargin: `${preloadDistance}px`,
|
|
326
|
+
threshold: [0, 0.01, 0.5, 1]
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
observe(element, callback) {
|
|
332
|
+
this.callbacks.set(element, callback);
|
|
333
|
+
if (!this.observer) {
|
|
334
|
+
callback(0, true);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.observer.observe(element);
|
|
338
|
+
}
|
|
339
|
+
unobserve(element) {
|
|
340
|
+
var _a;
|
|
341
|
+
this.callbacks.delete(element);
|
|
342
|
+
(_a = this.observer) == null ? void 0 : _a.unobserve(element);
|
|
343
|
+
}
|
|
344
|
+
disconnect() {
|
|
345
|
+
var _a;
|
|
346
|
+
(_a = this.observer) == null ? void 0 : _a.disconnect();
|
|
347
|
+
this.callbacks.clear();
|
|
348
|
+
}
|
|
349
|
+
handleEntries(entries) {
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
const callback = this.callbacks.get(entry.target);
|
|
352
|
+
if (!callback) continue;
|
|
353
|
+
const distance = estimateDistance(entry);
|
|
354
|
+
callback(distance, entry.isIntersecting);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function estimateDistance(entry) {
|
|
359
|
+
if (entry.isIntersecting) return 0;
|
|
360
|
+
const target = entry.boundingClientRect;
|
|
361
|
+
const root = entry.rootBounds;
|
|
362
|
+
if (!root) {
|
|
363
|
+
return Number.POSITIVE_INFINITY;
|
|
364
|
+
}
|
|
365
|
+
if (target.bottom < root.top) return root.top - target.bottom;
|
|
366
|
+
if (target.top > root.bottom) return target.top - root.bottom;
|
|
367
|
+
if (target.right < root.left) return root.left - target.right;
|
|
368
|
+
if (target.left > root.right) return target.left - root.right;
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/utils/connection.ts
|
|
373
|
+
function getConnectionApi() {
|
|
374
|
+
var _a, _b;
|
|
375
|
+
if (!isBrowser) return void 0;
|
|
376
|
+
const nav = navigator;
|
|
377
|
+
return (_b = (_a = nav.connection) != null ? _a : nav.mozConnection) != null ? _b : nav.webkitConnection;
|
|
378
|
+
}
|
|
379
|
+
function getConnectionInfo() {
|
|
380
|
+
var _a, _b;
|
|
381
|
+
const conn = getConnectionApi();
|
|
382
|
+
const supported = supportsNetworkInformation() && !!conn;
|
|
383
|
+
if (!supported || !conn) {
|
|
384
|
+
return {
|
|
385
|
+
effectiveType: null,
|
|
386
|
+
downlink: null,
|
|
387
|
+
rtt: null,
|
|
388
|
+
saveData: false,
|
|
389
|
+
supported: false
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
effectiveType: (_a = conn.effectiveType) != null ? _a : null,
|
|
394
|
+
downlink: typeof conn.downlink === "number" ? conn.downlink : null,
|
|
395
|
+
rtt: typeof conn.rtt === "number" ? conn.rtt : null,
|
|
396
|
+
saveData: (_b = conn.saveData) != null ? _b : false,
|
|
397
|
+
supported: true
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function subscribeToConnectionChange(onChange) {
|
|
401
|
+
const conn = getConnectionApi();
|
|
402
|
+
if (!conn || typeof conn.addEventListener !== "function") {
|
|
403
|
+
return () => {
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const handler = () => onChange(getConnectionInfo());
|
|
407
|
+
conn.addEventListener("change", handler);
|
|
408
|
+
return () => {
|
|
409
|
+
var _a;
|
|
410
|
+
return (_a = conn.removeEventListener) == null ? void 0 : _a.call(conn, "change", handler);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/core/LoadManager.ts
|
|
415
|
+
var LoadManager = class {
|
|
416
|
+
constructor(options = {}) {
|
|
417
|
+
this.refCounts = /* @__PURE__ */ new Map();
|
|
418
|
+
var _a, _b, _c;
|
|
419
|
+
this.preloadDistance = (_a = options.preloadDistance) != null ? _a : 1e3;
|
|
420
|
+
this.debug = (_b = options.debug) != null ? _b : false;
|
|
421
|
+
this.registry = new ResourceRegistry();
|
|
422
|
+
this.priorityEngine = new PriorityEngine();
|
|
423
|
+
this.viewportObserver = new ViewportObserver(this.preloadDistance);
|
|
424
|
+
this.connection = getConnectionInfo();
|
|
425
|
+
this.scheduler = new Scheduler(this.registry, this.priorityEngine, {
|
|
426
|
+
concurrency: (_c = options.concurrency) != null ? _c : 4,
|
|
427
|
+
onDecision: this.debug ? (d) => this.logDecision(d) : void 0
|
|
428
|
+
});
|
|
429
|
+
this.unsubscribeConnection = subscribeToConnectionChange((info) => {
|
|
430
|
+
this.connection = info;
|
|
431
|
+
this.requestPass();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
register(input) {
|
|
435
|
+
var _a, _b;
|
|
436
|
+
const refCount = ((_a = this.refCounts.get(input.id)) != null ? _a : 0) + 1;
|
|
437
|
+
this.refCounts.set(input.id, refCount);
|
|
438
|
+
let record = this.registry.register(input);
|
|
439
|
+
if (input.strategy === "eager" || input.priority === "CRITICAL") {
|
|
440
|
+
record = (_b = this.registry.setState(record.id, "eligible")) != null ? _b : record;
|
|
441
|
+
this.requestPass();
|
|
442
|
+
}
|
|
443
|
+
return record;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Decrements the id's reference count. Only when it reaches zero — i.e.
|
|
447
|
+
* the last mounted consumer of this resource has unmounted — does this
|
|
448
|
+
* actually abort any in-flight load, remove the scheduler's loader, and
|
|
449
|
+
* delete the registry entry. See the class doc for why.
|
|
450
|
+
*/
|
|
451
|
+
unregister(id) {
|
|
452
|
+
var _a, _b;
|
|
453
|
+
const remaining = ((_a = this.refCounts.get(id)) != null ? _a : 1) - 1;
|
|
454
|
+
if (remaining > 0) {
|
|
455
|
+
this.refCounts.set(id, remaining);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.refCounts.delete(id);
|
|
459
|
+
const record = this.registry.get(id);
|
|
460
|
+
if (record && record.state !== "loaded" && record.state !== "error") {
|
|
461
|
+
(_b = record.abortController) == null ? void 0 : _b.abort();
|
|
462
|
+
}
|
|
463
|
+
this.scheduler.unregisterLoader(id);
|
|
464
|
+
this.registry.unregister(id);
|
|
465
|
+
}
|
|
466
|
+
setLoader(id, loader) {
|
|
467
|
+
this.scheduler.registerLoader(id, loader);
|
|
468
|
+
this.requestPass();
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Attaches the shared ViewportObserver to an element for a given
|
|
472
|
+
* resource id. Returns an unobserve function. A no-op (returning a
|
|
473
|
+
* no-op cleanup) for resources that skip viewport gating — callers
|
|
474
|
+
* don't need to branch on that themselves.
|
|
475
|
+
*/
|
|
476
|
+
observeViewport(id, element) {
|
|
477
|
+
const callback = (distance) => this.reportViewportDistance(id, distance);
|
|
478
|
+
this.viewportObserver.observe(element, callback);
|
|
479
|
+
return () => this.viewportObserver.unobserve(element);
|
|
480
|
+
}
|
|
481
|
+
/** Called whenever a resource's distance-from-viewport changes. */
|
|
482
|
+
reportViewportDistance(id, distance) {
|
|
483
|
+
const record = this.registry.get(id);
|
|
484
|
+
if (!record) return;
|
|
485
|
+
this.registry.setViewportDistance(id, distance);
|
|
486
|
+
if (record.state === "idle") {
|
|
487
|
+
const withinZone = this.priorityEngine.isWithinPreloadZone(distance, this.preloadDistance);
|
|
488
|
+
if (withinZone) {
|
|
489
|
+
this.registry.setState(id, "eligible");
|
|
490
|
+
if (distance > 0) {
|
|
491
|
+
this.scheduler.prefetch(id, `entered preload zone at ${Math.round(distance)}px`);
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
this.scheduler.defer(id, `outside preload zone (${Math.round(distance)}px > ${this.preloadDistance}px)`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
this.requestPass();
|
|
498
|
+
}
|
|
499
|
+
requestPass() {
|
|
500
|
+
this.scheduler.requestPass({
|
|
501
|
+
connection: this.connection,
|
|
502
|
+
preloadDistance: this.preloadDistance
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
getConnection() {
|
|
506
|
+
return this.connection;
|
|
507
|
+
}
|
|
508
|
+
getDebugSnapshot() {
|
|
509
|
+
return {
|
|
510
|
+
resources: this.registry.all(),
|
|
511
|
+
decisions: this.scheduler.getDecisionLog(),
|
|
512
|
+
connection: this.connection
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
setPriority(id, priority) {
|
|
516
|
+
this.registry.update(id, { priority });
|
|
517
|
+
this.requestPass();
|
|
518
|
+
}
|
|
519
|
+
destroy() {
|
|
520
|
+
var _a;
|
|
521
|
+
(_a = this.unsubscribeConnection) == null ? void 0 : _a.call(this);
|
|
522
|
+
this.viewportObserver.disconnect();
|
|
523
|
+
this.refCounts.clear();
|
|
524
|
+
this.registry.clear();
|
|
525
|
+
}
|
|
526
|
+
logDecision(decision) {
|
|
527
|
+
if (typeof console === "undefined") return;
|
|
528
|
+
console.debug(
|
|
529
|
+
`[react-fastload] ${decision.action} "${decision.resourceId}" \u2014 ${decision.reason}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/observers/PerformanceObserver.ts
|
|
535
|
+
var BrowserPerformanceObserver = class {
|
|
536
|
+
constructor() {
|
|
537
|
+
this.observers = [];
|
|
538
|
+
}
|
|
539
|
+
observe(entryTypes, callback, options) {
|
|
540
|
+
var _a;
|
|
541
|
+
if (!supportsPerformanceObserver()) return;
|
|
542
|
+
for (const type of entryTypes) {
|
|
543
|
+
try {
|
|
544
|
+
const observer = new PerformanceObserver((list) => {
|
|
545
|
+
callback(list.getEntries());
|
|
546
|
+
});
|
|
547
|
+
observer.observe({ type, buffered: (_a = options == null ? void 0 : options.buffered) != null ? _a : true });
|
|
548
|
+
this.observers.push(observer);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
disconnect() {
|
|
554
|
+
for (const observer of this.observers) observer.disconnect();
|
|
555
|
+
this.observers = [];
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// src/metrics/WebVitals.ts
|
|
560
|
+
var WebVitals = class {
|
|
561
|
+
constructor() {
|
|
562
|
+
this.snapshot = { fcp: null, lcp: null, cls: null };
|
|
563
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
564
|
+
this.perfObserver = new BrowserPerformanceObserver();
|
|
565
|
+
this.clsAccumulator = 0;
|
|
566
|
+
}
|
|
567
|
+
start() {
|
|
568
|
+
if (!supportsNavigationTiming()) return;
|
|
569
|
+
this.perfObserver.observe(["paint"], (entries) => {
|
|
570
|
+
for (const entry of entries) {
|
|
571
|
+
if (entry.name === "first-contentful-paint") {
|
|
572
|
+
this.update({ fcp: entry.startTime });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
this.perfObserver.observe(["largest-contentful-paint"], (entries) => {
|
|
577
|
+
const last = entries[entries.length - 1];
|
|
578
|
+
if (last) {
|
|
579
|
+
const value = last.renderTime || last.loadTime || last.startTime;
|
|
580
|
+
this.update({ lcp: value });
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
this.perfObserver.observe(["layout-shift"], (entries) => {
|
|
584
|
+
for (const entry of entries) {
|
|
585
|
+
if (!entry.hadRecentInput) {
|
|
586
|
+
this.clsAccumulator += entry.value;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
this.update({ cls: this.clsAccumulator });
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
getSnapshot() {
|
|
593
|
+
return { ...this.snapshot };
|
|
594
|
+
}
|
|
595
|
+
subscribe(listener) {
|
|
596
|
+
this.listeners.add(listener);
|
|
597
|
+
return () => this.listeners.delete(listener);
|
|
598
|
+
}
|
|
599
|
+
stop() {
|
|
600
|
+
this.perfObserver.disconnect();
|
|
601
|
+
this.listeners.clear();
|
|
602
|
+
}
|
|
603
|
+
update(patch) {
|
|
604
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
605
|
+
for (const listener of this.listeners) listener(this.snapshot);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/metrics/ResourceMetrics.ts
|
|
610
|
+
function getNavigationMetrics() {
|
|
611
|
+
if (!supportsNavigationTiming()) {
|
|
612
|
+
return { ttfb: null, domContentLoaded: null, loadEvent: null };
|
|
613
|
+
}
|
|
614
|
+
const [nav] = performance.getEntriesByType("navigation");
|
|
615
|
+
if (!nav) {
|
|
616
|
+
return { ttfb: null, domContentLoaded: null, loadEvent: null };
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
ttfb: round(nav.responseStart - nav.startTime),
|
|
620
|
+
domContentLoaded: round(nav.domContentLoadedEventEnd - nav.startTime),
|
|
621
|
+
loadEvent: nav.loadEventEnd > 0 ? round(nav.loadEventEnd - nav.startTime) : null
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function getResourceTimingSummary() {
|
|
625
|
+
if (!supportsNavigationTiming()) {
|
|
626
|
+
return { totalResources: 0, totalTransferBytes: 0, slowestResourceMs: null };
|
|
627
|
+
}
|
|
628
|
+
const entries = performance.getEntriesByType("resource");
|
|
629
|
+
let totalTransferBytes = 0;
|
|
630
|
+
let slowest = null;
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
totalTransferBytes += entry.transferSize || 0;
|
|
633
|
+
const duration = entry.duration;
|
|
634
|
+
if (slowest === null || duration > slowest) slowest = duration;
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
totalResources: entries.length,
|
|
638
|
+
totalTransferBytes,
|
|
639
|
+
slowestResourceMs: slowest !== null ? round(slowest) : null
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function round(value) {
|
|
643
|
+
return Math.round(value * 100) / 100;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/metrics/MetricsCollector.ts
|
|
647
|
+
var MetricsCollector = class {
|
|
648
|
+
constructor(loadManager) {
|
|
649
|
+
this.loadManager = loadManager;
|
|
650
|
+
this.webVitals = new WebVitals();
|
|
651
|
+
this.started = false;
|
|
652
|
+
}
|
|
653
|
+
start() {
|
|
654
|
+
if (this.started) return;
|
|
655
|
+
this.started = true;
|
|
656
|
+
this.webVitals.start();
|
|
657
|
+
}
|
|
658
|
+
stop() {
|
|
659
|
+
this.webVitals.stop();
|
|
660
|
+
this.started = false;
|
|
661
|
+
}
|
|
662
|
+
subscribeWebVitals(listener) {
|
|
663
|
+
return this.webVitals.subscribe(listener);
|
|
664
|
+
}
|
|
665
|
+
snapshot() {
|
|
666
|
+
const vitals = this.webVitals.getSnapshot();
|
|
667
|
+
const nav = getNavigationMetrics();
|
|
668
|
+
const resourceTiming = getResourceTimingSummary();
|
|
669
|
+
const registrySummary = this.loadManager.registry.summary();
|
|
670
|
+
return {
|
|
671
|
+
fcp: vitals.fcp,
|
|
672
|
+
lcp: vitals.lcp,
|
|
673
|
+
cls: vitals.cls,
|
|
674
|
+
ttfb: nav.ttfb,
|
|
675
|
+
totalResourcesObserved: resourceTiming.totalResources,
|
|
676
|
+
totalTransferBytesObserved: resourceTiming.totalTransferBytes,
|
|
677
|
+
initialRequests: registrySummary.initialRequests,
|
|
678
|
+
deferredRequests: registrySummary.deferredRequests,
|
|
679
|
+
deferredResources: registrySummary.deferredResources,
|
|
680
|
+
bytesDeferred: registrySummary.bytesDeferred,
|
|
681
|
+
imagesDeferred: registrySummary.imagesDeferred,
|
|
682
|
+
videosDeferred: registrySummary.videosDeferred,
|
|
683
|
+
audiosDeferred: registrySummary.audiosDeferred,
|
|
684
|
+
componentsDeferred: registrySummary.componentsDeferred
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
var FastLoadContext = React5.createContext(null);
|
|
689
|
+
var STRATEGY_CONCURRENCY = {
|
|
690
|
+
adaptive: 4,
|
|
691
|
+
eager: 8,
|
|
692
|
+
conservative: 2
|
|
693
|
+
};
|
|
694
|
+
function FastLoadProvider({
|
|
695
|
+
children,
|
|
696
|
+
strategy = "adaptive",
|
|
697
|
+
preloadDistance = 1e3,
|
|
698
|
+
concurrency,
|
|
699
|
+
debug = false
|
|
700
|
+
}) {
|
|
701
|
+
const loadManagerRef = React5.useRef();
|
|
702
|
+
if (!loadManagerRef.current) {
|
|
703
|
+
loadManagerRef.current = new LoadManager({
|
|
704
|
+
preloadDistance,
|
|
705
|
+
concurrency: concurrency != null ? concurrency : STRATEGY_CONCURRENCY[strategy],
|
|
706
|
+
debug
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const metricsCollectorRef = React5.useRef();
|
|
710
|
+
if (!metricsCollectorRef.current) {
|
|
711
|
+
metricsCollectorRef.current = new MetricsCollector(loadManagerRef.current);
|
|
712
|
+
}
|
|
713
|
+
React5.useEffect(() => {
|
|
714
|
+
const manager = loadManagerRef.current;
|
|
715
|
+
const collector = metricsCollectorRef.current;
|
|
716
|
+
collector.start();
|
|
717
|
+
return () => {
|
|
718
|
+
collector.stop();
|
|
719
|
+
manager.destroy();
|
|
720
|
+
};
|
|
721
|
+
}, []);
|
|
722
|
+
const value = React5.useMemo(
|
|
723
|
+
() => ({
|
|
724
|
+
loadManager: loadManagerRef.current,
|
|
725
|
+
metricsCollector: metricsCollectorRef.current,
|
|
726
|
+
debug
|
|
727
|
+
}),
|
|
728
|
+
[debug]
|
|
729
|
+
);
|
|
730
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(FastLoadContext.Provider, { value, children: [
|
|
731
|
+
children,
|
|
732
|
+
debug && /* @__PURE__ */ jsxRuntime.jsx(FastLoadDebugPanel, {})
|
|
733
|
+
] });
|
|
734
|
+
}
|
|
735
|
+
function useFastLoadContext() {
|
|
736
|
+
const ctx = React5.useContext(FastLoadContext);
|
|
737
|
+
if (!ctx) {
|
|
738
|
+
throw new Error(
|
|
739
|
+
"react-fastload: this hook/component must be used inside a <FastLoadProvider>."
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
return ctx;
|
|
743
|
+
}
|
|
744
|
+
function FastLoadDebugPanel() {
|
|
745
|
+
var _a, _b;
|
|
746
|
+
const { loadManager } = useFastLoadContext();
|
|
747
|
+
const [snapshot, setSnapshot] = React5.useState(() => loadManager.getDebugSnapshot());
|
|
748
|
+
React5.useEffect(() => {
|
|
749
|
+
const unsubscribe = loadManager.registry.subscribe(() => {
|
|
750
|
+
setSnapshot(loadManager.getDebugSnapshot());
|
|
751
|
+
});
|
|
752
|
+
const interval = setInterval(() => setSnapshot(loadManager.getDebugSnapshot()), 1e3);
|
|
753
|
+
return () => {
|
|
754
|
+
unsubscribe();
|
|
755
|
+
clearInterval(interval);
|
|
756
|
+
};
|
|
757
|
+
}, [loadManager]);
|
|
758
|
+
const isProduction = typeof process !== "undefined" && ((_a = process.env) == null ? void 0 : _a.NODE_ENV) === "production";
|
|
759
|
+
if (isProduction) return null;
|
|
760
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
761
|
+
"div",
|
|
762
|
+
{
|
|
763
|
+
style: {
|
|
764
|
+
position: "fixed",
|
|
765
|
+
bottom: 8,
|
|
766
|
+
right: 8,
|
|
767
|
+
zIndex: 999999,
|
|
768
|
+
maxWidth: 360,
|
|
769
|
+
maxHeight: 320,
|
|
770
|
+
overflow: "auto",
|
|
771
|
+
background: "rgba(17, 17, 17, 0.92)",
|
|
772
|
+
color: "#e6e6e6",
|
|
773
|
+
fontFamily: "monospace",
|
|
774
|
+
fontSize: 11,
|
|
775
|
+
borderRadius: 8,
|
|
776
|
+
padding: 10,
|
|
777
|
+
lineHeight: 1.4
|
|
778
|
+
},
|
|
779
|
+
"data-testid": "fastload-debug-panel",
|
|
780
|
+
children: [
|
|
781
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 700, marginBottom: 6 }, children: "react-fastload debug" }),
|
|
782
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
783
|
+
"connection: ",
|
|
784
|
+
(_b = snapshot.connection.effectiveType) != null ? _b : "unknown",
|
|
785
|
+
" (saveData: ",
|
|
786
|
+
String(snapshot.connection.saveData),
|
|
787
|
+
")"
|
|
788
|
+
] }),
|
|
789
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 6 }, children: [
|
|
790
|
+
"resources (",
|
|
791
|
+
snapshot.resources.length,
|
|
792
|
+
")"
|
|
793
|
+
] }),
|
|
794
|
+
snapshot.resources.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { opacity: r.state === "loaded" ? 0.6 : 1 }, children: [
|
|
795
|
+
"[",
|
|
796
|
+
r.priority,
|
|
797
|
+
"] ",
|
|
798
|
+
r.id,
|
|
799
|
+
" \u2014 ",
|
|
800
|
+
r.state,
|
|
801
|
+
r.deferred ? " (deferred)" : "",
|
|
802
|
+
r.prefetched ? " (prefetched)" : ""
|
|
803
|
+
] }, r.id)),
|
|
804
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 6 }, children: "recent decisions" }),
|
|
805
|
+
snapshot.decisions.slice(-8).map((d, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
806
|
+
d.action,
|
|
807
|
+
" ",
|
|
808
|
+
d.resourceId,
|
|
809
|
+
": ",
|
|
810
|
+
d.reason
|
|
811
|
+
] }, i))
|
|
812
|
+
]
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
function useLazyLoad(options) {
|
|
817
|
+
const { loadManager } = useFastLoadContext();
|
|
818
|
+
const { id, type, priority, strategy, estimatedSize, meta, loader } = options;
|
|
819
|
+
const unobserveRef = React5.useRef(void 0);
|
|
820
|
+
const elementRef = React5.useRef(null);
|
|
821
|
+
const shouldObserveRef = React5.useRef(strategy !== "eager" && priority !== "CRITICAL");
|
|
822
|
+
shouldObserveRef.current = strategy !== "eager" && priority !== "CRITICAL";
|
|
823
|
+
const [state, setState] = React5.useState("idle");
|
|
824
|
+
const [record, setRecord] = React5.useState(void 0);
|
|
825
|
+
React5.useEffect(() => {
|
|
826
|
+
const initial = loadManager.register({ id, type, priority, strategy, estimatedSize, meta });
|
|
827
|
+
setRecord(initial);
|
|
828
|
+
setState(initial.state);
|
|
829
|
+
loadManager.setLoader(id, loader);
|
|
830
|
+
const unsubscribe = loadManager.registry.subscribe((all) => {
|
|
831
|
+
const found = all.find((r) => r.id === id);
|
|
832
|
+
if (found) {
|
|
833
|
+
setRecord(found);
|
|
834
|
+
setState(found.state);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
if (shouldObserveRef.current && elementRef.current) {
|
|
838
|
+
unobserveRef.current = loadManager.observeViewport(id, elementRef.current);
|
|
839
|
+
}
|
|
840
|
+
return () => {
|
|
841
|
+
var _a;
|
|
842
|
+
unsubscribe();
|
|
843
|
+
(_a = unobserveRef.current) == null ? void 0 : _a.call(unobserveRef);
|
|
844
|
+
unobserveRef.current = void 0;
|
|
845
|
+
loadManager.unregister(id);
|
|
846
|
+
};
|
|
847
|
+
}, [id]);
|
|
848
|
+
const ref = (node) => {
|
|
849
|
+
var _a;
|
|
850
|
+
if (elementRef.current === node) return;
|
|
851
|
+
(_a = unobserveRef.current) == null ? void 0 : _a.call(unobserveRef);
|
|
852
|
+
unobserveRef.current = void 0;
|
|
853
|
+
elementRef.current = node;
|
|
854
|
+
if (node && shouldObserveRef.current) {
|
|
855
|
+
unobserveRef.current = loadManager.observeViewport(id, node);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
return { ref, state, record };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/resources/ImageLoader.ts
|
|
862
|
+
var PRIORITY_TO_FETCH_PRIORITY = {
|
|
863
|
+
CRITICAL: "high",
|
|
864
|
+
HIGH: "high",
|
|
865
|
+
NORMAL: "auto",
|
|
866
|
+
LOW: "low",
|
|
867
|
+
IDLE: "low"
|
|
868
|
+
};
|
|
869
|
+
function loadImage(options) {
|
|
870
|
+
return new Promise((resolve, reject) => {
|
|
871
|
+
var _a, _b, _c;
|
|
872
|
+
if (typeof Image === "undefined") {
|
|
873
|
+
resolve({});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if ((_a = options.signal) == null ? void 0 : _a.aborted) {
|
|
877
|
+
reject(new DOMException("Image load aborted before it started", "AbortError"));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const img = new Image();
|
|
881
|
+
if (supportsFetchPriority()) {
|
|
882
|
+
img.fetchPriority = PRIORITY_TO_FETCH_PRIORITY[options.priority];
|
|
883
|
+
}
|
|
884
|
+
img.decoding = (_b = options.decoding) != null ? _b : "async";
|
|
885
|
+
const cleanup = () => {
|
|
886
|
+
var _a2;
|
|
887
|
+
img.onload = null;
|
|
888
|
+
img.onerror = null;
|
|
889
|
+
(_a2 = options.signal) == null ? void 0 : _a2.removeEventListener("abort", onAbort);
|
|
890
|
+
};
|
|
891
|
+
const onAbort = () => {
|
|
892
|
+
cleanup();
|
|
893
|
+
img.src = "";
|
|
894
|
+
reject(new DOMException("Image load aborted", "AbortError"));
|
|
895
|
+
};
|
|
896
|
+
img.onload = () => {
|
|
897
|
+
cleanup();
|
|
898
|
+
resolve(img);
|
|
899
|
+
};
|
|
900
|
+
img.onerror = (event) => {
|
|
901
|
+
cleanup();
|
|
902
|
+
reject(event instanceof Event ? new Error("Image failed to load") : event);
|
|
903
|
+
};
|
|
904
|
+
(_c = options.signal) == null ? void 0 : _c.addEventListener("abort", onAbort, { once: true });
|
|
905
|
+
img.src = options.src;
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
function resolvePriority(priority) {
|
|
909
|
+
if (!priority || priority === "auto") return "NORMAL";
|
|
910
|
+
return priority;
|
|
911
|
+
}
|
|
912
|
+
function SmartImage({
|
|
913
|
+
src,
|
|
914
|
+
priority = "auto",
|
|
915
|
+
placeholder,
|
|
916
|
+
resourceId,
|
|
917
|
+
strategy = "auto",
|
|
918
|
+
estimatedSize,
|
|
919
|
+
alt,
|
|
920
|
+
decoding = "async",
|
|
921
|
+
style,
|
|
922
|
+
onLoad,
|
|
923
|
+
onError,
|
|
924
|
+
...rest
|
|
925
|
+
}) {
|
|
926
|
+
const id = resourceId != null ? resourceId : `image:${src}`;
|
|
927
|
+
const resolvedPriority = resolvePriority(priority);
|
|
928
|
+
const effectiveStrategy = strategy === "auto" ? resolvedPriority === "CRITICAL" ? "eager" : "lazy" : strategy;
|
|
929
|
+
const loader = React5.useMemo(
|
|
930
|
+
() => (resource) => {
|
|
931
|
+
var _a;
|
|
932
|
+
return loadImage({ src, priority: resolvedPriority, decoding, signal: (_a = resource.abortController) == null ? void 0 : _a.signal }).then(
|
|
933
|
+
() => void 0
|
|
934
|
+
);
|
|
935
|
+
},
|
|
936
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
937
|
+
[src, resolvedPriority, decoding]
|
|
938
|
+
);
|
|
939
|
+
const { ref, state } = useLazyLoad({
|
|
940
|
+
id,
|
|
941
|
+
type: "image",
|
|
942
|
+
priority: resolvedPriority,
|
|
943
|
+
strategy: effectiveStrategy,
|
|
944
|
+
estimatedSize,
|
|
945
|
+
// Stored so tooling (e.g. a benchmark or devtools panel) can
|
|
946
|
+
// cross-reference a deferred registry entry against its real
|
|
947
|
+
// Resource Timing entry once it eventually loads. Never read by the
|
|
948
|
+
// scheduler itself.
|
|
949
|
+
meta: { src },
|
|
950
|
+
loader
|
|
951
|
+
});
|
|
952
|
+
const shouldRenderRealImage = effectiveStrategy === "eager" || state === "loading" || state === "loaded";
|
|
953
|
+
const containerStyle = {
|
|
954
|
+
...style,
|
|
955
|
+
...placeholder && !shouldRenderRealImage ? {
|
|
956
|
+
backgroundImage: `url(${placeholder})`,
|
|
957
|
+
backgroundSize: "cover",
|
|
958
|
+
backgroundPosition: "center"
|
|
959
|
+
} : {}
|
|
960
|
+
};
|
|
961
|
+
if (!shouldRenderRealImage) {
|
|
962
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
963
|
+
"div",
|
|
964
|
+
{
|
|
965
|
+
ref,
|
|
966
|
+
role: "img",
|
|
967
|
+
"aria-label": alt,
|
|
968
|
+
"aria-busy": state === "eligible",
|
|
969
|
+
"data-fastload-state": state,
|
|
970
|
+
style: { display: "inline-block", ...containerStyle }
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
const imgProps = {
|
|
975
|
+
src,
|
|
976
|
+
alt,
|
|
977
|
+
decoding,
|
|
978
|
+
loading: effectiveStrategy === "eager" ? "eager" : "lazy",
|
|
979
|
+
style: containerStyle,
|
|
980
|
+
onLoad,
|
|
981
|
+
onError,
|
|
982
|
+
...supportsFetchPriority() ? { fetchPriority: fetchPriorityFor(resolvedPriority) } : {},
|
|
983
|
+
...rest
|
|
984
|
+
};
|
|
985
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
986
|
+
"img",
|
|
987
|
+
{
|
|
988
|
+
ref,
|
|
989
|
+
"data-fastload-state": state,
|
|
990
|
+
...imgProps
|
|
991
|
+
}
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
function fetchPriorityFor(priority) {
|
|
995
|
+
if (priority === "CRITICAL" || priority === "HIGH") return "high";
|
|
996
|
+
if (priority === "LOW" || priority === "IDLE") return "low";
|
|
997
|
+
return "auto";
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/resources/VideoLoader.ts
|
|
1001
|
+
function loadVideo(options) {
|
|
1002
|
+
return new Promise((resolve) => {
|
|
1003
|
+
var _a, _b;
|
|
1004
|
+
if ((_a = options.signal) == null ? void 0 : _a.aborted) {
|
|
1005
|
+
resolve();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (options.poster && (options.priority === "CRITICAL" || options.priority === "HIGH")) {
|
|
1009
|
+
if (typeof Image === "undefined") {
|
|
1010
|
+
resolve();
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const img = new Image();
|
|
1014
|
+
const cleanup = () => {
|
|
1015
|
+
var _a2;
|
|
1016
|
+
img.onload = null;
|
|
1017
|
+
img.onerror = null;
|
|
1018
|
+
(_a2 = options.signal) == null ? void 0 : _a2.removeEventListener("abort", onAbort);
|
|
1019
|
+
};
|
|
1020
|
+
const onAbort = () => {
|
|
1021
|
+
cleanup();
|
|
1022
|
+
img.src = "";
|
|
1023
|
+
resolve();
|
|
1024
|
+
};
|
|
1025
|
+
img.onload = () => {
|
|
1026
|
+
cleanup();
|
|
1027
|
+
resolve();
|
|
1028
|
+
};
|
|
1029
|
+
img.onerror = () => {
|
|
1030
|
+
cleanup();
|
|
1031
|
+
resolve();
|
|
1032
|
+
};
|
|
1033
|
+
(_b = options.signal) == null ? void 0 : _b.addEventListener("abort", onAbort, { once: true });
|
|
1034
|
+
img.src = options.poster;
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
resolve();
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
function resolvePriority2(priority) {
|
|
1041
|
+
if (!priority || priority === "auto") return "NORMAL";
|
|
1042
|
+
return priority;
|
|
1043
|
+
}
|
|
1044
|
+
function SmartVideo({
|
|
1045
|
+
src,
|
|
1046
|
+
poster,
|
|
1047
|
+
priority = "auto",
|
|
1048
|
+
resourceId,
|
|
1049
|
+
strategy = "auto",
|
|
1050
|
+
estimatedSize,
|
|
1051
|
+
aspectRatio,
|
|
1052
|
+
autoplay = false,
|
|
1053
|
+
muted,
|
|
1054
|
+
controls = true,
|
|
1055
|
+
style,
|
|
1056
|
+
...rest
|
|
1057
|
+
}) {
|
|
1058
|
+
const id = resourceId != null ? resourceId : `video:${src}`;
|
|
1059
|
+
const resolvedPriority = resolvePriority2(priority);
|
|
1060
|
+
const effectiveStrategy = strategy === "auto" ? resolvedPriority === "CRITICAL" ? "eager" : "lazy" : strategy;
|
|
1061
|
+
const [userInteracted, setUserInteracted] = React5.useState(false);
|
|
1062
|
+
const loader = React5.useMemo(
|
|
1063
|
+
() => (resource) => {
|
|
1064
|
+
var _a;
|
|
1065
|
+
return loadVideo({ poster, priority: resolvedPriority, signal: (_a = resource.abortController) == null ? void 0 : _a.signal });
|
|
1066
|
+
},
|
|
1067
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1068
|
+
[src, poster, resolvedPriority]
|
|
1069
|
+
);
|
|
1070
|
+
const { ref, state } = useLazyLoad({
|
|
1071
|
+
id,
|
|
1072
|
+
type: "video",
|
|
1073
|
+
priority: resolvedPriority,
|
|
1074
|
+
strategy: effectiveStrategy,
|
|
1075
|
+
estimatedSize,
|
|
1076
|
+
// Stored so tooling (e.g. a benchmark or devtools panel) can
|
|
1077
|
+
// cross-reference a deferred registry entry against its real
|
|
1078
|
+
// Resource Timing entry once it eventually loads. Never read by the
|
|
1079
|
+
// scheduler itself.
|
|
1080
|
+
meta: { src },
|
|
1081
|
+
loader
|
|
1082
|
+
});
|
|
1083
|
+
const isEligible = effectiveStrategy === "eager" || state === "loading" || state === "loaded";
|
|
1084
|
+
const shouldMountVideo = isEligible || userInteracted;
|
|
1085
|
+
if (!shouldMountVideo) {
|
|
1086
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1087
|
+
"div",
|
|
1088
|
+
{
|
|
1089
|
+
ref,
|
|
1090
|
+
role: "button",
|
|
1091
|
+
tabIndex: 0,
|
|
1092
|
+
"aria-label": "Load video",
|
|
1093
|
+
"data-fastload-state": state,
|
|
1094
|
+
onClick: () => setUserInteracted(true),
|
|
1095
|
+
onKeyDown: (e) => {
|
|
1096
|
+
if (e.key === "Enter" || e.key === " ") setUserInteracted(true);
|
|
1097
|
+
},
|
|
1098
|
+
style: {
|
|
1099
|
+
position: "relative",
|
|
1100
|
+
cursor: "pointer",
|
|
1101
|
+
backgroundImage: poster ? `url(${poster})` : void 0,
|
|
1102
|
+
backgroundSize: "cover",
|
|
1103
|
+
backgroundPosition: "center",
|
|
1104
|
+
display: "flex",
|
|
1105
|
+
alignItems: "center",
|
|
1106
|
+
justifyContent: "center",
|
|
1107
|
+
minHeight: aspectRatio ? void 0 : 120,
|
|
1108
|
+
aspectRatio,
|
|
1109
|
+
...style
|
|
1110
|
+
},
|
|
1111
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PlayGlyph, {})
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1116
|
+
"video",
|
|
1117
|
+
{
|
|
1118
|
+
ref,
|
|
1119
|
+
src,
|
|
1120
|
+
poster,
|
|
1121
|
+
preload: resolvedPriority === "CRITICAL" || resolvedPriority === "HIGH" ? "auto" : "metadata",
|
|
1122
|
+
autoPlay: autoplay && isEligible,
|
|
1123
|
+
muted: autoplay ? true : muted,
|
|
1124
|
+
controls,
|
|
1125
|
+
"data-fastload-state": state,
|
|
1126
|
+
style: { aspectRatio, ...style },
|
|
1127
|
+
...rest
|
|
1128
|
+
}
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
function PlayGlyph() {
|
|
1132
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "48", height: "48", viewBox: "0 0 48 48", "aria-hidden": "true", children: [
|
|
1133
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "24", cy: "24", r: "23", fill: "rgba(0,0,0,0.55)", stroke: "white", strokeWidth: "1" }),
|
|
1134
|
+
/* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "19,14 19,34 35,24", fill: "white" })
|
|
1135
|
+
] });
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/resources/AudioLoader.ts
|
|
1139
|
+
function loadAudio(options) {
|
|
1140
|
+
return new Promise((resolve) => {
|
|
1141
|
+
var _a, _b;
|
|
1142
|
+
if ((_a = options.signal) == null ? void 0 : _a.aborted) {
|
|
1143
|
+
resolve();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (options.priority !== "CRITICAL" && options.priority !== "HIGH") {
|
|
1147
|
+
resolve();
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof Audio === "undefined") {
|
|
1151
|
+
resolve();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const audio = new Audio();
|
|
1155
|
+
audio.preload = "metadata";
|
|
1156
|
+
const cleanup = () => {
|
|
1157
|
+
var _a2;
|
|
1158
|
+
audio.removeEventListener("loadedmetadata", finish);
|
|
1159
|
+
audio.removeEventListener("error", finish);
|
|
1160
|
+
(_a2 = options.signal) == null ? void 0 : _a2.removeEventListener("abort", onAbort);
|
|
1161
|
+
};
|
|
1162
|
+
const finish = () => {
|
|
1163
|
+
cleanup();
|
|
1164
|
+
resolve();
|
|
1165
|
+
};
|
|
1166
|
+
const onAbort = () => {
|
|
1167
|
+
cleanup();
|
|
1168
|
+
audio.src = "";
|
|
1169
|
+
resolve();
|
|
1170
|
+
};
|
|
1171
|
+
audio.addEventListener("loadedmetadata", finish, { once: true });
|
|
1172
|
+
audio.addEventListener("error", finish, { once: true });
|
|
1173
|
+
(_b = options.signal) == null ? void 0 : _b.addEventListener("abort", onAbort, { once: true });
|
|
1174
|
+
audio.src = options.src;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function resolvePriority3(priority) {
|
|
1178
|
+
if (!priority || priority === "auto") return "NORMAL";
|
|
1179
|
+
return priority;
|
|
1180
|
+
}
|
|
1181
|
+
function SmartAudio({
|
|
1182
|
+
src,
|
|
1183
|
+
priority = "auto",
|
|
1184
|
+
resourceId,
|
|
1185
|
+
strategy = "auto",
|
|
1186
|
+
estimatedSize,
|
|
1187
|
+
label,
|
|
1188
|
+
autoplay = false,
|
|
1189
|
+
muted,
|
|
1190
|
+
controls = true,
|
|
1191
|
+
...rest
|
|
1192
|
+
}) {
|
|
1193
|
+
const id = resourceId != null ? resourceId : `audio:${src}`;
|
|
1194
|
+
const resolvedPriority = resolvePriority3(priority);
|
|
1195
|
+
const effectiveStrategy = strategy === "auto" ? resolvedPriority === "CRITICAL" ? "eager" : "lazy" : strategy;
|
|
1196
|
+
const [userInteracted, setUserInteracted] = React5.useState(false);
|
|
1197
|
+
const loader = React5.useMemo(
|
|
1198
|
+
() => (resource) => {
|
|
1199
|
+
var _a;
|
|
1200
|
+
return loadAudio({ src, priority: resolvedPriority, signal: (_a = resource.abortController) == null ? void 0 : _a.signal });
|
|
1201
|
+
},
|
|
1202
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1203
|
+
[src, resolvedPriority]
|
|
1204
|
+
);
|
|
1205
|
+
const { ref, state } = useLazyLoad({
|
|
1206
|
+
id,
|
|
1207
|
+
type: "audio",
|
|
1208
|
+
priority: resolvedPriority,
|
|
1209
|
+
strategy: effectiveStrategy,
|
|
1210
|
+
estimatedSize,
|
|
1211
|
+
// Stored so tooling (e.g. a benchmark or devtools panel) can
|
|
1212
|
+
// cross-reference a deferred registry entry against its real
|
|
1213
|
+
// Resource Timing entry once it eventually loads. Never read by the
|
|
1214
|
+
// scheduler itself.
|
|
1215
|
+
meta: { src },
|
|
1216
|
+
loader
|
|
1217
|
+
});
|
|
1218
|
+
const isEligible = effectiveStrategy === "eager" || state === "loading" || state === "loaded";
|
|
1219
|
+
const shouldMountAudio = isEligible || userInteracted;
|
|
1220
|
+
if (!shouldMountAudio) {
|
|
1221
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1222
|
+
"div",
|
|
1223
|
+
{
|
|
1224
|
+
ref,
|
|
1225
|
+
role: "button",
|
|
1226
|
+
tabIndex: 0,
|
|
1227
|
+
"aria-label": label ? `Load audio: ${label}` : "Load audio",
|
|
1228
|
+
"data-fastload-state": state,
|
|
1229
|
+
onClick: () => setUserInteracted(true),
|
|
1230
|
+
onKeyDown: (e) => {
|
|
1231
|
+
if (e.key === "Enter" || e.key === " ") setUserInteracted(true);
|
|
1232
|
+
},
|
|
1233
|
+
style: {
|
|
1234
|
+
display: "inline-flex",
|
|
1235
|
+
alignItems: "center",
|
|
1236
|
+
gap: 8,
|
|
1237
|
+
padding: "8px 12px",
|
|
1238
|
+
borderRadius: 999,
|
|
1239
|
+
border: "1px solid rgba(0,0,0,0.15)",
|
|
1240
|
+
cursor: "pointer",
|
|
1241
|
+
userSelect: "none"
|
|
1242
|
+
},
|
|
1243
|
+
children: [
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsx(PlayGlyph2, {}),
|
|
1245
|
+
label != null ? label : "Load audio"
|
|
1246
|
+
]
|
|
1247
|
+
}
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1251
|
+
"audio",
|
|
1252
|
+
{
|
|
1253
|
+
ref,
|
|
1254
|
+
src,
|
|
1255
|
+
preload: resolvedPriority === "CRITICAL" || resolvedPriority === "HIGH" ? "auto" : "metadata",
|
|
1256
|
+
autoPlay: autoplay && isEligible,
|
|
1257
|
+
muted: autoplay ? true : muted,
|
|
1258
|
+
controls,
|
|
1259
|
+
"data-fastload-state": state,
|
|
1260
|
+
...rest
|
|
1261
|
+
}
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
function PlayGlyph2() {
|
|
1265
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "6,4 6,20 20,12", fill: "currentColor" }) });
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/resources/ComponentLoader.ts
|
|
1269
|
+
function createComponentLoader(importFn) {
|
|
1270
|
+
let cached;
|
|
1271
|
+
return () => {
|
|
1272
|
+
if (!cached) {
|
|
1273
|
+
cached = importFn();
|
|
1274
|
+
}
|
|
1275
|
+
return cached;
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function lazyComponent(importFn, options = {}) {
|
|
1279
|
+
var _a, _b, _c;
|
|
1280
|
+
const cachedImport = createComponentLoader(importFn);
|
|
1281
|
+
const priority = (_a = options.priority) != null ? _a : "NORMAL";
|
|
1282
|
+
const strategy = (_b = options.strategy) != null ? _b : priority === "CRITICAL" ? "eager" : "lazy";
|
|
1283
|
+
function LazyComponentWrapper(props) {
|
|
1284
|
+
var _a2, _b2, _c2, _d;
|
|
1285
|
+
const generatedId = React5.useId();
|
|
1286
|
+
const id = (_a2 = options.resourceId) != null ? _a2 : `component:${generatedId}`;
|
|
1287
|
+
const LazyReal = React5.useMemo(() => React5__default.default.lazy(cachedImport), []);
|
|
1288
|
+
const loader = React5.useMemo(
|
|
1289
|
+
() => () => cachedImport().then(() => void 0),
|
|
1290
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1291
|
+
[]
|
|
1292
|
+
);
|
|
1293
|
+
const { ref, state } = useLazyLoad({
|
|
1294
|
+
id,
|
|
1295
|
+
type: "component",
|
|
1296
|
+
priority,
|
|
1297
|
+
strategy,
|
|
1298
|
+
estimatedSize: options.estimatedSize,
|
|
1299
|
+
loader
|
|
1300
|
+
});
|
|
1301
|
+
const isEligible = strategy === "eager" || state === "eligible" || state === "loading" || state === "loaded";
|
|
1302
|
+
if (!isEligible) {
|
|
1303
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, "data-fastload-state": state, children: (_b2 = options.placeholder) != null ? _b2 : null });
|
|
1304
|
+
}
|
|
1305
|
+
const Rendered = LazyReal;
|
|
1306
|
+
return /* @__PURE__ */ jsxRuntime.jsx(React5.Suspense, { fallback: (_d = (_c2 = options.fallback) != null ? _c2 : options.placeholder) != null ? _d : null, children: /* @__PURE__ */ jsxRuntime.jsx(Rendered, { ...props }) });
|
|
1307
|
+
}
|
|
1308
|
+
LazyComponentWrapper.displayName = `LazyComponent(${(_c = options.resourceId) != null ? _c : "anonymous"})`;
|
|
1309
|
+
return LazyComponentWrapper;
|
|
1310
|
+
}
|
|
1311
|
+
function usePriority(resourceId, priority) {
|
|
1312
|
+
const { loadManager } = useFastLoadContext();
|
|
1313
|
+
React5.useEffect(() => {
|
|
1314
|
+
if (!loadManager.registry.has(resourceId)) return;
|
|
1315
|
+
loadManager.setPriority(resourceId, priority);
|
|
1316
|
+
}, [loadManager, resourceId, priority]);
|
|
1317
|
+
}
|
|
1318
|
+
function useFastLoadMetrics() {
|
|
1319
|
+
const { loadManager, metricsCollector } = useFastLoadContext();
|
|
1320
|
+
const [metrics, setMetrics] = React5.useState(() => metricsCollector.snapshot());
|
|
1321
|
+
React5.useEffect(() => {
|
|
1322
|
+
const refresh = () => setMetrics(metricsCollector.snapshot());
|
|
1323
|
+
const unsubVitals = metricsCollector.subscribeWebVitals(refresh);
|
|
1324
|
+
const unsubRegistry = loadManager.registry.subscribe(refresh);
|
|
1325
|
+
refresh();
|
|
1326
|
+
return () => {
|
|
1327
|
+
unsubVitals();
|
|
1328
|
+
unsubRegistry();
|
|
1329
|
+
};
|
|
1330
|
+
}, [loadManager, metricsCollector]);
|
|
1331
|
+
return metrics;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
exports.FastLoadProvider = FastLoadProvider;
|
|
1335
|
+
exports.LoadManager = LoadManager;
|
|
1336
|
+
exports.PriorityEngine = PriorityEngine;
|
|
1337
|
+
exports.ResourceRegistry = ResourceRegistry;
|
|
1338
|
+
exports.Scheduler = Scheduler;
|
|
1339
|
+
exports.SmartAudio = SmartAudio;
|
|
1340
|
+
exports.SmartImage = SmartImage;
|
|
1341
|
+
exports.SmartVideo = SmartVideo;
|
|
1342
|
+
exports.lazyComponent = lazyComponent;
|
|
1343
|
+
exports.useFastLoadContext = useFastLoadContext;
|
|
1344
|
+
exports.useFastLoadMetrics = useFastLoadMetrics;
|
|
1345
|
+
exports.useLazyLoad = useLazyLoad;
|
|
1346
|
+
exports.usePriority = usePriority;
|
|
1347
|
+
//# sourceMappingURL=index.cjs.map
|
|
1348
|
+
//# sourceMappingURL=index.cjs.map
|