Skip to content

Commit dad6fcb

Browse files
committed
feat: add multi-tier caching to Icon component
- Add criticalIcons.ts: 50 most-used icons inlined as SVG strings for instant synchronous rendering (zero network requests) - Add iconCache.ts: memory Map + sessionStorage persistence layer with hydration on module load and try/catch for all storage ops - Update Icon.tsx with 3-tier lookup: critical icons → memory/session cache → HTTP fetch - Increase MAX_CONCURRENT_FETCHES from 4 to 10 - Add scheduleIdlePreload() for background preloading in batches of 5 using requestIdleCallback (with setTimeout fallback) - Update preloadIcons to skip icons in critical set or cache - Delegate clearIconCache to centralized clearCache()
1 parent 87d591b commit dad6fcb

File tree

3 files changed

+168
-19
lines changed

3 files changed

+168
-19
lines changed

src/components/ui/Icon.tsx

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, createSignal, createEffect, JSX, splitProps, onCleanup } from 'solid-js';
2-
3-
// Cache for loaded SVG content
4-
const svgCache = new Map<string, string>();
2+
import { getIconName } from '../../utils/iconMap';
3+
import { criticalIcons } from '../../utils/criticalIcons';
4+
import { getFromCache, addToCache, hasInCache, clearCache } from '../../utils/iconCache';
55

66
// Cache for failed icons to avoid retrying
77
const failedIcons = new Set<string>();
@@ -11,7 +11,7 @@ const pendingFetches = new Map<string, Promise<string | null>>();
1111

1212
// Request queue to limit concurrent fetches
1313
let activeFetches = 0;
14-
const MAX_CONCURRENT_FETCHES = 4;
14+
const MAX_CONCURRENT_FETCHES = 10;
1515
const fetchQueue: Array<() => void> = [];
1616

1717
function processQueue() {
@@ -21,9 +21,6 @@ function processQueue() {
2121
}
2222
}
2323

24-
// Icon name mapping from various libraries to Font Awesome
25-
import { getIconName } from '../../utils/iconMap';
26-
2724
export interface IconProps extends JSX.SvgSVGAttributes<SVGSVGElement> {
2825
name: string;
2926
size?: number | string;
@@ -59,9 +56,10 @@ export const Icon: Component<IconProps> = (props) => {
5956
return null;
6057
}
6158

62-
// Check cache
63-
if (svgCache.has(iconName)) {
64-
return svgCache.get(iconName)!;
59+
// Check memory/session cache
60+
const cached = getFromCache(iconName);
61+
if (cached) {
62+
return cached;
6563
}
6664

6765
// Check if already fetching
@@ -84,7 +82,7 @@ export const Icon: Component<IconProps> = (props) => {
8482
}
8583

8684
const svgText = await response.text();
87-
svgCache.set(iconName, svgText);
85+
addToCache(iconName, svgText);
8886
resolve(svgText);
8987
} catch {
9088
failedIcons.add(iconName);
@@ -114,9 +112,17 @@ export const Icon: Component<IconProps> = (props) => {
114112
return;
115113
}
116114

117-
// Check cache synchronously first
118-
if (svgCache.has(iconName)) {
119-
parseSvgContent(svgCache.get(iconName)!);
115+
// Tier 1: critical icons (inlined, synchronous)
116+
const inlined = criticalIcons.get(iconName);
117+
if (inlined) {
118+
parseSvgContent(inlined);
119+
return;
120+
}
121+
122+
// Tier 2: memory/session cache (synchronous)
123+
const cached = getFromCache(iconName);
124+
if (cached) {
125+
parseSvgContent(cached);
120126
return;
121127
}
122128

@@ -126,7 +132,7 @@ export const Icon: Component<IconProps> = (props) => {
126132
return;
127133
}
128134

129-
// Load async
135+
// Tier 3: HTTP fetch (async)
130136
loadIcon(iconName).then((svgText) => {
131137
if (!mounted) return;
132138
if (svgText) {
@@ -190,28 +196,69 @@ export const Icon: Component<IconProps> = (props) => {
190196
export const preloadIcons = async (iconNames: string[]): Promise<void> => {
191197
const promises = iconNames.map(async (name) => {
192198
const resolvedName = getIconName(name);
193-
if (!resolvedName || svgCache.has(resolvedName)) return;
199+
if (!resolvedName) return;
200+
201+
// Skip if available in critical icons or cache
202+
if (criticalIcons.has(resolvedName) || hasInCache(resolvedName)) return;
194203

195204
try {
196205
const svgPath = `/kit-a943e80cf4-desktop/svgs-full/light/${resolvedName}.svg`;
197206
const response = await fetch(svgPath);
198207
if (response.ok) {
199208
const svgText = await response.text();
200-
svgCache.set(resolvedName, svgText);
209+
addToCache(resolvedName, svgText);
201210
}
202-
} catch (err) {
211+
} catch {
203212
// Silently fail for preloading
204213
}
205214
});
206215

207216
await Promise.all(promises);
208217
};
209218

219+
/**
220+
* Preload non-critical icons during idle time
221+
* Uses requestIdleCallback with setTimeout fallback
222+
*/
223+
export const scheduleIdlePreload = (iconNames: string[]): void => {
224+
const BATCH_SIZE = 5;
225+
let index = 0;
226+
227+
const processBatch = (_deadline?: IdleDeadline) => {
228+
const batch: string[] = [];
229+
while (index < iconNames.length && batch.length < BATCH_SIZE) {
230+
const name = getIconName(iconNames[index]);
231+
index++;
232+
if (name && !criticalIcons.has(name) && !hasInCache(name) && !failedIcons.has(name)) {
233+
batch.push(name);
234+
}
235+
}
236+
237+
if (batch.length > 0) {
238+
preloadIcons(batch);
239+
}
240+
241+
if (index < iconNames.length) {
242+
if (typeof requestIdleCallback === 'function') {
243+
requestIdleCallback(processBatch);
244+
} else {
245+
setTimeout(processBatch, 50);
246+
}
247+
}
248+
};
249+
250+
if (typeof requestIdleCallback === 'function') {
251+
requestIdleCallback(processBatch);
252+
} else {
253+
setTimeout(processBatch, 50);
254+
}
255+
};
256+
210257
/**
211258
* Clear the icon cache (useful for memory management)
212259
*/
213260
export const clearIconCache = (): void => {
214-
svgCache.clear();
261+
clearCache();
215262
};
216263

217264
export default Icon;

0 commit comments

Comments
 (0)