Building persistent Chrome Extension using Manifest V3
Introduction:
This post will guide you through making an MV3 extension persistent using a workaround approach and cover its architecture and setup — this is not a from-scratch build guide.
Requirements:
- Basic knowledge of web development
- Familiarity with event-driven architecture
What’s Changed in MV3?
In MV3, background.js
has been replaced by a service worker that runs in an event-driven manner. This change improves Chrome's resource efficiency but comes with trade-offs:
- No Direct DOM Access: Unlike
background.js
, service workers cannot directly interact with the DOM. To modify the DOM, you'll needcontent.js
(a content script) injected into the web page. - Event-Driven and Non-Persistent: Service workers only activate when needed (e.g., on alarms or incoming messages) and shut down when idle. This behavior requires rethinking how background tasks are handled.
Working Architecture
- Service Worker (
background.js
): Runs background tasks and manages alarms to keep reminders going. - Content Script (
content.js
): Injected into web pages to interact with the DOM and respond to user interactions. - Popup (
popup.js
): Handles user input, such as setting reminder intervals and enabling sound notifications.
This image explains the working architecture of the background.js and content.js more here.
Extension: Water-Kitty (Water Reminder)
An extension which will remind you to have a sip in X intervals with a cute “meow” voice.
Check it out here: link
Folder Structure
every file has unique purpose.
- manifest.json: Configuration file for the extension
- popup.html: The user interface of the extension
- popup.css: Styling for the popup
- popup.js: Logic for handling user events in the popup
- background.js: The service worker
- content.js: Content script that interacts with the DOM
manifest.json
{
"manifest_version": 3,
"name": "Water Kitty",
"version": "1.0",
"description": "Cute kitty voice to remind you stay hydrated.",
"permissions": ["notifications", "storage", "scripting"]:
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"web_accessible_resources": [{
"resources": ["final_meow.mp3"],
"matches": ["<all_urls>"],
"extension_ids": []
}],
"action": {
"default_popup": "popup.html",
"default_icon": {
"48": "icons/icon48.png"
}
},
"host_permissions": ["*://*/*"],
"icons": {
"48": "icons/icon48.png"
}
}
Note: Only include the permissions your extension needs. Unnecessary permissions can raise security concerns and complicate the Chrome Web Store approval process.
popup.js
document.addEventListener('DOMContentLoaded', () => {
const intervalInput = document.getElementById('interval');
const saveButton = document.getElementById('save');
const stopButton = document.getElementById('stop');
const soundCheckbox = document.getElementById('sound');
// Load the current settings from storage
chrome.storage.sync.get(['interval', 'reminderActive', 'soundEnabled'], (result) => {
intervalInput.value = result.interval || 60;
if (result.reminderActive) {
stopButton.style.display = 'inline-block';
}
soundCheckbox.checked = result.soundEnabled || false;
});
// Save the new interval and start the reminder
saveButton.addEventListener('click', () => {
const interval = parseInt(intervalInput.value, 10);
const soundEnabled = soundCheckbox.checked;
chrome.storage.sync.set({ interval, reminderActive: true, soundEnabled }, () => {
chrome.runtime.sendMessage({ action: 'startReminder', interval, soundEnabled }, (response) => {
if (chrome.runtime.lastError) {
console.log("Error sending message:", chrome.runtime.lastError);
} else {
stopButton.style.display = 'inline-block';
... }
});
});
});
// Stop the reminders
stopButton.addEventListener('click', () => {
chrome.storage.sync.set({ reminderActive: false }, () => {
chrome.runtime.sendMessage({ action: 'stopReminder' }, (response) => {
if (chrome.runtime.lastError) {
console.log("Error sending message:", chrome.runtime.lastError);
} else {
stopButton.style.display = 'none';
}
});
});
});
});
Making the Service Worker Persistent
To keep the service worker alive, you could set up recurring alarms or use the following approach:
- Alarms: Create alarms using
chrome.alarms
to trigger background tasks periodically. - Workaround: Use a
setInterval
function or similar logic inside your service worker to periodically ping itself.
to make service-worker alive all the time i used a keep alive method (point 2) which checks in every 20 seconds.
background.js
...
// Keep-alive logic
function keepAlive() {
setInterval(() => {
chrome.runtime.getPlatformInfo(function(info) {
console.log('Keeping service worker alive. Platform: ' + info.os);
});
}, 20000); // Every 20 seconds
}
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.set({ interval: defaultInterval, reminderActive: false, soundEnabled: false });
initialize();
keepAlive(); // Start keep-alive on install
});
chrome.runtime.onStartup.addListener(() => {
initialize();
keepAlive(); // Restart keep-alive on startup
});
...
a snippet from bakcground.js without keepAlive() i was facing this service-worker going inactive.
repo: kitty-remind-me
Limitations and Challenges of MV3
When working with Manifest V3, developers may encounter certain limitations that can affect the behavior and functionality of extensions:
- Content Script Injection Delays:
- One notable issue is that the
content.js
script may not get injected into a tab immediately. In some cases, it only gets injected after the user interacts with or navigates within the tab (e.g., performing a search or other action). - Implication: This behavior can lead to delays in DOM access or manipulation by the content script, impacting extensions that rely on immediate interaction with the web page.
- Workaround: Plan the logic of your extension to account for these potential delays, and consider strategies for handling interactions more efficiently.
2. User Interaction Requirement for Sound Notifications:
- If your extension needs to send notifications with sound or play audio in response to an event, Chrome requires that the user must have interacted with the tab beforehand. This restriction is part of Chrome’s security and user experience policies to prevent intrusive behavior.
- Implication: Extensions that play sounds or send notifications autonomously may not work as expected if the tab is inactive or if the user hasn’t engaged with the browser.
- Solution: Ensure that the extension logic involves user interaction before playing sounds or sending audio notifications. This may mean prompting users to engage with the extension at setup or explaining the limitations within the extension’s UI.
Conclusion
Tackling the issue of creating a persistent background process in Manifest V3 was challenging, especially since most resources and documentation still focus on Manifest V2. I wrote this guide hoping it might be helpful to someone facing similar struggles or navigating these changes for the first time.
If you find anything in this post that is incorrect or could be improved, please feel free to reach out or leave a comment. Your feedback is invaluable for making this resource as accurate and helpful as possible.