How Session Storage Can Improve Site Performance

Posted on:

Most of the time, you read about web applications that use sessionStorage inside web applications. These applications will use sessionStorage to do things like store the state of an app, so you can resume working on the app, even if you reload the page, or accidentally leave close the app. Most of the time, any further considerations for how to use sessionStorage is not considered, and that's a shame. It's a very useful utility that can not only improve your site speed, but drastically reduce server load. In fact, the site you're using right now makes use of sessionStorage to accomplish this exact thing.

Most-any headless, or nearly-headless WordPress website can lean on session storage.

What is JavaScript sessionStorage?

sessionStorage is a data store that is managed by your web browser, using JavaScript. Any data placed in sessionStorage remains there for the extent of the browser's session. As soon as the visitor ends their session, this storage is deleted. Most browsers end their session when the browser is completely closed.

This means that all of the data that is stored in sessionStorage should be thought of as temporary, since any of the data in it can be deleted at any time. In other words, this works very much like the WordPress object cache, where you can store data to be accessed later, but you should always have a way to create that data if it doesn't exist in the cache at that moment.

Using sessionStorage

You don't need any libraries or special things to use sessionStorage, it's built directly into your Window object. There are 4 key methods to work with sessionStorage.

  1. sessionStorage.setItem() - used to add an item to sessionStorage to be accessed later
  2. sessionStorage.getItem() - used to get an item that was previously stored in sessionStorage
  3. sessionStorage.removeItem() - used to remove an item from sessionStorage
  4. sessionStorage.clear() - used to clear all records in sessionStorage. (use with caution)

Most of the time, you use sessionStorage.getItem() to see if the cached item exists, and if it doesn’t, calculate the value, and add it to the cache for next-time. Kind-of like this:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_616d7bdb6c9f0",
    "name": "acf/snippet",
    "data": {
      "language": "javascript",
      "_language": "field_60d7f32b1b540",
      "code": "function slowTaskExample(){\r\n\r\n    // First, try to get the item from storage\r\n    let result = sessionStorage.getItem( 'uniqueStorageKey' )\r\n\r\n    // If the item is not in sessionStorage, create it, and store in storage for next time.\r\n    if( null === result ){\r\n        result = [];\r\n        for( let i = 0; i < 100; i++ ){\r\n            result.push( 'Appended item ' + i )\r\n        }\r\n\r\n        try{\r\n        // Now add this to sessionStorage. Note that the result was converted to JSON.\r\n        sessionStorage.setItem( 'uniqueStorageKey', JSON.stringify( result ) )\r\n        } catch(e){\r\n            // Catch the error if sessionStorage fails to save.\r\n        }\r\n    // Otherwise, parse the JSON string from sessionStorage and return that directly\r\n    } else {\r\n        result = JSON.parse( result )\r\n    }\r\n\r\n    // Return the parsed result\r\n    return result\r\n}",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "preview"
  },
  "innerBlocks": []
}

It is important that the sessionStorage key is unique, otherwise you would end up getting the wrong cached item. That could certainly cause some problems, so be sure your key is always unique. Another thing about sessionStorage is that it takes many data types, and will convert them to a string when storing in sessionStorage. In the example above, the result returns an array. When stored in sessionStorage, this data is converted into a string, which would cause unexpected results if the data were to be accessed later. To work around this, the sessionStorage result is converted to a JSON string beforehand.

Let's take this trivial example, and make it less-trivial by adding an argument to the function that gets used to change the result. This would change the data that is stored in the cache based on the argument, and could cause a mismatch between what would be generated and what is in the cache. To get around this, it's best to use the arguments to generate the cache key, like this:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_616d7beb6c9f1",
    "name": "acf/snippet",
    "data": {
      "language": "javascript",
      "_language": "field_60d7f32b1b540",
      "code": "function slowTaskExample( numberOfTimes = 100 ){\r\n\r\n    const cacheKey = `uniqueStorageKey${numberOfTimes}`\r\n\r\n    // First, try to get the item from storage\r\n    let result = sessionStorage.getItem( cacheKey )\r\n\r\n    // If the item is not in sessionStorage, create it, and store in storage for next time.\r\n    if( null === result ){\r\n        result = [];\r\n        for( let i = 0; i < numberOfTimes; i++ ){\r\n            result.push( 'Appended item ' + i )\r\n        }\r\n\r\n        try{\r\n        // Now add this to sessionStorage. Note that the result was converted to JSON.\r\n        sessionStorage.setItem( cacheKey, JSON.stringify( result ) )\r\n        } catch(e){\r\n            // Catch the error if sessionStorage fails to save.\r\n        }\r\n    // Otherwise, parse the JSON string from sessionStorage and return that directly\r\n    } else {\r\n        result = JSON.parse( result )\r\n    }\r\n\r\n    // Return the parsed result\r\n    return result\r\n}",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "preview"
  },
  "innerBlocks": []
}

Reduce Server Load

The examples provided above are fairly trivial, but what if you stored RESTful requests that simply fetch public data from an API? By doing this, you could reduce the number of REST requests your website needs to make to access data, and simply load it directly from sessionStorage. This would allow many of your web pages to load instantly, since the data is already stored in the cache.

The example below wraps everything in a promise. This allows us to use async/await inside the promise and allows us to determine iffetchExample should be called asynchronously, or synchronously.

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_616d7bff6c9f2",
    "name": "acf/snippet",
    "data": {
      "language": "javascript",
      "_language": "field_60d7f32b1b540",
      "code": "function fetchExample(){\r\n    return new Promise( async ( res, rej ) => {\r\n\r\n    // First, try to get the item from storage\r\n    let result = sessionStorage.getItem( 'posts' )\r\n\r\n    // If the item is not in sessionStorage, create it, and store in storage for next time.\r\n    if( null === result ){\r\n        const response = await fetch('https://nearly-headless.dev/wp-json/wp/v2/posts')\r\n        result = await response.json()\r\n\r\n        try{\r\n        // Now add this to sessionStorage. Note that the result is converted to JSON.\r\n        sessionStorage.setItem( 'posts', JSON.stringify( result ) )\r\n        } catch(e){\r\n            // Catch the error if sessionStorage fails to save.\r\n            res( result ) \r\n        }\r\n    // Otherwise, parse the JSON string from sessionStorage and return that directly\r\n    } else {\r\n        result = JSON.parse( result )\r\n    }\r\n\r\n    // Return the parsed result\r\n    res( result )\r\n    })\r\n}",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "preview"
  },
  "innerBlocks": []
}

Now, the first time this function is called, it will make a REST request to fetch the posts, however any time it runs thereafter, it will automatically fetch the data from sessionStorage, instead. Pretty freaking cool, right? If you're using the WordPress apiFetch library, you could even take this a step further and make the above function work as middleware. You could set it up so that your instance of apiFetch automatically caches the data through apiFetch. This is a bit of an advanced technique, but when set-up, anyone who extends your system would benefit from all of your caching optimizations without necessarily even knowing they're using it!

Nicholas and SessionStorage

The Nicholas router is both a routing, and caching system designed to work with caching content on different WordPress pages. Behind the scenes, it uses sessionStorage, and if your content is specific to a webpage, it can make caching much easier.

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_616d7c0b6c9f3",
    "name": "acf/snippet",
    "data": {
      "language": "javascript",
      "_language": "field_60d7f32b1b540",
      "code": "\r\n// Merge this data with the existing cache data.\r\ncurrentUrl.updateCache( { custom: 'data to add to this page cache' } )\r\n\r\n// This will get the cache data\r\nconst cachedData = currentUrl.getCache()",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "preview"
  },
  "innerBlocks": []
}

Nicholas is both a router and a caching solution, designed to cache data as it pertains to different webpages, and is used extensively in the nearly headless WordPress website course that's coming out soon.

Conclusion

Unlike server-side caching, sessionStorage caches directly in the browser. This can bypass the need to even touch the server, allowing many things to load instantly. This is the key component behind what makes the nearly headless website demo so fast - it makes extensive use of sessionStorage to store all of the website data needed to render any given page. You don't have to push it this far, however. Even something as simple as the fetch example above have a big impact on your site's performance.