@@ -8,6 +8,14 @@ import type {
88const lastBlobMap : Map < number , string > = new Map ( ) ;
99const transparentBlobMap : Map < string , string > = new Map ( ) ;
1010
11+ // Safari memory management: limit cached blob count to prevent unbounded growth
12+ const MAX_CACHED_BLOBS = 100 ;
13+ const MAX_TRANSPARENT_CACHE = 20 ;
14+
15+ // Periodic cleanup to help Safari's garbage collector
16+ let cleanupCounter = 0 ;
17+ const CLEANUP_INTERVAL = 50 ; // Run cleanup every 50 worker messages
18+
1119export interface ImageBitmapDataURLRequestWorker {
1220 postMessage : (
1321 message : ImageBitmapDataURLWorkerParams ,
@@ -31,11 +39,22 @@ async function getTransparentBlobFor(
3139 const id = `${ width } -${ height } ` ;
3240 if ( 'OffscreenCanvas' in globalThis ) {
3341 if ( transparentBlobMap . has ( id ) ) return transparentBlobMap . get ( id ) ! ;
42+
43+ // Limit cache size to prevent memory growth in Safari
44+ if ( transparentBlobMap . size >= MAX_TRANSPARENT_CACHE ) {
45+ const firstKey = transparentBlobMap . keys ( ) . next ( ) . value ;
46+ transparentBlobMap . delete ( firstKey ) ;
47+ }
48+
3449 const offscreen = new OffscreenCanvas ( width , height ) ;
3550 offscreen . getContext ( '2d' ) ; // creates rendering context for `converToBlob`
36- const blob = await offscreen . convertToBlob ( dataURLOptions ) ; // takes a while
51+ let blob : Blob | null = await offscreen . convertToBlob ( dataURLOptions ) ; // takes a while
3752 const arrayBuffer = await blob . arrayBuffer ( ) ;
3853 const base64 = encode ( arrayBuffer ) ; // cpu intensive
54+
55+ // Explicitly null out blob reference to help Safari's GC
56+ blob = null ;
57+
3958 transparentBlobMap . set ( id , base64 ) ;
4059 return base64 ;
4160 } else {
@@ -51,6 +70,27 @@ worker.onmessage = async function (e) {
5170 if ( 'OffscreenCanvas' in globalThis ) {
5271 const { id, bitmap, width, height, dataURLOptions } = e . data ;
5372
73+ // Periodic cleanup to help Safari manage memory
74+ cleanupCounter ++ ;
75+ if (
76+ cleanupCounter >= CLEANUP_INTERVAL ||
77+ lastBlobMap . size > MAX_CACHED_BLOBS
78+ ) {
79+ cleanupCounter = 0 ;
80+
81+ // Limit lastBlobMap size to prevent unbounded growth
82+ if ( lastBlobMap . size > MAX_CACHED_BLOBS ) {
83+ const entriesToRemove = lastBlobMap . size - MAX_CACHED_BLOBS ;
84+ const iterator = lastBlobMap . keys ( ) ;
85+ for ( let i = 0 ; i < entriesToRemove ; i ++ ) {
86+ const key = iterator . next ( ) . value ;
87+ if ( key !== undefined ) {
88+ lastBlobMap . delete ( key ) ;
89+ }
90+ }
91+ }
92+ }
93+
5494 const transparentBase64 = getTransparentBlobFor (
5595 width ,
5696 height ,
@@ -62,11 +102,15 @@ worker.onmessage = async function (e) {
62102
63103 ctx . drawImage ( bitmap , 0 , 0 ) ;
64104 bitmap . close ( ) ;
65- const blob = await offscreen . convertToBlob ( dataURLOptions ) ; // takes a while
105+ let blob : Blob | null = await offscreen . convertToBlob ( dataURLOptions ) ; // takes a while
66106 const type = blob . type ;
67107 const arrayBuffer = await blob . arrayBuffer ( ) ;
68108 const base64 = encode ( arrayBuffer ) ; // cpu intensive
69109
110+ // Explicitly null out blob reference to help Safari's GC
111+ // This is critical for Safari to release the blob from memory
112+ blob = null ;
113+
70114 // on first try we should check if canvas is transparent,
71115 // no need to save it's contents in that case
72116 if ( ! lastBlobMap . has ( id ) && ( await transparentBase64 ) === base64 ) {
0 commit comments