Chrome extensions are a great way for developers to provide users with useful tools without having to build an entire web app. However, Chrome extensions have a specific structure, and data persistence can be tricky. While there are great docs for Chrome’s Storage API, that doesn’t cover everything. Currently, no single resource clearly explains all the options for persisting state in Chrome extensions for different durations. This guide explains the options and limitations for state persistence in Chrome extensions, helping developers choose the best fit for their projects.
- Popup lifetime storage – the default for popups
For Chrome extensions that use a popup, anytime the user opens a popup, it is a new instance of that popup. So if the user closes the popup (which happens automatically if they click into the browser), and then reopens it, that new popup will not remember or be aware of anything done previously.
- Side Panel lifetime storage – default for side panels
Like popups, when the user closes and reopens a side panel, they are creating a new instance of the side panel. Data will only persist as long as the side panel is open, and any data created when they had the side panel open earlier will be lost.
However, unlike popups, side panels will not automatically close whenever the user clicks outside of it, so side panels are able to have much longer lifetimes than popups. A side panel can stay open as the user navigates to different web pages in a tab or even different tabs within Chrome. As a result, data in a side panel is capable of persisting much longer than in a popup, though it still will be lost if the user closes and reopens it.
-
Page Lifetime Storage – Message Passing
If an extension interacts with the webpage, the extension must track changes or actions already taken. For instance, if an extension changes a web page’s colors to display in dark mode, it should remember this, so it does not prompt the user to repeat something they have already done. Additionally, if the user refreshes the page, any previously executed code is cleared. So, we don’t want to persist data beyond the current page session, as it will be outdated.
To handle this, we use message passing. The content script has the code we run on the webpage, which automatically persists for the page lifetime. However, as mentioned above, popups and side panels – the UIs for the user – do not. Message passing allows us to make sure the UIs are up to date.
First, we define the state in the content script (e.g., “initial,” “loading,” “success,” or “error”). We pass a message every time it changes so the UI is up to date. We also request the current state every time the popup or side panel is opened. So if the user closes and reopens a popup while the extension is loading, the new popup will request the state, receive ‘loading’, and render the ‘loading’ UI.
Here is an example:
popup.js
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {action: "getExtensionState"}, function(response) {
// if the content script hasn't been injected, then the code in that script hasn't been run, and we'll get an error or no response
if (chrome.runtime.lastError || !response) {
return;
} else if (response) {
// if the code in content script has been injected, we'll get a response which tells us what state the code is at (initial, loading, success, error, etc)
renderUI(response.extensionState)
}
});
});
content-script.js
let extensionState = 'initial';
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.action === "getExtensionState")
sendResponse({extensionState});
});
In some cases, the content script will be injected every time we open the extension. In others, we run the content script at a specific time – ie, the user clicks a button in the popup, which then injects the code (ie, “Click me to display this page in dark mode”). If the content script has never been run, we will get an error when we send the “getExtensionState” message from the popup. The first if statement in the example popup.js code is there to catch that error and allow us to do whatever we want to do.
- Deprecated: extension lifetime – background worker
This option is deprecated and no longer usable. However, a lot of Chrome resources are older, and it’s not always obvious which 8 year old Stack Overflow answers are still relevant and which ones aren’t, so I’m briefly touching on it so you know not to try to implement this.
In past versions of Chrome extensions, there was the option of ‘background workers’ (V2 background worker docs). It was possible to set these to ‘persist,’ and they would stay active for the extension instance lifetime, and across tabs. The field ‘background’ still exists in Manifest V3, but it is now considered a service worker and only stays active as long as it is running code – it is put to sleep within 30 seconds of inactivity (service workers docs). There is no out of the box option to set these service workers as persistent.
Chrome Storage API
Chrome’s Storage API provides multiple options to persist data longer. To use any of these, you’ll have to add ‘storage’ permissions in the Manifest. The docs for the Chrome Storage API are here and cover the final three options.
- Browser Session storage – chrome.storage.session
The shortest Chrome Storage API option is chrome session storage, which holds data in memory for the duration of the Chrome browser session. In this context, a Chrome browser session is the time period any windows are open with the same Chrome profile. So if a user has two Chrome windows open, closes one but the other remains open, the user is still in the same browser session, and the data will persist. Additionally, if the user refreshes the page or closes the tab, the data will persist. The data will be cleared only if they close all browser windows for that Chrome profile, restart Chrome, or reload the extension.
Example implementation from the API docs:
chrome.storage.session.set({ key: value }).then(() => {
console.log("Value was set");
});
chrome.storage.session.get(["key"]).then((result) => {
console.log("Value is " + result.key);
});
- Extension installation lifetime storage – chrome.storage.local
Chrome’s local storage allows the data to be saved as long as the extension is installed. It will persist even if the page is refreshed, or the browser is fully closed and reopened. It is only cleared if the extension is removed or chrome.storage.local.clear() is called.
Example implementation from the API docs:
chrome.storage.local.set({ key: value }).then(() => {
console.log("Value is set");
});
chrome.storage.local.get(["key"]).then((result) => {
console.log("Value is " + result.key);
});
-
Storage across chrome devices – chrome.storage.sync
Chrome’s sync storage will sync the saved data to any Chrome browser the user is logged into. If a user does not have sync enabled, it will fall back to local storage behavior. The data is only cleared if the extension is uninstalled or chrome.storage.local.clear() is called.
Example implementation from the API docs:
chrome.storage.sync.set({ key: value }).then(() => {
console.log("Value is set");
});
chrome.storage.sync.get(["key"]).then((result) => {
console.log("Value is " + result.key);
});
Did I miss anything? Please let me know in the comments!