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.
@@ -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 };