Have you ever clicked on a SharePoint page and had to wait for data to load, even though it was fine a minute ago? You’re not alone. Many SPFx components suffer from caching strategies that force users to wait for data refreshes at the wrong moment. But what if we could refresh the cache at a better time—not when the user is waiting, but when they don’t even notice?

Table of Contents

Why Cache ?

Whether you are directly using the SharePoint API or accessing data via SharePoint Search, retrieving SharePoint data is generally fast.

However, in cases involving heavy SharePoint item access or deep Term Store data trees, fetching data can take considerable time, causing users to wait for operations to complete before the UI can render.

flowchart TB %% Define the background color for the whole diagram classDef default fill:#CFA63D,stroke:#c7a42b,stroke-width:1px; A[🧑🏽‍🦳 User requests data] B[Data fetched from SharePoint ⏳] C["🖥️ Data is rendered (always freshest)"] A --> B linkStyle 0 stroke-width:2px,stroke:#0aa8a7 B --> C linkStyle 1 stroke-width:2px,stroke:#0aa8a7

This isn’t the experience we want to provide. Users today expect pages to load quickly. Caching seems like an obvious solution and, at first glance, appears to solve most of these problems.

Where to Cache ?

There are many places to cache data. One convenient option is the browser’s LocalStorage or SessionStorage :

  • LocalStorage : Persists across all browser tabs and sessions but has a limited size per origin. Typical use cases: Caching (as in this blog post), storing user preferences, etc.
  • SessionStorage : Only available within a specific tab and session. Typical use cases: Persisting form inputs, maintaining navigation states, etc.

For this post, I will focus on LocalStorage as I want caching works for a set period, even if the user closes all browser tabs.

Caching vs Freshness

Caching is useful, but it comes with tradeoffs:

  • Reduced freshness: Data is served from the cache until expiration, meaning users may not always see the latest data.
  • User wait time: When the cache expires, data is refreshed, forcing users to wait while the freshest data is fetched—resulting in poor UI responsiveness.
flowchart TB %% Define the background color for the whole diagram classDef default fill:#CFA63D,stroke:#c7a42b,stroke-width:1px; A[🧑🏾‍🦱 User requests data] B[Check localStorage] C{✅ Is cache valid?} subgraph YesBranch [ ] D[Return cached data instantly ⚡] end subgraph NoBranch [ ] E[Cache expired] F[Data fetched from SharePoint ⏳] G[Update localStorage] end H[🖥️ Data is rendered] A --> B linkStyle 0 stroke-width:2px,stroke:#0aa8a7 B --> C linkStyle 1 stroke-width:2px,stroke:#0aa8a7 C -- Yes --> YesBranch linkStyle 2 stroke-width:2px,stroke:#0aa8a7 C -- No --> NoBranch linkStyle 3 stroke-width:2px,stroke:#da2323 E --> F linkStyle 4 stroke-width:2px,stroke:#da2323 F --> G linkStyle 5 stroke-width:2px,stroke:#da2323 YesBranch --> H linkStyle 6 stroke-width:2px,stroke:#0aa8a7 NoBranch --> H linkStyle 7 stroke-width:2px,stroke:#da2323

Note: To minimize user wait time, we can increase the cache expiration duration, but this reduces data freshness, which may introduce other issues.

A Better Time to Cache: Just After Expiration

One of our components using caching was slow once a day (caching was set for 18 hours), and that frustrated me.

The solution is simple but counterintuitive—at least for Swiss people who like to be on time:
✅ Instead of waiting for the cache to expire before refreshing data, return the cached data immediately and refresh in the background.

Some might argue that this means serving stale data. That’s absolutely true! But with this approach, you can refresh data more frequently while keeping the UI responsive. The result? A better user experience almost every time.

flowchart TB %% Define the background color for the whole diagram classDef default fill:#CFA63D,stroke:#c7a42b,stroke-width:1px; A[🧑🏼‍🦰 User requests data] B[Check localStorage] C{✅ Is cache valid?} D[Return cached data instantly ⚡] F[Return cached data instantly ⚡] G[⏳ Schedule background refresh on idle] H[⏳ Background: fetch fresh data] I[Update localStorage] J[Fresh data ready for future requests] R[🖥️ Data is rendered] %% Main flow A --> B B --> C C -- Yes --> D D --> R C -- No --> F F --> R F --> G G --> H H --> I I --> J %% Styling paths linkStyle 0,1,2,3,4,5 stroke-width:2px,stroke:#0aa8a7 linkStyle 6,7,8,9 stroke-width:2px,stroke:#da2323

Tradeoffs of This Approach:

  • First load remains slow (either when a user visits a page for the first time or after the cache is cleared).
  • Users who visit infrequently may briefly see older data before it refreshes.

However, in most cases, this performs far better than just using caching with expiration.

Generic Cache Manager

To simplify implementation, I created a utility class that manages caching and deferred refresh automatically. It also leverages requestIdleCallback, a browser API for background execution.

const requestIdleCallbackFn: (callback: IdleRequestCallback) => number =
    typeof window.requestIdleCallback === 'function'
        ? window.requestIdleCallback.bind(window)
        : (callback: IdleRequestCallback): number => {
            return setTimeout(() => {
                callback({
                    didTimeout: true,
                    timeRemaining: () => 0,
                });
            }, 200);
        };

Example: Caching Term Store Data

// Create a CacheManager instance for term store data
// Termstore data are not read here
const mytermstoredata = new CacheManager<IOrderedTermInfo[]>(
    "YOUR_LOCALSTORAGECACHE_KEY_GUID",
    CACHE_TIMEOUT_SECONDS,
    () => {
        return this.sp.termStore.sets
            .getById("guid....")
            .getAllChildrenAsOrderedTree();
    }
);

// Now we get the data from the termstore, or perform whatever long operation we have passed to the CacheManager
mytermstoredata.getData(false);

The CacheManager handles everything automatically—just call getData().

Full Code

I’ve created an SPFx solution where you can test this caching approach in a Web Part. 🔗 Code available here: react-caching

Conclusion

Fast and responsive SPFx solutions are key to user adoption. By implementing deferred caching, you ensure that your components load quickly almost every time, while still keeping data fresh.

This approach optimizes performance without making the UI just wait for new data!