New Project! ChatWithDesign 🛠️ AI Chat that can create, modify Figma files and even create websites.Try it now

Chrome Extension tip: never cache current tab

June 17, 20163 min read

chrome extension banner

Chrome Extensions typically use a background.js script which runs in a separate process that is shared across all tabs. Therefore it is important to know which tab we are currently viewing to be able to interact with it. Luckily many of the API methods that allow us to hook into different life-cycle events specify the tab we are working with.

Lets say we have an extension called Pinky which colors the page pink. It would look something like this:

// background.js
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript(tab.id, {
    code: 'document.body.style.backgroundColor = "pink";'
  });
});

Awesome! It all works, now we just need to publish the extension and we can live long and prosper in glory.

Later as we add more code we might be tempted to do something like this:

// background.js

let currentTab;

chrome.browserAction.onClicked.addListener((tab) => {
  currentTab = tab;
  makePink();
  addCopyrights();
});

function makePink() {
  chrome.tabs.executeScript(currentTab.id, {
    code: 'document.body.style.backgroundColor = "pink";'
  });
}

function addCopyrights() {
  chrome.tabs.executeScript(currentTab.id, {
    code: 'document.body.innerHTML += "© Pinky & the Brain World Wide Web Domination Extension";'
  });
}

Great, now we added another method and refactored to use a module level variable currentTab to cache the current tab. This was a fatal mistake. We will only discover it as we add our new delayed pink domination feature, which would color the page 10 seconds after the button is clicked (you know to really annoy those pesky users).

Lets take a look

// background.js

let currentTab;

chrome.browserAction.onClicked.addListener((tab) => {
  currentTab = tab;
  setTimeout(() => {
    makePink();
    addCopyrights();
  }, 10000);
});

function makePink() {
  chrome.tabs.executeScript(currentTab.id, {
    code: 'document.body.style.backgroundColor = "pink";'
  });
}

function addCopyrights() {
  chrome.tabs.executeScript(currentTab.id, {
    code: 'document.body.innerHTML += "© Pinky & the Brain World Wide Web Domination Extension";'
  });
}

We have just created a race condition. Imagine we have two tabs open, what would happen if we click on the extension button and then immediately switch to the other tab and clicked on the extension button again. Our injected code will run twice on the same tab after 10 seconds. The reason is that currentTab gets updated when the extension is clicked, but it might not be the tab we are interested in.

Our best course of action is to pass in the tab into the functions instead of caching it.

// background.js

chrome.browserAction.onClicked.addListener((tab) => {
  setTimeout(() => {
    makePink(tab);
    addCopyrights(tab);
  }, 10000);
});

function makePink(tab) {
  chrome.tabs.executeScript(tab.id, {
    code: 'document.body.style.backgroundColor = "pink";'
  });
}

function addCopyrights(tab) {
  chrome.tabs.executeScript(tab.id, {
    code: 'document.body.innerHTML += "© Pinky & the Brain World Wide Web Domination Extension";'
  });
}

Note if we did want to grab the current tab we should use chrome.tabs.query({active: true, currentWindow: true}).

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  let tab = tabs[0];
  // ... do stuff ...
});

This will help when you do not want to pass the tab around and you are actually interested in the current open tab. Certain calls like chrome.windows.onFocusChanged will not pass the current tab in, so you will have to use the above method to find it.

Be mindful of what tab you are using and if you have async events in there, think twice if the information can become stale. Often times the current tab is too volatile to cache in a variable, so always remember to query it or pass it as a parameter.

It took us a long time to track this issue in one of the chrome extensions I am working on. It resulted in unpredictable behaviour like tab not opening, tab opening on the wrong tab, messages being lost and functionality breaking seemingly at random. The biggest issue is it is really hard to reproduce and only happens in specific cases, therefore it passed all our testing until it reached production where we detected and isolated it.

Happy coding!

© 2024 Michael Yagudaev