Skip to content

Commit 83878ef

Browse files
authored
fix(js): fixed caching performance issues (#1604)
Reduces localStorage blocking in createBrowserLocalStorageCache to improve INP by merging two filter passes into one (cutting get() from 2 reads + 2 writes to 1 read + 1 write) and creating new macrotasks (instead of waiting for the microtask queue to finish synchronously) via setTimeout before heavy work in get(), set(), and delete().
1 parent 526bf76 commit 83878ef

2 files changed

Lines changed: 32 additions & 35 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@
103103
},
104104
{
105105
"path": "packages/algoliasearch/dist/algoliasearch-lite.umd.js",
106-
"maxSize": "4.6KB"
106+
"maxSize": "4.7KB"
107107
},
108108
{
109109
"path": "packages/recommend/dist/recommend.umd.js",
110-
"maxSize": "4.4KB"
110+
"maxSize": "4.5KB"
111111
}
112112
]
113113
}

packages/cache-browser-local-storage/src/createBrowserLocalStorageCache.ts

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { Cache, CacheEvents } from '@algolia/cache-common';
22

33
import { BrowserLocalStorageCacheItem, BrowserLocalStorageOptions } from '.';
44

5+
function yieldToMain(): Promise<void> {
6+
return new Promise(resolve => setTimeout(resolve, 0));
7+
}
8+
59
export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptions): Cache {
610
const namespaceKey = `algoliasearch-client-js-${options.key}`;
711

@@ -23,30 +27,24 @@ export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptio
2327
getStorage().setItem(namespaceKey, JSON.stringify(namespace));
2428
};
2529

26-
const removeOutdatedCacheItems = () => {
30+
const getFilteredNamespace = (): Record<string, BrowserLocalStorageCacheItem> => {
2731
const timeToLive = options.timeToLive ? options.timeToLive * 1000 : null;
2832
const namespace = getNamespace<BrowserLocalStorageCacheItem>();
33+
const currentTime = new Date().getTime();
2934

30-
const filteredNamespaceWithoutOldFormattedCacheItems = Object.fromEntries(
35+
return Object.fromEntries(
3136
Object.entries(namespace).filter(([, cacheItem]) => {
32-
return cacheItem.timestamp !== undefined;
33-
})
34-
);
37+
if (!cacheItem || cacheItem.timestamp === undefined) {
38+
return false;
39+
}
3540

36-
setNamespace(filteredNamespaceWithoutOldFormattedCacheItems);
41+
if (!timeToLive) {
42+
return true;
43+
}
3744

38-
if (!timeToLive) return;
39-
40-
const filteredNamespaceWithoutExpiredItems = Object.fromEntries(
41-
Object.entries(filteredNamespaceWithoutOldFormattedCacheItems).filter(([, cacheItem]) => {
42-
const currentTimestamp = new Date().getTime();
43-
const isExpired = cacheItem.timestamp + timeToLive < currentTimestamp;
44-
45-
return !isExpired;
45+
return cacheItem.timestamp + timeToLive >= currentTime;
4646
})
4747
);
48-
49-
setNamespace(filteredNamespaceWithoutExpiredItems);
5048
};
5149

5250
return {
@@ -57,25 +55,24 @@ export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptio
5755
miss: () => Promise.resolve(),
5856
}
5957
): Readonly<Promise<TValue>> {
60-
return Promise.resolve()
61-
.then(() => {
62-
removeOutdatedCacheItems();
63-
64-
const keyAsString = JSON.stringify(key);
65-
66-
return getNamespace<Promise<BrowserLocalStorageCacheItem>>()[keyAsString];
67-
})
68-
.then(value => {
69-
return Promise.all([value ? value.value : defaultValue(), value !== undefined]);
70-
})
71-
.then(([value, exists]) => {
72-
return Promise.all([value, exists || events.miss(value)]);
73-
})
74-
.then(([value]) => value);
58+
return yieldToMain().then(() => {
59+
const namespace = getFilteredNamespace();
60+
const keyAsString = JSON.stringify(key);
61+
const cachedItem = namespace[keyAsString];
62+
63+
setNamespace(namespace);
64+
65+
if (cachedItem) {
66+
return cachedItem.value as TValue;
67+
}
68+
69+
// eslint-disable-next-line promise/no-nesting
70+
return defaultValue().then((value: TValue) => events.miss(value).then(() => value));
71+
});
7572
},
7673

7774
set<TValue>(key: object | string, value: TValue): Readonly<Promise<TValue>> {
78-
return Promise.resolve().then(() => {
75+
return yieldToMain().then(() => {
7976
const namespace = getNamespace();
8077

8178
// eslint-disable-next-line functional/immutable-data
@@ -91,7 +88,7 @@ export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptio
9188
},
9289

9390
delete(key: object | string): Readonly<Promise<void>> {
94-
return Promise.resolve().then(() => {
91+
return yieldToMain().then(() => {
9592
const namespace = getNamespace();
9693

9794
// eslint-disable-next-line functional/immutable-data

0 commit comments

Comments
 (0)