@sc4rfurryx/proteusjs 1.1.1 → 2.0.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/README.md +684 -899
- package/dist/.tsbuildinfo +1 -1
- package/dist/modules/a11y-audit.d.ts +1 -1
- package/dist/modules/a11y-audit.esm.js +3 -3
- package/dist/modules/a11y-primitives.d.ts +2 -2
- package/dist/modules/a11y-primitives.esm.js +2 -2
- package/dist/modules/a11y-primitives.esm.js.map +1 -1
- package/dist/modules/anchor.d.ts +1 -1
- package/dist/modules/anchor.esm.js +2 -2
- package/dist/modules/container.d.ts +1 -1
- package/dist/modules/container.esm.js +34 -34
- package/dist/modules/container.esm.js.map +1 -1
- package/dist/modules/perf.d.ts +1 -1
- package/dist/modules/perf.esm.js +2 -2
- package/dist/modules/popover.d.ts +1 -1
- package/dist/modules/popover.esm.js +2 -2
- package/dist/modules/scroll.d.ts +1 -1
- package/dist/modules/scroll.esm.js +14 -14
- package/dist/modules/scroll.esm.js.map +1 -1
- package/dist/modules/transitions.d.ts +1 -1
- package/dist/modules/transitions.esm.js +12 -12
- package/dist/modules/transitions.esm.js.map +1 -1
- package/dist/modules/typography.d.ts +1 -1
- package/dist/modules/typography.esm.js +2 -2
- package/dist/proteus.cjs.js +68 -68
- package/dist/proteus.cjs.js.map +1 -1
- package/dist/proteus.d.ts +13 -13
- package/dist/proteus.esm.js +68 -68
- package/dist/proteus.esm.js.map +1 -1
- package/dist/proteus.esm.min.js +2 -2
- package/dist/proteus.esm.min.js.map +1 -1
- package/dist/proteus.js +68 -68
- package/dist/proteus.js.map +1 -1
- package/dist/proteus.min.js +2 -2
- package/dist/proteus.min.js.map +1 -1
- package/package.json +40 -8
- package/src/adapters/react.ts +607 -264
- package/src/adapters/svelte.ts +321 -321
- package/src/adapters/vue.ts +268 -268
- package/src/core/ProteusJS.ts +6 -6
- package/src/index.ts +2 -2
- package/src/modules/a11y-audit/index.ts +84 -84
- package/src/modules/a11y-primitives/index.ts +151 -151
- package/src/modules/anchor/index.ts +259 -259
- package/src/modules/container/index.ts +230 -230
- package/src/modules/perf/index.ts +291 -291
- package/src/modules/popover/index.ts +238 -238
- package/src/modules/scroll/index.ts +251 -251
- package/src/modules/transitions/index.ts +145 -145
- package/src/modules/typography/index.ts +239 -239
- package/src/utils/version.ts +1 -1
- package/dist/adapters/react.d.ts +0 -140
- package/dist/adapters/react.esm.js +0 -849
- package/dist/adapters/react.esm.js.map +0 -1
- package/dist/adapters/svelte.d.ts +0 -181
- package/dist/adapters/svelte.esm.js +0 -909
- package/dist/adapters/svelte.esm.js.map +0 -1
- package/dist/adapters/vue.d.ts +0 -205
- package/dist/adapters/vue.esm.js +0 -873
- package/dist/adapters/vue.esm.js.map +0 -1
|
@@ -1,291 +1,291 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @sc4rfurryx/proteusjs/perf
|
|
3
|
-
* Performance guardrails and CWV-friendly patterns
|
|
4
|
-
*
|
|
5
|
-
* @version
|
|
6
|
-
* @author sc4rfurry
|
|
7
|
-
* @license MIT
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface SpeculationOptions {
|
|
11
|
-
prerender?: string[];
|
|
12
|
-
prefetch?: string[];
|
|
13
|
-
sameOriginOnly?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ContentVisibilityOptions {
|
|
17
|
-
containIntrinsicSize?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Apply content-visibility for performance optimization
|
|
22
|
-
*/
|
|
23
|
-
export function contentVisibility(
|
|
24
|
-
selector: string | Element,
|
|
25
|
-
mode: 'auto' | 'hidden' = 'auto',
|
|
26
|
-
opts: ContentVisibilityOptions = {}
|
|
27
|
-
): void {
|
|
28
|
-
const elements = typeof selector === 'string'
|
|
29
|
-
? document.querySelectorAll(selector)
|
|
30
|
-
: [selector];
|
|
31
|
-
|
|
32
|
-
const { containIntrinsicSize = '1000px 400px' } = opts;
|
|
33
|
-
|
|
34
|
-
elements.forEach(element => {
|
|
35
|
-
const el = element as HTMLElement;
|
|
36
|
-
el.style.contentVisibility = mode;
|
|
37
|
-
|
|
38
|
-
if (mode === 'auto') {
|
|
39
|
-
el.style.containIntrinsicSize = containIntrinsicSize;
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Set fetch priority for resources
|
|
46
|
-
*/
|
|
47
|
-
export function fetchPriority(
|
|
48
|
-
selector: string | Element,
|
|
49
|
-
priority: 'high' | 'low' | 'auto'
|
|
50
|
-
): void {
|
|
51
|
-
const elements = typeof selector === 'string'
|
|
52
|
-
? document.querySelectorAll(selector)
|
|
53
|
-
: [selector];
|
|
54
|
-
|
|
55
|
-
elements.forEach(element => {
|
|
56
|
-
if (element instanceof HTMLImageElement ||
|
|
57
|
-
element instanceof HTMLLinkElement ||
|
|
58
|
-
element instanceof HTMLScriptElement) {
|
|
59
|
-
(element as HTMLImageElement | HTMLLinkElement | HTMLScriptElement & { fetchPriority?: string }).fetchPriority = priority;
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Set up speculation rules for prerendering and prefetching
|
|
66
|
-
*/
|
|
67
|
-
export function speculate(opts: SpeculationOptions): void {
|
|
68
|
-
const { prerender = [], prefetch = [], sameOriginOnly = true } = opts;
|
|
69
|
-
|
|
70
|
-
// Check for Speculation Rules API support
|
|
71
|
-
if (!('supports' in HTMLScriptElement && HTMLScriptElement.supports('speculationrules'))) {
|
|
72
|
-
console.warn('Speculation Rules API not supported');
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const rules: any = {};
|
|
77
|
-
|
|
78
|
-
if (prerender.length > 0) {
|
|
79
|
-
rules.prerender = prerender.map(url => {
|
|
80
|
-
const rule: any = { where: { href_matches: url } };
|
|
81
|
-
if (sameOriginOnly) {
|
|
82
|
-
rule.where.href_matches = new URL(url, window.location.origin).href;
|
|
83
|
-
}
|
|
84
|
-
return rule;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (prefetch.length > 0) {
|
|
89
|
-
rules.prefetch = prefetch.map(url => {
|
|
90
|
-
const rule: any = { where: { href_matches: url } };
|
|
91
|
-
if (sameOriginOnly) {
|
|
92
|
-
rule.where.href_matches = new URL(url, window.location.origin).href;
|
|
93
|
-
}
|
|
94
|
-
return rule;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (Object.keys(rules).length === 0) return;
|
|
99
|
-
|
|
100
|
-
// Create and inject speculation rules script
|
|
101
|
-
const script = document.createElement('script');
|
|
102
|
-
script.type = 'speculationrules';
|
|
103
|
-
script.textContent = JSON.stringify(rules);
|
|
104
|
-
document.head.appendChild(script);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Yield to browser using scheduler.yield or postTask when available
|
|
109
|
-
*/
|
|
110
|
-
export async function yieldToBrowser(): Promise<void> {
|
|
111
|
-
// Use scheduler.yield if available (Chrome 115+)
|
|
112
|
-
if ('scheduler' in window && 'yield' in (window as any).scheduler) {
|
|
113
|
-
return (window as any).scheduler.yield();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Use scheduler.postTask if available
|
|
117
|
-
if ('scheduler' in window && 'postTask' in (window as any).scheduler) {
|
|
118
|
-
return new Promise(resolve => {
|
|
119
|
-
(window as any).scheduler.postTask(resolve, { priority: 'user-blocking' });
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Fallback to setTimeout
|
|
124
|
-
return new Promise(resolve => {
|
|
125
|
-
setTimeout(resolve, 0);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Optimize images with loading and decoding hints
|
|
131
|
-
*/
|
|
132
|
-
export function optimizeImages(selector: string | Element = 'img'): void {
|
|
133
|
-
const images = typeof selector === 'string'
|
|
134
|
-
? document.querySelectorAll(selector)
|
|
135
|
-
: [selector];
|
|
136
|
-
|
|
137
|
-
images.forEach(img => {
|
|
138
|
-
if (!(img instanceof HTMLImageElement)) return;
|
|
139
|
-
|
|
140
|
-
// Set loading attribute if not already set
|
|
141
|
-
if (!img.hasAttribute('loading')) {
|
|
142
|
-
const rect = img.getBoundingClientRect();
|
|
143
|
-
const isAboveFold = rect.top < window.innerHeight;
|
|
144
|
-
img.loading = isAboveFold ? 'eager' : 'lazy';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Set decoding hint
|
|
148
|
-
if (!img.hasAttribute('decoding')) {
|
|
149
|
-
img.decoding = 'async';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Set fetch priority for above-fold images
|
|
153
|
-
if (!img.hasAttribute('fetchpriority')) {
|
|
154
|
-
const rect = img.getBoundingClientRect();
|
|
155
|
-
const isAboveFold = rect.top < window.innerHeight;
|
|
156
|
-
if (isAboveFold) {
|
|
157
|
-
(img as any).fetchPriority = 'high';
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Preload critical resources
|
|
165
|
-
*/
|
|
166
|
-
export function preloadCritical(resources: Array<{ href: string; as: string; type?: string }>): void {
|
|
167
|
-
resources.forEach(({ href, as, type }) => {
|
|
168
|
-
// Check if already preloaded
|
|
169
|
-
const existing = document.querySelector(`link[rel="preload"][href="${href}"]`);
|
|
170
|
-
if (existing) return;
|
|
171
|
-
|
|
172
|
-
const link = document.createElement('link');
|
|
173
|
-
link.rel = 'preload';
|
|
174
|
-
link.href = href;
|
|
175
|
-
link.as = as;
|
|
176
|
-
if (type) {
|
|
177
|
-
link.type = type;
|
|
178
|
-
}
|
|
179
|
-
document.head.appendChild(link);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Measure and report Core Web Vitals
|
|
185
|
-
*/
|
|
186
|
-
export function measureCWV(): Promise<{ lcp?: number; fid?: number; cls?: number }> {
|
|
187
|
-
return new Promise(resolve => {
|
|
188
|
-
const metrics: { lcp?: number; fid?: number; cls?: number } = {};
|
|
189
|
-
let metricsCount = 0;
|
|
190
|
-
const totalMetrics = 3;
|
|
191
|
-
|
|
192
|
-
const checkComplete = () => {
|
|
193
|
-
metricsCount++;
|
|
194
|
-
if (metricsCount >= totalMetrics) {
|
|
195
|
-
resolve(metrics);
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// LCP (Largest Contentful Paint)
|
|
200
|
-
if ('PerformanceObserver' in window) {
|
|
201
|
-
try {
|
|
202
|
-
const lcpObserver = new PerformanceObserver(list => {
|
|
203
|
-
const entries = list.getEntries();
|
|
204
|
-
const lastEntry = entries[entries.length - 1] as any;
|
|
205
|
-
metrics.lcp = lastEntry.startTime;
|
|
206
|
-
});
|
|
207
|
-
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
|
|
208
|
-
|
|
209
|
-
// Stop observing after 10 seconds
|
|
210
|
-
setTimeout(() => {
|
|
211
|
-
lcpObserver.disconnect();
|
|
212
|
-
checkComplete();
|
|
213
|
-
}, 10000);
|
|
214
|
-
} catch {
|
|
215
|
-
checkComplete();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// FID (First Input Delay)
|
|
219
|
-
try {
|
|
220
|
-
const fidObserver = new PerformanceObserver(list => {
|
|
221
|
-
const entries = list.getEntries();
|
|
222
|
-
entries.forEach((entry: any) => {
|
|
223
|
-
metrics.fid = entry.processingStart - entry.startTime;
|
|
224
|
-
});
|
|
225
|
-
fidObserver.disconnect();
|
|
226
|
-
checkComplete();
|
|
227
|
-
});
|
|
228
|
-
fidObserver.observe({ entryTypes: ['first-input'] });
|
|
229
|
-
|
|
230
|
-
// If no input after 10 seconds, consider FID as 0
|
|
231
|
-
setTimeout(() => {
|
|
232
|
-
if (metrics.fid === undefined) {
|
|
233
|
-
metrics.fid = 0;
|
|
234
|
-
fidObserver.disconnect();
|
|
235
|
-
checkComplete();
|
|
236
|
-
}
|
|
237
|
-
}, 10000);
|
|
238
|
-
} catch {
|
|
239
|
-
checkComplete();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// CLS (Cumulative Layout Shift)
|
|
243
|
-
try {
|
|
244
|
-
let clsValue = 0;
|
|
245
|
-
const clsObserver = new PerformanceObserver(list => {
|
|
246
|
-
list.getEntries().forEach((entry: any) => {
|
|
247
|
-
if (!entry.hadRecentInput) {
|
|
248
|
-
clsValue += entry.value;
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
metrics.cls = clsValue;
|
|
252
|
-
});
|
|
253
|
-
clsObserver.observe({ entryTypes: ['layout-shift'] });
|
|
254
|
-
|
|
255
|
-
// Stop observing after 10 seconds
|
|
256
|
-
setTimeout(() => {
|
|
257
|
-
clsObserver.disconnect();
|
|
258
|
-
checkComplete();
|
|
259
|
-
}, 10000);
|
|
260
|
-
} catch {
|
|
261
|
-
checkComplete();
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
264
|
-
// Fallback if PerformanceObserver is not supported
|
|
265
|
-
setTimeout(() => resolve(metrics), 100);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Export boost object to match usage examples in upgrade spec
|
|
271
|
-
export const boost = {
|
|
272
|
-
contentVisibility,
|
|
273
|
-
fetchPriority,
|
|
274
|
-
speculate,
|
|
275
|
-
yieldToBrowser,
|
|
276
|
-
optimizeImages,
|
|
277
|
-
preloadCritical,
|
|
278
|
-
measureCWV
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
// Export all functions as named exports and default object
|
|
282
|
-
export default {
|
|
283
|
-
contentVisibility,
|
|
284
|
-
fetchPriority,
|
|
285
|
-
speculate,
|
|
286
|
-
yieldToBrowser,
|
|
287
|
-
optimizeImages,
|
|
288
|
-
preloadCritical,
|
|
289
|
-
measureCWV,
|
|
290
|
-
boost
|
|
291
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @sc4rfurryx/proteusjs/perf
|
|
3
|
+
* Performance guardrails and CWV-friendly patterns
|
|
4
|
+
*
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
* @author sc4rfurry
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface SpeculationOptions {
|
|
11
|
+
prerender?: string[];
|
|
12
|
+
prefetch?: string[];
|
|
13
|
+
sameOriginOnly?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ContentVisibilityOptions {
|
|
17
|
+
containIntrinsicSize?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Apply content-visibility for performance optimization
|
|
22
|
+
*/
|
|
23
|
+
export function contentVisibility(
|
|
24
|
+
selector: string | Element,
|
|
25
|
+
mode: 'auto' | 'hidden' = 'auto',
|
|
26
|
+
opts: ContentVisibilityOptions = {}
|
|
27
|
+
): void {
|
|
28
|
+
const elements = typeof selector === 'string'
|
|
29
|
+
? document.querySelectorAll(selector)
|
|
30
|
+
: [selector];
|
|
31
|
+
|
|
32
|
+
const { containIntrinsicSize = '1000px 400px' } = opts;
|
|
33
|
+
|
|
34
|
+
elements.forEach(element => {
|
|
35
|
+
const el = element as HTMLElement;
|
|
36
|
+
el.style.contentVisibility = mode;
|
|
37
|
+
|
|
38
|
+
if (mode === 'auto') {
|
|
39
|
+
el.style.containIntrinsicSize = containIntrinsicSize;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set fetch priority for resources
|
|
46
|
+
*/
|
|
47
|
+
export function fetchPriority(
|
|
48
|
+
selector: string | Element,
|
|
49
|
+
priority: 'high' | 'low' | 'auto'
|
|
50
|
+
): void {
|
|
51
|
+
const elements = typeof selector === 'string'
|
|
52
|
+
? document.querySelectorAll(selector)
|
|
53
|
+
: [selector];
|
|
54
|
+
|
|
55
|
+
elements.forEach(element => {
|
|
56
|
+
if (element instanceof HTMLImageElement ||
|
|
57
|
+
element instanceof HTMLLinkElement ||
|
|
58
|
+
element instanceof HTMLScriptElement) {
|
|
59
|
+
(element as HTMLImageElement | HTMLLinkElement | HTMLScriptElement & { fetchPriority?: string }).fetchPriority = priority;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set up speculation rules for prerendering and prefetching
|
|
66
|
+
*/
|
|
67
|
+
export function speculate(opts: SpeculationOptions): void {
|
|
68
|
+
const { prerender = [], prefetch = [], sameOriginOnly = true } = opts;
|
|
69
|
+
|
|
70
|
+
// Check for Speculation Rules API support
|
|
71
|
+
if (!('supports' in HTMLScriptElement && HTMLScriptElement.supports('speculationrules'))) {
|
|
72
|
+
console.warn('Speculation Rules API not supported');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rules: any = {};
|
|
77
|
+
|
|
78
|
+
if (prerender.length > 0) {
|
|
79
|
+
rules.prerender = prerender.map(url => {
|
|
80
|
+
const rule: any = { where: { href_matches: url } };
|
|
81
|
+
if (sameOriginOnly) {
|
|
82
|
+
rule.where.href_matches = new URL(url, window.location.origin).href;
|
|
83
|
+
}
|
|
84
|
+
return rule;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (prefetch.length > 0) {
|
|
89
|
+
rules.prefetch = prefetch.map(url => {
|
|
90
|
+
const rule: any = { where: { href_matches: url } };
|
|
91
|
+
if (sameOriginOnly) {
|
|
92
|
+
rule.where.href_matches = new URL(url, window.location.origin).href;
|
|
93
|
+
}
|
|
94
|
+
return rule;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (Object.keys(rules).length === 0) return;
|
|
99
|
+
|
|
100
|
+
// Create and inject speculation rules script
|
|
101
|
+
const script = document.createElement('script');
|
|
102
|
+
script.type = 'speculationrules';
|
|
103
|
+
script.textContent = JSON.stringify(rules);
|
|
104
|
+
document.head.appendChild(script);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Yield to browser using scheduler.yield or postTask when available
|
|
109
|
+
*/
|
|
110
|
+
export async function yieldToBrowser(): Promise<void> {
|
|
111
|
+
// Use scheduler.yield if available (Chrome 115+)
|
|
112
|
+
if ('scheduler' in window && 'yield' in (window as any).scheduler) {
|
|
113
|
+
return (window as any).scheduler.yield();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Use scheduler.postTask if available
|
|
117
|
+
if ('scheduler' in window && 'postTask' in (window as any).scheduler) {
|
|
118
|
+
return new Promise(resolve => {
|
|
119
|
+
(window as any).scheduler.postTask(resolve, { priority: 'user-blocking' });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback to setTimeout
|
|
124
|
+
return new Promise(resolve => {
|
|
125
|
+
setTimeout(resolve, 0);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Optimize images with loading and decoding hints
|
|
131
|
+
*/
|
|
132
|
+
export function optimizeImages(selector: string | Element = 'img'): void {
|
|
133
|
+
const images = typeof selector === 'string'
|
|
134
|
+
? document.querySelectorAll(selector)
|
|
135
|
+
: [selector];
|
|
136
|
+
|
|
137
|
+
images.forEach(img => {
|
|
138
|
+
if (!(img instanceof HTMLImageElement)) return;
|
|
139
|
+
|
|
140
|
+
// Set loading attribute if not already set
|
|
141
|
+
if (!img.hasAttribute('loading')) {
|
|
142
|
+
const rect = img.getBoundingClientRect();
|
|
143
|
+
const isAboveFold = rect.top < window.innerHeight;
|
|
144
|
+
img.loading = isAboveFold ? 'eager' : 'lazy';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Set decoding hint
|
|
148
|
+
if (!img.hasAttribute('decoding')) {
|
|
149
|
+
img.decoding = 'async';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Set fetch priority for above-fold images
|
|
153
|
+
if (!img.hasAttribute('fetchpriority')) {
|
|
154
|
+
const rect = img.getBoundingClientRect();
|
|
155
|
+
const isAboveFold = rect.top < window.innerHeight;
|
|
156
|
+
if (isAboveFold) {
|
|
157
|
+
(img as any).fetchPriority = 'high';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Preload critical resources
|
|
165
|
+
*/
|
|
166
|
+
export function preloadCritical(resources: Array<{ href: string; as: string; type?: string }>): void {
|
|
167
|
+
resources.forEach(({ href, as, type }) => {
|
|
168
|
+
// Check if already preloaded
|
|
169
|
+
const existing = document.querySelector(`link[rel="preload"][href="${href}"]`);
|
|
170
|
+
if (existing) return;
|
|
171
|
+
|
|
172
|
+
const link = document.createElement('link');
|
|
173
|
+
link.rel = 'preload';
|
|
174
|
+
link.href = href;
|
|
175
|
+
link.as = as;
|
|
176
|
+
if (type) {
|
|
177
|
+
link.type = type;
|
|
178
|
+
}
|
|
179
|
+
document.head.appendChild(link);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Measure and report Core Web Vitals
|
|
185
|
+
*/
|
|
186
|
+
export function measureCWV(): Promise<{ lcp?: number; fid?: number; cls?: number }> {
|
|
187
|
+
return new Promise(resolve => {
|
|
188
|
+
const metrics: { lcp?: number; fid?: number; cls?: number } = {};
|
|
189
|
+
let metricsCount = 0;
|
|
190
|
+
const totalMetrics = 3;
|
|
191
|
+
|
|
192
|
+
const checkComplete = () => {
|
|
193
|
+
metricsCount++;
|
|
194
|
+
if (metricsCount >= totalMetrics) {
|
|
195
|
+
resolve(metrics);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// LCP (Largest Contentful Paint)
|
|
200
|
+
if ('PerformanceObserver' in window) {
|
|
201
|
+
try {
|
|
202
|
+
const lcpObserver = new PerformanceObserver(list => {
|
|
203
|
+
const entries = list.getEntries();
|
|
204
|
+
const lastEntry = entries[entries.length - 1] as any;
|
|
205
|
+
metrics.lcp = lastEntry.startTime;
|
|
206
|
+
});
|
|
207
|
+
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
|
|
208
|
+
|
|
209
|
+
// Stop observing after 10 seconds
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
lcpObserver.disconnect();
|
|
212
|
+
checkComplete();
|
|
213
|
+
}, 10000);
|
|
214
|
+
} catch {
|
|
215
|
+
checkComplete();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// FID (First Input Delay)
|
|
219
|
+
try {
|
|
220
|
+
const fidObserver = new PerformanceObserver(list => {
|
|
221
|
+
const entries = list.getEntries();
|
|
222
|
+
entries.forEach((entry: any) => {
|
|
223
|
+
metrics.fid = entry.processingStart - entry.startTime;
|
|
224
|
+
});
|
|
225
|
+
fidObserver.disconnect();
|
|
226
|
+
checkComplete();
|
|
227
|
+
});
|
|
228
|
+
fidObserver.observe({ entryTypes: ['first-input'] });
|
|
229
|
+
|
|
230
|
+
// If no input after 10 seconds, consider FID as 0
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
if (metrics.fid === undefined) {
|
|
233
|
+
metrics.fid = 0;
|
|
234
|
+
fidObserver.disconnect();
|
|
235
|
+
checkComplete();
|
|
236
|
+
}
|
|
237
|
+
}, 10000);
|
|
238
|
+
} catch {
|
|
239
|
+
checkComplete();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// CLS (Cumulative Layout Shift)
|
|
243
|
+
try {
|
|
244
|
+
let clsValue = 0;
|
|
245
|
+
const clsObserver = new PerformanceObserver(list => {
|
|
246
|
+
list.getEntries().forEach((entry: any) => {
|
|
247
|
+
if (!entry.hadRecentInput) {
|
|
248
|
+
clsValue += entry.value;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
metrics.cls = clsValue;
|
|
252
|
+
});
|
|
253
|
+
clsObserver.observe({ entryTypes: ['layout-shift'] });
|
|
254
|
+
|
|
255
|
+
// Stop observing after 10 seconds
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
clsObserver.disconnect();
|
|
258
|
+
checkComplete();
|
|
259
|
+
}, 10000);
|
|
260
|
+
} catch {
|
|
261
|
+
checkComplete();
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
// Fallback if PerformanceObserver is not supported
|
|
265
|
+
setTimeout(() => resolve(metrics), 100);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Export boost object to match usage examples in upgrade spec
|
|
271
|
+
export const boost = {
|
|
272
|
+
contentVisibility,
|
|
273
|
+
fetchPriority,
|
|
274
|
+
speculate,
|
|
275
|
+
yieldToBrowser,
|
|
276
|
+
optimizeImages,
|
|
277
|
+
preloadCritical,
|
|
278
|
+
measureCWV
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Export all functions as named exports and default object
|
|
282
|
+
export default {
|
|
283
|
+
contentVisibility,
|
|
284
|
+
fetchPriority,
|
|
285
|
+
speculate,
|
|
286
|
+
yieldToBrowser,
|
|
287
|
+
optimizeImages,
|
|
288
|
+
preloadCritical,
|
|
289
|
+
measureCWV,
|
|
290
|
+
boost
|
|
291
|
+
};
|