Vuex-persistedstate: Save State Performance

Created on 11 Dec 2017  路  5Comments  路  Source: robinvdvleuten/vuex-persistedstate

Hey, I'm have come across a minor performance issue when saving state in the subscriber. In this instance there is a simple fix available too, but I thought I would reach out to see what people think about it first.

So, you're wanting to persist the data, without getting in the way of your code performance. Then why are we blocking the saveState waiting for the write of data to the data storage in question.

The problem

The current implementation causes waits for localStorage or any other storage method to block the current thread waiting until its performed its operation.

This can cause the application to become non-responsive for a moment while we're writing this state to disk.

/* helper to simulate the delay in localStorage writing */
function wait(ms){
   var start = new Date().getTime();
   var end = start;
   while(end < start + ms) {
     end = new Date().getTime();
  }
}

/* ORIGINAL blocking version */
const myPlugin = store => {
  store.subscribe((mutation, state) => {
    console.time('VuexSubscription')
    wait(7000)
    console.timeEnd('VuexSubscription')
  })
}

The proposal

Make the write method asynchronous. In this example we go one more and use requestIdleCallback in order to find some guaranteed free time and avoid jank.

/* PROPOSED non blocking version */
// Shim `requestIdleCallback`
const requestIdleCallback = window.requestIdleCallback || (cb => {
  let start = Date.now();
  return setTimeout(() => {
    cb({
      didTimeout: false,
      timeRemaining() {
        return Math.max(0, 50 - (Date.now() - start))
      }
    })
  }, 1);
})

const myPlugin = store => {
  store.subscribe((mutation, state) => {
    console.time('VuexSubscription')
    requestIdleCallback(() => {
      wait(7000)
    }, {timeout: 60}) 
    console.timeEnd('VuexSubscription')
  })
}

So, what do you think?

Is it absolutely important to block the main thread waiting for the data to write to the storage?

enhancement

Most helpful comment

For those coming from Google and looking for a quick patch solution.

Put this into your store/index.js

const requestIdleCallback = window.requestIdleCallback || (cb => {
  let start = Date.now()
  return setTimeout(() => {
    let data = {
      didTimeout: false,
      timeRemaining () {
        return Math.max(0, 50 - (Date.now() - start))
      }
    }
    cb(data)
  }, 1)
})

const vuePersist = {
  // ... your other config
  setState: (key, state, storage) => {
    requestIdleCallback(() => {
      storage.setItem(key, JSON.stringify(state))
    })
  }
}

:+1: have a great day

All 5 comments

For those coming from Google and looking for a quick patch solution.

Put this into your store/index.js

const requestIdleCallback = window.requestIdleCallback || (cb => {
  let start = Date.now()
  return setTimeout(() => {
    let data = {
      didTimeout: false,
      timeRemaining () {
        return Math.max(0, 50 - (Date.now() - start))
      }
    }
    cb(data)
  }, 1)
})

const vuePersist = {
  // ... your other config
  setState: (key, state, storage) => {
    requestIdleCallback(() => {
      storage.setItem(key, JSON.stringify(state))
    })
  }
}

:+1: have a great day

@kylewelsby thanks for your thorough writings here! I see your point and you are correct in stating that the writing doesn't have to be blocking the thread. However your solution with the requestIdleCallback() call, is not supported in that many browsers. Maybe you have some ideas for that?

There's really two simple options, embrace the future and polyfill, or back-down to a well know API like requestAnimationFrame or setTimeout.

In the version above, we use a non intrusive polyfill, Chrome 47+,Firefox 55+ and Opera 34+ will benefit from this native API. Edge, IE & Safari. See CanIUse

Just a suggestion really.

An alternative with a throttled setState, when you don't need to persist the state that often :

import Vue from 'vue';
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import _throttle from 'lodash/throttle';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  plugins: [
    createPersistedState({
      setState: _throttle((key, state, storage) => {
        storage.setItem(key, JSON.stringify(state));
      }, 100),
    }),
  ],
});

Another alternative to throttling (lightly tested) is to overwrite the state that we are going to set when we become idle. That avoids making repeated calls to hit local storage later on.

```
let settingState = null
let setInProgress = false

export default ({ store }) => {
window.onNuxtReady(() => {
createPersistedState({
...
setState: (key, state, storage) => {
console.log('Consider set state', settingState)

    if (settingState) {
      console.log('Already setting')
      if (!setInProgress) {
        // We're not currently setting the state, we're waiting - so we can overwrite the state we're
        // intending to set with the latest one.  This saves multiple slow calls to set in local storage.
        console.log('Can overwrite')
        settingState = state
      }
    } else {
      // We're not already setting it.  Queue it up for when we're idle.
      console.log('Queue for idle')
      settingState = state
    }

    requestIdleCallback(() => {
      if (settingState) {
        console.log('set state now')
        setInProgress = true
        storage.setItem(key, JSON.stringify(state))
        setInProgress = false
        settingState = null
        console.log('completed set state')
      } else {
        // We have already set the latest state in an earlier callback.
        console.log('Nothing to set')
      }
    })
  }
})(store)

})
}

Was this page helpful?
0 / 5 - 0 ratings