Skip to content

Commit 212cd4f

Browse files
committed
refactor(cache): implement EvictingMap for cache management to limit memory usage and improve performance
1 parent 674ef27 commit 212cd4f

3 files changed

Lines changed: 53 additions & 20 deletions

File tree

src/api/preCache.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { getStyle, inlineSingleBackgroundEntry, precacheCommonTags, isSafari } from '../utils'
33
import { embedCustomFonts, collectUsedFontVariants, collectUsedCodepoints, ensureFontsReady } from '../modules/fonts.js'
44
import { snapFetch } from '../modules/snapFetch.js'
5-
import { cache, applyCachePolicy } from '../core/cache.js'
5+
import { cache, applyCachePolicy, EvictingMap } from '../core/cache.js'
66
import { inlineBackgroundImages } from '../modules/background.js'
77

88
/**
@@ -37,8 +37,8 @@ export async function preCache(root = document, options = {}) {
3737
if (!cache.session.styleCache) {
3838
cache.session.styleCache = new WeakMap()
3939
}
40-
cache.image = cache.image || new Map()
41-
cache.background = cache.background || new Map()
40+
cache.image = cache.image || new EvictingMap(100)
41+
cache.background = cache.background || new EvictingMap(100)
4242

4343
// Pre-inline background images into cache (best-effort)
4444
try {

src/core/cache.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
1+
/** Max entries before evicting oldest (FIFO). Keeps lib lightweight, avoids memory leaks. */
2+
const MAX_IMAGE = 100
3+
const MAX_BACKGROUND = 100
4+
const MAX_RESOURCE = 150
5+
const MAX_BASE_STYLE = 50
6+
const MAX_DEFAULT_STYLE = 30
7+
8+
/**
9+
* Map that evicts oldest entries when exceeding maxSize. FIFO order.
10+
* @extends Map
11+
*/
12+
class EvictingMap extends Map {
13+
constructor(maxSize = 100, ...args) {
14+
super(...args)
15+
this._maxSize = maxSize
16+
}
17+
set(key, value) {
18+
if (this.size >= this._maxSize && !this.has(key)) {
19+
const first = this.keys().next().value
20+
if (first !== undefined) this.delete(first)
21+
}
22+
return super.set(key, value)
23+
}
24+
}
25+
126
/**
227
* Global caches for images, styles, and resources.
28+
* Persistent caches use EvictingMap to avoid unbounded memory growth.
329
*/
430
export const cache = {
5-
image: new Map(),
6-
background: new Map(),
7-
resource: new Map(),
8-
defaultStyle: new Map(),
9-
baseStyle: new Map(),
31+
image: new EvictingMap(MAX_IMAGE),
32+
background: new EvictingMap(MAX_BACKGROUND),
33+
resource: new EvictingMap(MAX_RESOURCE),
34+
defaultStyle: new EvictingMap(MAX_DEFAULT_STYLE),
35+
baseStyle: new EvictingMap(MAX_BASE_STYLE),
1036
computedStyle: new WeakMap(),
1137
font: new Set(),
1238
session: {
@@ -16,6 +42,8 @@ export const cache = {
1642
}
1743
}
1844

45+
export { EvictingMap }
46+
1947
/**
2048
* Normalizes shorthand values to canonical cache policies.
2149
* - true => "soft"
@@ -64,13 +92,12 @@ export function applyCachePolicy(policy = 'soft') {
6492
cache.session.styleCache = new WeakMap()
6593

6694
cache.computedStyle = new WeakMap()
67-
cache.baseStyle = new Map()
68-
cache.defaultStyle = new Map()
69-
70-
cache.image = new Map()
71-
cache.background = new Map()
72-
cache.resource = new Map()
73-
cache.font = new Set()
95+
cache.baseStyle = new EvictingMap(MAX_BASE_STYLE)
96+
cache.defaultStyle = new EvictingMap(MAX_DEFAULT_STYLE)
97+
cache.image = new EvictingMap(MAX_IMAGE)
98+
cache.background = new EvictingMap(MAX_BACKGROUND)
99+
cache.resource = new EvictingMap(MAX_RESOURCE)
100+
cache.font = new Set()
74101
return
75102
}
76103
default: {

src/utils/clone.helpers.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { idle } from './index.js'
7-
import { cache } from '../core/cache.js'
7+
import { cache, EvictingMap } from '../core/cache.js'
88
import { snapFetch } from '../modules/snapFetch.js'
99
import { inlineAllStyles } from '../modules/styles.js'
1010

@@ -382,10 +382,16 @@ export async function rasterizeIframe(iframe, sessionCache, options) {
382382

383383
const { contentWidth, contentHeight, rect } = measureContentBox(iframe)
384384

385-
// Prefer snapdom from the iframe realm; fallback to host's window.snapdom
386-
const snap = options?.snap
385+
// Prefer options.snap (set by main()); fallback to window.snapdom (IIFE build)
386+
let snap = options?.snap
387+
if (!snap && typeof window !== 'undefined' && window.snapdom) {
388+
snap = window.snapdom
389+
}
387390
if (!snap || typeof snap.toPng !== 'function') {
388-
throw new Error('snapdom.toPng not available in iframe or window')
391+
throw new Error(
392+
'[snapdom] iframe capture requires snapdom.toPng. Use snapdom(el) or pass options.snap. ' +
393+
'With ESM, assign window.snapdom = snapdom after import if using iframes.'
394+
)
389395
}
390396

391397
// Avoid double scaling; parent capture decides final scale
@@ -526,7 +532,7 @@ export function createCheckboxRadioReplacement(node) {
526532

527533
// ========== Blob URL Helpers ==========
528534

529-
var _blobToDataUrlCache = new Map()
535+
var _blobToDataUrlCache = new EvictingMap(80)
530536

531537
/**
532538
* Read a blob: URL and return its data URL, with memoization + shared cache.

0 commit comments

Comments
 (0)