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.d.cts
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared type definitions for the ReactFastLoad scheduling engine.
|
|
5
|
+
*
|
|
6
|
+
* These types are the contract between the registry, priority engine,
|
|
7
|
+
* observers, and scheduler. Keep this file dependency-free so it can be
|
|
8
|
+
* imported from anywhere without pulling in React or DOM-heavy code.
|
|
9
|
+
*/
|
|
10
|
+
type Priority = "CRITICAL" | "HIGH" | "NORMAL" | "LOW" | "IDLE";
|
|
11
|
+
type ResourceType = "image" | "video" | "audio" | "component" | "other";
|
|
12
|
+
type ResourceState = "idle" | "eligible" | "loading" | "loaded" | "error";
|
|
13
|
+
type LoadingStrategy = "eager" | "lazy" | "auto";
|
|
14
|
+
interface ResourceTimestamps {
|
|
15
|
+
/** When the resource was registered with the registry. */
|
|
16
|
+
registeredAt: number;
|
|
17
|
+
/** When the resource became eligible to load (left "idle"). */
|
|
18
|
+
eligibleAt?: number;
|
|
19
|
+
/** When the scheduler handed the resource to its loader. */
|
|
20
|
+
loadStartedAt?: number;
|
|
21
|
+
/** When loading finished (success or error). */
|
|
22
|
+
loadEndedAt?: number;
|
|
23
|
+
}
|
|
24
|
+
interface ResourceRecord {
|
|
25
|
+
id: string;
|
|
26
|
+
type: ResourceType;
|
|
27
|
+
priority: Priority;
|
|
28
|
+
state: ResourceState;
|
|
29
|
+
/** Estimated byte size, if known (e.g. from a srcset hint, or measured via Resource Timing after load). */
|
|
30
|
+
estimatedSize?: number;
|
|
31
|
+
/** Distance from the viewport in pixels at last measurement. <= 0 means intersecting. */
|
|
32
|
+
viewportDistance?: number;
|
|
33
|
+
strategy: LoadingStrategy;
|
|
34
|
+
timestamps: ResourceTimestamps;
|
|
35
|
+
/** True if the scheduler intentionally delayed this resource past its natural request time. */
|
|
36
|
+
deferred: boolean;
|
|
37
|
+
/** True if this resource was proactively prefetched ahead of viewport entry. */
|
|
38
|
+
prefetched: boolean;
|
|
39
|
+
/** Arbitrary metadata a resource loader may attach (e.g. natural width/height, video duration). */
|
|
40
|
+
meta?: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Created once per registry entry (not per mounted component — see
|
|
43
|
+
* LoadManager's reference counting). A loader may read
|
|
44
|
+
* `resource.abortController?.signal` to cancel in-flight work; the
|
|
45
|
+
* registry calls `.abort()` when the LAST consumer of this id
|
|
46
|
+
* unmounts. `undefined` in environments without AbortController.
|
|
47
|
+
*/
|
|
48
|
+
abortController?: AbortController;
|
|
49
|
+
}
|
|
50
|
+
type RegisterResourceInput = Pick<ResourceRecord, "id" | "type" | "priority" | "strategy"> & Partial<Pick<ResourceRecord, "estimatedSize" | "meta">>;
|
|
51
|
+
interface SchedulerDecision {
|
|
52
|
+
resourceId: string;
|
|
53
|
+
action: "load" | "defer" | "prefetch";
|
|
54
|
+
reason: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
}
|
|
57
|
+
interface ConnectionInfo {
|
|
58
|
+
/** Effective connection type, e.g. "4g", "3g", "2g", "slow-2g". Null if unsupported. */
|
|
59
|
+
effectiveType: string | null;
|
|
60
|
+
/** Estimated downlink in Mbps. Null if unsupported. */
|
|
61
|
+
downlink: number | null;
|
|
62
|
+
/** Estimated round-trip time in ms. Null if unsupported. */
|
|
63
|
+
rtt: number | null;
|
|
64
|
+
/** User has requested reduced data usage. */
|
|
65
|
+
saveData: boolean;
|
|
66
|
+
/** Whether the Network Information API is supported in this environment. */
|
|
67
|
+
supported: boolean;
|
|
68
|
+
}
|
|
69
|
+
interface SchedulerOptions {
|
|
70
|
+
/** Maximum number of resources the scheduler will actively load at once. Default 4. */
|
|
71
|
+
concurrency?: number;
|
|
72
|
+
/** Called whenever the scheduler makes a decision about a resource. Used by debug mode. */
|
|
73
|
+
onDecision?: (decision: SchedulerDecision) => void;
|
|
74
|
+
}
|
|
75
|
+
interface FastLoadDebugSnapshot {
|
|
76
|
+
resources: ResourceRecord[];
|
|
77
|
+
decisions: SchedulerDecision[];
|
|
78
|
+
connection: ConnectionInfo;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface PriorityContext {
|
|
82
|
+
connection: ConnectionInfo;
|
|
83
|
+
/** Preload distance in pixels, as configured on the provider. */
|
|
84
|
+
preloadDistance: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* PriorityEngine turns a resource's declared priority plus live signals
|
|
88
|
+
* (viewport distance, network conditions) into an *effective* priority
|
|
89
|
+
* score used purely for ordering within the scheduler. It never mutates
|
|
90
|
+
* the resource's declared priority — that stays whatever the developer set.
|
|
91
|
+
*/
|
|
92
|
+
declare class PriorityEngine {
|
|
93
|
+
/**
|
|
94
|
+
* Lower score = loaded sooner. CRITICAL always sorts first regardless
|
|
95
|
+
* of other signals, per the "never delay explicitly critical resources"
|
|
96
|
+
* requirement.
|
|
97
|
+
*/
|
|
98
|
+
score(resource: ResourceRecord, ctx: PriorityContext): number;
|
|
99
|
+
compare(a: ResourceRecord, b: ResourceRecord, ctx: PriorityContext): number;
|
|
100
|
+
isConstrainedConnection(connection: ConnectionInfo): boolean;
|
|
101
|
+
/** Whether a resource sitting at `distance` px from the viewport should become eligible. */
|
|
102
|
+
isWithinPreloadZone(distance: number | undefined, preloadDistance: number): boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type Listener = (resources: ResourceRecord[]) => void;
|
|
106
|
+
/**
|
|
107
|
+
* ResourceRegistry is the single source of truth for every resource
|
|
108
|
+
* ReactFastLoad knows about. It does not make loading decisions itself —
|
|
109
|
+
* that's the PriorityEngine and Scheduler's job — it only stores state
|
|
110
|
+
* and notifies subscribers when that state changes.
|
|
111
|
+
*/
|
|
112
|
+
declare class ResourceRegistry {
|
|
113
|
+
private resources;
|
|
114
|
+
private listeners;
|
|
115
|
+
register(input: RegisterResourceInput): ResourceRecord;
|
|
116
|
+
unregister(id: string): void;
|
|
117
|
+
get(id: string): ResourceRecord | undefined;
|
|
118
|
+
has(id: string): boolean;
|
|
119
|
+
all(): ResourceRecord[];
|
|
120
|
+
byState(state: ResourceState): ResourceRecord[];
|
|
121
|
+
update(id: string, patch: Partial<ResourceRecord>): ResourceRecord | undefined;
|
|
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: string, state: ResourceState): ResourceRecord | undefined;
|
|
134
|
+
setViewportDistance(id: string, distance: number): void;
|
|
135
|
+
markDeferred(id: string): void;
|
|
136
|
+
markPrefetched(id: string): void;
|
|
137
|
+
subscribe(listener: Listener): () => void;
|
|
138
|
+
clear(): void;
|
|
139
|
+
/** Aggregate counters used by the metrics layer. Pure derived data, no side effects. */
|
|
140
|
+
summary(): {
|
|
141
|
+
totalRequests: number;
|
|
142
|
+
initialRequests: number;
|
|
143
|
+
deferredRequests: number;
|
|
144
|
+
deferredResources: number;
|
|
145
|
+
bytesDeferred: number;
|
|
146
|
+
imagesDeferred: number;
|
|
147
|
+
videosDeferred: number;
|
|
148
|
+
audiosDeferred: number;
|
|
149
|
+
componentsDeferred: number;
|
|
150
|
+
};
|
|
151
|
+
private emit;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type LoaderFn = (resource: ResourceRecord) => Promise<void>;
|
|
155
|
+
interface RunOptions {
|
|
156
|
+
ctx: PriorityContext;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Scheduler is the central coordinator described in the architecture:
|
|
160
|
+
*
|
|
161
|
+
* Registry -> PriorityEngine -> signals -> Scheduler -> loading decision
|
|
162
|
+
*
|
|
163
|
+
* It owns no DOM logic and knows nothing about images/video/components —
|
|
164
|
+
* it only decides, given the current registry state and priority scores,
|
|
165
|
+
* which *eligible* resources get a loader slot right now, respecting a
|
|
166
|
+
* concurrency cap so competing requests don't starve visible content.
|
|
167
|
+
*
|
|
168
|
+
* Loaders are registered per resource type by the resources/* modules and
|
|
169
|
+
* invoked by the scheduler when it decides to load a resource.
|
|
170
|
+
*/
|
|
171
|
+
declare class Scheduler {
|
|
172
|
+
private registry;
|
|
173
|
+
private priorityEngine;
|
|
174
|
+
private concurrency;
|
|
175
|
+
private onDecision?;
|
|
176
|
+
private activeCount;
|
|
177
|
+
private loaders;
|
|
178
|
+
private decisions;
|
|
179
|
+
private readonly maxDecisionLog;
|
|
180
|
+
private scheduled;
|
|
181
|
+
constructor(registry: ResourceRegistry, priorityEngine: PriorityEngine, options?: SchedulerOptions);
|
|
182
|
+
/** Register the function used to actually load a given resource id (called once per resource). */
|
|
183
|
+
registerLoader(resourceId: string, loader: LoaderFn): void;
|
|
184
|
+
unregisterLoader(resourceId: string): void;
|
|
185
|
+
/**
|
|
186
|
+
* Request a scheduling pass. Coalesced into a microtask so multiple
|
|
187
|
+
* rapid registry updates (e.g. many IntersectionObserver callbacks in
|
|
188
|
+
* one frame) only trigger a single pass.
|
|
189
|
+
*/
|
|
190
|
+
requestPass(ctx: PriorityContext): void;
|
|
191
|
+
/** Synchronous scheduling pass, exposed directly for tests and advanced use. */
|
|
192
|
+
runPass({ ctx }: RunOptions): void;
|
|
193
|
+
private dispatch;
|
|
194
|
+
/** Explicitly defer a resource (used by observers when it leaves the preload zone again, or on slow connections). */
|
|
195
|
+
defer(resourceId: string, reason: string): void;
|
|
196
|
+
prefetch(resourceId: string, reason: string): void;
|
|
197
|
+
getDecisionLog(): SchedulerDecision[];
|
|
198
|
+
private record;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type ViewportCallback = (distance: number, isIntersecting: boolean) => void;
|
|
202
|
+
/**
|
|
203
|
+
* Thin, testable wrapper around IntersectionObserver.
|
|
204
|
+
*
|
|
205
|
+
* Rather than only exposing a boolean "is it visible", this computes an
|
|
206
|
+
* approximate pixel distance from the viewport so the PriorityEngine can
|
|
207
|
+
* rank near-viewport resources ahead of far-off ones, and so `rootMargin`
|
|
208
|
+
* (the preload distance) can be configured per observer instance.
|
|
209
|
+
*
|
|
210
|
+
* Falls back to reporting elements as immediately eligible when
|
|
211
|
+
* IntersectionObserver isn't supported (very old browsers, some SSR/test
|
|
212
|
+
* environments) — a graceful degrade rather than a crash, per the
|
|
213
|
+
* "progressive enhancement" requirement.
|
|
214
|
+
*/
|
|
215
|
+
declare class ViewportObserver {
|
|
216
|
+
private observer?;
|
|
217
|
+
private callbacks;
|
|
218
|
+
private preloadDistance;
|
|
219
|
+
constructor(preloadDistance?: number);
|
|
220
|
+
observe(element: Element, callback: ViewportCallback): void;
|
|
221
|
+
unobserve(element: Element): void;
|
|
222
|
+
disconnect(): void;
|
|
223
|
+
private handleEntries;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface LoadManagerOptions {
|
|
227
|
+
preloadDistance?: number;
|
|
228
|
+
concurrency?: number;
|
|
229
|
+
debug?: boolean;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* LoadManager wires together the registry, priority engine, and scheduler
|
|
233
|
+
* into the single object that FastLoadProvider creates and shares via
|
|
234
|
+
* context. This is the "central intellectual component" the architecture
|
|
235
|
+
* calls for — resource wrappers (SmartImage, SmartVideo, LazyComponent)
|
|
236
|
+
* only ever talk to a LoadManager instance, never to the DOM-observation
|
|
237
|
+
* or scheduling internals directly.
|
|
238
|
+
*
|
|
239
|
+
* Two behaviors worth calling out because they're easy to get wrong:
|
|
240
|
+
*
|
|
241
|
+
* 1. Request deduplication: `register()` is reference-counted. Two
|
|
242
|
+
* mounted components that resolve to the SAME resource id (by default,
|
|
243
|
+
* `${type}:${src}` — see SmartImage/SmartVideo/SmartAudio) share ONE
|
|
244
|
+
* registry entry, one loader call, and one AbortController. The entry
|
|
245
|
+
* is only actually removed — and its AbortController only actually
|
|
246
|
+
* aborted — once every consumer has unregistered. Without this, an
|
|
247
|
+
* id reused by two instances would let the first unmount delete a
|
|
248
|
+
* resource the second instance still depends on.
|
|
249
|
+
* 2. Viewport observation is centralized here as ONE shared
|
|
250
|
+
* IntersectionObserver for the whole provider, not one per resource.
|
|
251
|
+
* An earlier version had each `useLazyLoad` call create its own
|
|
252
|
+
* ViewportObserver — harmless correctness-wise, but wasteful: a page
|
|
253
|
+
* with 50 resources was creating 50 separate native
|
|
254
|
+
* IntersectionObserver instances all doing the same kind of work.
|
|
255
|
+
*/
|
|
256
|
+
declare class LoadManager {
|
|
257
|
+
readonly registry: ResourceRegistry;
|
|
258
|
+
readonly priorityEngine: PriorityEngine;
|
|
259
|
+
readonly scheduler: Scheduler;
|
|
260
|
+
readonly viewportObserver: ViewportObserver;
|
|
261
|
+
readonly preloadDistance: number;
|
|
262
|
+
readonly debug: boolean;
|
|
263
|
+
private connection;
|
|
264
|
+
private unsubscribeConnection?;
|
|
265
|
+
private refCounts;
|
|
266
|
+
constructor(options?: LoadManagerOptions);
|
|
267
|
+
register(input: RegisterResourceInput): ResourceRecord;
|
|
268
|
+
/**
|
|
269
|
+
* Decrements the id's reference count. Only when it reaches zero — i.e.
|
|
270
|
+
* the last mounted consumer of this resource has unmounted — does this
|
|
271
|
+
* actually abort any in-flight load, remove the scheduler's loader, and
|
|
272
|
+
* delete the registry entry. See the class doc for why.
|
|
273
|
+
*/
|
|
274
|
+
unregister(id: string): void;
|
|
275
|
+
setLoader(id: string, loader: LoaderFn): void;
|
|
276
|
+
/**
|
|
277
|
+
* Attaches the shared ViewportObserver to an element for a given
|
|
278
|
+
* resource id. Returns an unobserve function. A no-op (returning a
|
|
279
|
+
* no-op cleanup) for resources that skip viewport gating — callers
|
|
280
|
+
* don't need to branch on that themselves.
|
|
281
|
+
*/
|
|
282
|
+
observeViewport(id: string, element: Element): () => void;
|
|
283
|
+
/** Called whenever a resource's distance-from-viewport changes. */
|
|
284
|
+
reportViewportDistance(id: string, distance: number): void;
|
|
285
|
+
requestPass(): void;
|
|
286
|
+
getConnection(): ConnectionInfo;
|
|
287
|
+
getDebugSnapshot(): FastLoadDebugSnapshot;
|
|
288
|
+
setPriority(id: string, priority: Priority): void;
|
|
289
|
+
destroy(): void;
|
|
290
|
+
private logDecision;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
interface WebVitalsSnapshot {
|
|
294
|
+
/** First Contentful Paint, in ms. null until observed or if unsupported. */
|
|
295
|
+
fcp: number | null;
|
|
296
|
+
/** Largest Contentful Paint, in ms. Updates as later, larger candidates appear. */
|
|
297
|
+
lcp: number | null;
|
|
298
|
+
/** Cumulative Layout Shift score (unitless). Accumulates for the page's lifetime. */
|
|
299
|
+
cls: number | null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The metrics object returned by useFastLoadMetrics().
|
|
304
|
+
*
|
|
305
|
+
* Every field is documented with where its value actually comes from.
|
|
306
|
+
* Nothing here is fabricated: browser-observed fields are `null` until
|
|
307
|
+
* the browser has actually reported them, and internal counters are
|
|
308
|
+
* derived directly from the registry's bookkeeping (never guessed).
|
|
309
|
+
*/
|
|
310
|
+
interface FastLoadMetrics {
|
|
311
|
+
/** First Contentful Paint in ms. Browser-observed. */
|
|
312
|
+
fcp: number | null;
|
|
313
|
+
/** Largest Contentful Paint in ms. Browser-observed; may update as the page loads. */
|
|
314
|
+
lcp: number | null;
|
|
315
|
+
/** Cumulative Layout Shift score. Browser-observed. */
|
|
316
|
+
cls: number | null;
|
|
317
|
+
/** Time to first byte in ms, from Navigation Timing. Browser-observed. */
|
|
318
|
+
ttfb: number | null;
|
|
319
|
+
/** Total resources loaded for the page per the Resource Timing API. Browser-observed. */
|
|
320
|
+
totalResourcesObserved: number;
|
|
321
|
+
/** Total bytes transferred per Resource Timing (0 when transferSize is unavailable, e.g. opaque cross-origin responses). Browser-observed. */
|
|
322
|
+
totalTransferBytesObserved: number;
|
|
323
|
+
/** Resources registered with ReactFastLoad that were NOT deferred (loaded on initial pass). Internal. */
|
|
324
|
+
initialRequests: number;
|
|
325
|
+
/** Resources registered with ReactFastLoad that the scheduler deferred past their natural load time. Internal. */
|
|
326
|
+
deferredRequests: number;
|
|
327
|
+
/** Same as deferredRequests; kept for API clarity per the requested shape. Internal. */
|
|
328
|
+
deferredResources: number;
|
|
329
|
+
/** Sum of estimatedSize across deferred resources. This is an ESTIMATE — see estimatedSize on each resource. Estimated. */
|
|
330
|
+
bytesDeferred: number;
|
|
331
|
+
imagesDeferred: number;
|
|
332
|
+
videosDeferred: number;
|
|
333
|
+
audiosDeferred: number;
|
|
334
|
+
componentsDeferred: number;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* MetricsCollector composes browser-observed Web Vitals / Resource Timing
|
|
338
|
+
* with the LoadManager's internal registry counters into one snapshot.
|
|
339
|
+
* It never blends the two categories into a single "score" — the
|
|
340
|
+
* distinction is preserved field-by-field, per the module's core
|
|
341
|
+
* requirement not to present estimates as measurements.
|
|
342
|
+
*/
|
|
343
|
+
declare class MetricsCollector {
|
|
344
|
+
private loadManager;
|
|
345
|
+
private webVitals;
|
|
346
|
+
private started;
|
|
347
|
+
constructor(loadManager: LoadManager);
|
|
348
|
+
start(): void;
|
|
349
|
+
stop(): void;
|
|
350
|
+
subscribeWebVitals(listener: (snapshot: WebVitalsSnapshot) => void): () => void;
|
|
351
|
+
snapshot(): FastLoadMetrics;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface FastLoadContextValue {
|
|
355
|
+
loadManager: LoadManager;
|
|
356
|
+
metricsCollector: MetricsCollector;
|
|
357
|
+
debug: boolean;
|
|
358
|
+
}
|
|
359
|
+
interface FastLoadProviderProps {
|
|
360
|
+
children: React.ReactNode;
|
|
361
|
+
/**
|
|
362
|
+
* "adaptive" (default) lets the scheduler weigh viewport distance,
|
|
363
|
+
* connection quality, and priority together. "eager" raises the
|
|
364
|
+
* effective floor so more resources load sooner (useful for
|
|
365
|
+
* low-resource-count pages where deferral has little benefit).
|
|
366
|
+
* "conservative" is the inverse — favors deferring anything not
|
|
367
|
+
* CRITICAL/HIGH, useful for resource-heavy pages on constrained devices.
|
|
368
|
+
*/
|
|
369
|
+
strategy?: "adaptive" | "eager" | "conservative";
|
|
370
|
+
preloadDistance?: number;
|
|
371
|
+
concurrency?: number;
|
|
372
|
+
/** Enables the developer debug overlay/console reporting described in the docs. */
|
|
373
|
+
debug?: boolean;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* FastLoadProvider initializes the resource registry, scheduler, and
|
|
377
|
+
* metrics collector for the subtree, and makes them available to
|
|
378
|
+
* SmartImage/SmartVideo/LazyComponent/hooks via context. There is exactly
|
|
379
|
+
* one LoadManager per provider instance — nesting providers creates
|
|
380
|
+
* independent scheduling domains, which is intentional (e.g. for
|
|
381
|
+
* micro-frontends) but not required for typical use.
|
|
382
|
+
*/
|
|
383
|
+
declare function FastLoadProvider({ children, strategy, preloadDistance, concurrency, debug, }: FastLoadProviderProps): React.JSX.Element;
|
|
384
|
+
declare function useFastLoadContext(): FastLoadContextValue;
|
|
385
|
+
|
|
386
|
+
type PriorityProp = Priority | "auto";
|
|
387
|
+
interface SmartImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "placeholder" | "onLoad" | "onError"> {
|
|
388
|
+
src: string;
|
|
389
|
+
/** "auto" (default) resolves to NORMAL; pass an explicit Priority to override the scheduler's ranking. */
|
|
390
|
+
priority?: PriorityProp;
|
|
391
|
+
/** Shown while `src` is loading/deferred. Rendered as a background-image so layout doesn't depend on it loading. */
|
|
392
|
+
placeholder?: string;
|
|
393
|
+
/** Explicit id for scheduler bookkeeping/debugging. Defaults to `src`. */
|
|
394
|
+
resourceId?: string;
|
|
395
|
+
/** "eager" skips viewport gating entirely (use for above-the-fold images). "lazy" (default) waits for the preload zone. "auto" behaves like "lazy" but lets CRITICAL priority still force eager. */
|
|
396
|
+
strategy?: LoadingStrategy;
|
|
397
|
+
/** Estimated byte size, used only for the bytesDeferred metric — never affects loading behavior. */
|
|
398
|
+
estimatedSize?: number;
|
|
399
|
+
onLoad?: (event: React.SyntheticEvent<HTMLImageElement>) => void;
|
|
400
|
+
onError?: (event: React.SyntheticEvent<HTMLImageElement>) => void;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* A drop-in <img> replacement that registers with the ReactFastLoad
|
|
404
|
+
* scheduler instead of relying solely on the browser's native
|
|
405
|
+
* `loading="lazy"`. This adds real value over native lazy loading in two
|
|
406
|
+
* ways: (1) a configurable preload distance shared across every resource
|
|
407
|
+
* type via one scheduler, and (2) priority-aware concurrency — so, e.g.,
|
|
408
|
+
* five LOW-priority images entering view at once don't compete with a
|
|
409
|
+
* HIGH-priority one for the same network queue.
|
|
410
|
+
*
|
|
411
|
+
* For CRITICAL/HIGH priority images with strategy="eager", this renders a
|
|
412
|
+
* plain <img> immediately — it deliberately does NOT duplicate
|
|
413
|
+
* `loading="lazy"` for resources that shouldn't be lazy at all.
|
|
414
|
+
*/
|
|
415
|
+
declare function SmartImage({ src, priority, placeholder, resourceId, strategy, estimatedSize, alt, decoding, style, onLoad, onError, ...rest }: SmartImageProps): React.JSX.Element;
|
|
416
|
+
|
|
417
|
+
interface SmartVideoProps extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "src" | "poster" | "preload"> {
|
|
418
|
+
src: string;
|
|
419
|
+
poster?: string;
|
|
420
|
+
priority?: PriorityProp;
|
|
421
|
+
resourceId?: string;
|
|
422
|
+
strategy?: LoadingStrategy;
|
|
423
|
+
/** Estimated byte size (the full video, not just the poster), used only for the bytesDeferred metric — never affects loading behavior. */
|
|
424
|
+
estimatedSize?: number;
|
|
425
|
+
/**
|
|
426
|
+
* CSS `aspect-ratio` (e.g. `"16/9"`) reserved for BOTH the pre-eligible
|
|
427
|
+
* placeholder and the real `<video>` element, so swapping between them
|
|
428
|
+
* doesn't change the element's box size — that mismatch is a real
|
|
429
|
+
* layout-shift (CLS) source if left to each render its own default
|
|
430
|
+
* height. Omit only if you're already reserving space yourself via a
|
|
431
|
+
* wrapping container.
|
|
432
|
+
*/
|
|
433
|
+
aspectRatio?: string;
|
|
434
|
+
/**
|
|
435
|
+
* Explicit autoplay request. Unlike the native `autoPlay` attribute,
|
|
436
|
+
* this is only honored once the resource has actually become eligible
|
|
437
|
+
* (in view / eager), so a below-the-fold "autoplay" video never
|
|
438
|
+
* silently starts downloading and playing off-screen.
|
|
439
|
+
*/
|
|
440
|
+
autoplay?: boolean;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* A <video> wrapper that avoids downloading video data before it's
|
|
444
|
+
* needed. Below the preload zone, it renders only the poster (if given)
|
|
445
|
+
* with `preload="none"`; once eligible, it mounts a real <video> with
|
|
446
|
+
* `preload="metadata"` (or "auto" for CRITICAL/HIGH priority) and, if
|
|
447
|
+
* `autoplay` was requested, attempts to play once mounted.
|
|
448
|
+
*
|
|
449
|
+
* We never fetch the video bytes ourselves — the browser's own
|
|
450
|
+
* range-request/streaming behavior handles that once the element mounts,
|
|
451
|
+
* per the "work with browser-native mechanisms" requirement.
|
|
452
|
+
*/
|
|
453
|
+
declare function SmartVideo({ src, poster, priority, resourceId, strategy, estimatedSize, aspectRatio, autoplay, muted, controls, style, ...rest }: SmartVideoProps): React.JSX.Element;
|
|
454
|
+
|
|
455
|
+
interface SmartAudioProps extends Omit<React.AudioHTMLAttributes<HTMLAudioElement>, "src" | "preload"> {
|
|
456
|
+
src: string;
|
|
457
|
+
priority?: PriorityProp;
|
|
458
|
+
resourceId?: string;
|
|
459
|
+
strategy?: LoadingStrategy;
|
|
460
|
+
/** Estimated byte size, used only for the bytesDeferred metric — never affects loading behavior. */
|
|
461
|
+
estimatedSize?: number;
|
|
462
|
+
/** Track title, shown on the pre-eligible placeholder's play affordance. Optional. */
|
|
463
|
+
label?: string;
|
|
464
|
+
/**
|
|
465
|
+
* Same semantics as SmartVideo's `autoplay`: only honored once the
|
|
466
|
+
* resource is actually eligible, and implies `muted` — most browsers
|
|
467
|
+
* block unmuted audio autoplay outright, so this exists mainly for
|
|
468
|
+
* background/ambient-track use cases where that's acceptable.
|
|
469
|
+
*/
|
|
470
|
+
autoplay?: boolean;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* An <audio> wrapper for songs/podcasts/sound effects — the audio
|
|
474
|
+
* equivalent of SmartVideo. Below the preload zone it renders a small
|
|
475
|
+
* click/keyboard-activatable placeholder instead of a real <audio>
|
|
476
|
+
* element, so nothing downloads early. Once eligible, it mounts a real
|
|
477
|
+
* <audio> with `preload="auto"` for CRITICAL/HIGH priority or
|
|
478
|
+
* `preload="metadata"` otherwise, and lets the browser's own streaming
|
|
479
|
+
* behavior handle the actual byte transfer — this library never fetches
|
|
480
|
+
* audio data itself.
|
|
481
|
+
*/
|
|
482
|
+
declare function SmartAudio({ src, priority, resourceId, strategy, estimatedSize, label, autoplay, muted, controls, ...rest }: SmartAudioProps): React.JSX.Element;
|
|
483
|
+
|
|
484
|
+
type DynamicImport<T> = () => Promise<{
|
|
485
|
+
default: T;
|
|
486
|
+
}>;
|
|
487
|
+
|
|
488
|
+
interface LazyComponentOptions {
|
|
489
|
+
priority?: Priority;
|
|
490
|
+
/** Overrides the provider's preloadDistance for this component only. Currently informational — actual gating uses the provider's shared observer distance; a per-instance override is planned for a future minor version. */
|
|
491
|
+
preloadDistance?: number;
|
|
492
|
+
strategy?: LoadingStrategy;
|
|
493
|
+
/** Rendered while the component hasn't become eligible to load yet. Defaults to null (renders nothing). */
|
|
494
|
+
placeholder?: React.ReactNode;
|
|
495
|
+
/** Rendered by Suspense while the dynamic import is in flight. Defaults to `placeholder`. */
|
|
496
|
+
fallback?: React.ReactNode;
|
|
497
|
+
resourceId?: string;
|
|
498
|
+
/** Estimated byte size of the chunk this component imports, used only for the bytesDeferred metric — never affects loading behavior. */
|
|
499
|
+
estimatedSize?: number;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Wraps React.lazy + Suspense so that the dynamic import() itself is not
|
|
503
|
+
* triggered until the scheduler decides this component is eligible
|
|
504
|
+
* (in the preload zone, or eager/CRITICAL). Plain React.lazy has no
|
|
505
|
+
* concept of "not yet" — it fetches as soon as it's first rendered. This
|
|
506
|
+
* is the actual gap LazyComponent closes over ordinary code splitting.
|
|
507
|
+
*/
|
|
508
|
+
declare function lazyComponent<P extends object>(importFn: DynamicImport<React.ComponentType<P>>, options?: LazyComponentOptions): React.ComponentType<P>;
|
|
509
|
+
|
|
510
|
+
interface UseLazyLoadOptions {
|
|
511
|
+
id: string;
|
|
512
|
+
type: ResourceType;
|
|
513
|
+
priority: Priority;
|
|
514
|
+
strategy: LoadingStrategy;
|
|
515
|
+
estimatedSize?: number;
|
|
516
|
+
meta?: Record<string, unknown>;
|
|
517
|
+
/** The actual load function (e.g. loadImage, loadVideo, dynamic import wrapper). */
|
|
518
|
+
loader: LoaderFn;
|
|
519
|
+
}
|
|
520
|
+
interface UseLazyLoadResult {
|
|
521
|
+
/** Attach to the DOM node whose viewport position should be observed. */
|
|
522
|
+
ref: (node: Element | null) => void;
|
|
523
|
+
state: ResourceState;
|
|
524
|
+
record: ResourceRecord | undefined;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Shared internal hook: registers a resource with the LoadManager, wires
|
|
528
|
+
* viewport observation up to the provider's single shared
|
|
529
|
+
* ViewportObserver (unless the strategy is "eager", which skips
|
|
530
|
+
* observation entirely and loads immediately), and tracks the resource's
|
|
531
|
+
* state for re-rendering. SmartImage, SmartVideo, and SmartAudio are thin
|
|
532
|
+
* wrappers around this hook plus their own rendering concerns.
|
|
533
|
+
*
|
|
534
|
+
* Request deduplication: `id` defaults to `${type}:${src}` in the Smart*
|
|
535
|
+
* components, so two mounted instances with the same `src` resolve to the
|
|
536
|
+
* SAME resource id here — LoadManager.register()/unregister() are
|
|
537
|
+
* reference-counted specifically so that works correctly (the resource
|
|
538
|
+
* isn't torn down until the LAST consumer unmounts). Pass an explicit
|
|
539
|
+
* `resourceId` to opt out when two elements with the same src genuinely
|
|
540
|
+
* need independent scheduling.
|
|
541
|
+
*/
|
|
542
|
+
declare function useLazyLoad(options: UseLazyLoadOptions): UseLazyLoadResult;
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Lets a component update a registered resource's priority after the
|
|
546
|
+
* fact — e.g. bumping an image from LOW to HIGH once the user hovers
|
|
547
|
+
* over a card, or once a carousel slide becomes the active one.
|
|
548
|
+
*/
|
|
549
|
+
declare function usePriority(resourceId: string, priority: Priority): void;
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Returns a live-updating FastLoadMetrics snapshot. Re-renders when:
|
|
553
|
+
* - a browser Web Vitals entry arrives (FCP/LCP/CLS), or
|
|
554
|
+
* - the resource registry changes (a resource loads, defers, etc.)
|
|
555
|
+
*
|
|
556
|
+
* The returned object always reflects real state at the time of the
|
|
557
|
+
* render that produced it — see FastLoadMetrics' field-level docs for
|
|
558
|
+
* which values are browser-observed vs. internal bookkeeping.
|
|
559
|
+
*/
|
|
560
|
+
declare function useFastLoadMetrics(): FastLoadMetrics;
|
|
561
|
+
|
|
562
|
+
export { type ConnectionInfo, type FastLoadContextValue, type FastLoadDebugSnapshot, type FastLoadMetrics, FastLoadProvider, type FastLoadProviderProps, type LazyComponentOptions, LoadManager, type LoadingStrategy, type Priority, PriorityEngine, type PriorityProp, type ResourceRecord, ResourceRegistry, type ResourceState, type ResourceType, Scheduler, type SchedulerDecision, SmartAudio, type SmartAudioProps, SmartImage, type SmartImageProps, SmartVideo, type SmartVideoProps, type UseLazyLoadOptions, type UseLazyLoadResult, type WebVitalsSnapshot, lazyComponent, useFastLoadContext, useFastLoadMetrics, useLazyLoad, usePriority };
|