When Google rolled out Chrome Extensions Manifest V3 (MV3), the web development community let out a collective groan. Persistent background pages were killed off, replaced by transient Background Service Workers that shut down after 30 seconds of inactivity. WebRequest APIs were restricted, and many classic productivity extensions became sluggish or broke entirely.
Many developers reacted by pulling in heavy bundlers—Webpack, Vite, Rollup, and layers of framework dependencies—just to manage a simple popup and sync some state.
When building TabPilot & WebClipper Pro, our goal was the complete opposite:
Build a lightning-fast, production-grade Chrome extension using 100% pure vanilla JavaScript, zero external npm packages, zero bundlers, and an offline-first architecture.
In this breakdown, I will show you how to solve the three hardest architectural challenges in modern Manifest V3 development:
- Keeping timers and state alive when the Background Service Worker sleeps (via
chrome.alarms). - Extracting clean, structured GitHub-Flavored Markdown directly from live webpage DOMs (without 100KB libraries like Turndown).
- Safely deduplicating browser tabs across windows with zero UI lag.
1. Surviving Service Worker Sleep: Alarms Over setInterval
In Manifest V2, developers routinely put setInterval() in a persistent background script to run Pomodoro timers, sync background tasks, or poll APIs.
In Manifest V3, this will fail silently. If the user does not interact with your extension, Chrome will terminate the Service Worker thread after 30 seconds to conserve memory and battery. Your setInterval simply vanishes into thin air.
The correct, bulletproof approach is to combine chrome.alarms with chrome.storage.local. When an alarm fires, Chrome wakes the Service Worker up, executes the listener, and allows it to go back to sleep.
Here is the exact focus sprint implementation from TabPilot Pro's background.js:
/**
* TabPilot Pro - Background Focus Timer (MV3 Service Worker Safe)
*/
const FOCUS_ALARM = "tabpilot_focus_tick";
async function getStorage(keys) {
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
}
async function setStorage(obj) {
return new Promise((resolve) => chrome.storage.local.set(obj, resolve));
}
// Update the extension icon badge with remaining minutes
function updateFocusBadge(focusState) {
if (!focusState || !focusState.active) {
chrome.action.setBadgeText({ text: "" });
return;
}
const remMs = Math.max(0, focusState.endsAt - Date.now());
const remMin = Math.ceil(remMs / 60000);
chrome.action.setBadgeBackgroundColor({ color: "#f59e0b" }); // Warm amber
chrome.action.setBadgeText({ text: `${remMin}m` });
}
// Start a persistent 25-minute sprint
async function startFocusSprint(durationMin = 25) {
const endsAt = Date.now() + durationMin * 60 * 1000;
const focusState = { active: true, durationMin, endsAt, startedAt: Date.now() };
await setStorage({ focusState });
updateFocusBadge(focusState);
// Chrome alarms wake up the Service Worker every 1 minute
chrome.alarms.create(FOCUS_ALARM, { periodInMinutes: 1 });
}
// The alarm handler that survives service worker dormancy
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== FOCUS_ALARM) return;
const { focusState, focusStats = { completedSprints: 0, totalMinutes: 0 } } =
await getStorage(["focusState", "focusStats"]);
if (!focusState || !focusState.active) {
chrome.alarms.clear(FOCUS_ALARM);
chrome.action.setBadgeText({ text: "" });
return;
}
// Timer complete
if (Date.now() >= focusState.endsAt) {
const nextStats = {
completedSprints: (focusStats.completedSprints || 0) + 1,
totalMinutes: (focusStats.totalMinutes || 0) + (focusState.durationMin || 25)
};
await setStorage({
focusState: { ...focusState, active: false, completedAt: new Date().toISOString() },
focusStats: nextStats
});
chrome.alarms.clear(FOCUS_ALARM);
// Flash green completion badge for 6 seconds
chrome.action.setBadgeBackgroundColor({ color: "#10b981" });
chrome.action.setBadgeText({ text: "DONE" });
setTimeout(() => chrome.action.setBadgeText({ text: "" }), 6000);
} else {
updateFocusBadge(focusState);
}
});
Because the state is persisted in chrome.storage.local and synchronized on every alarm tick, the timer remains 100% accurate even if Chrome aggressively pauses the service worker.
2. Zero-Dependency DOM-to-Markdown Extraction
Most web clippers package large npm libraries (like turndown or readability) that inflate extension size and introduce security vulnerabilities.
In TabPilot Pro, we wrote a lightweight, pure JavaScript content script (content_clipper.js) that injects into any webpage via chrome.scripting.executeScript, parses article content, handles nested elements, and outputs GitHub-Flavored Markdown.
Clean HTML Table to Markdown Converter
Tables are notoriously tricky to convert from DOM nodes without external tools. Here is how we convert arbitrary HTML <table> elements into aligned Markdown tables:
function convertTable(tableEl) {
const rows = Array.from(tableEl.querySelectorAll("tr"));
if (!rows.length) return "";
// Extract cell text and escape literal pipes
const matrix = rows.map((tr) =>
Array.from(tr.querySelectorAll("th, td")).map((cell) =>
(cell.innerText || cell.textContent || "")
.replace(/\s+/g, " ")
.trim()
.replace(/\|/g, "\\|")
)
).filter((r) => r.length > 0);
if (!matrix.length) return "";
// Normalize column counts across varying row lengths
const colCount = Math.max(...matrix.map((r) => r.length));
const pad = (row) => {
const copy = row.slice();
while (copy.length < colCount) copy.push("");
return copy;
};
const header = pad(matrix[0]);
const sep = header.map(() => "---");
const body = matrix.slice(1).map(pad);
const lines = [
`| ${header.join(" | ")} |`,
`| ${sep.join(" | ")} |`,
...body.map((r) => `| ${r.join(" | ")} |`)
];
return `\n\n${lines.join("\n")}\n\n`;
}
Recursive Node Traversal with Syntax Blocks
For code blocks, headings, blockquotes, and lists, a simple recursive walker ignores noisy tags (scripts, styles, iframes, navigation) and preserves syntax highlighting tags:
const SKIP_TAGS = new Set([
"SCRIPT", "STYLE", "NOSCRIPT", "IFRAME", "SVG", "CANVAS",
"NAV", "FOOTER", "ASIDE", "FORM", "BUTTON", "INPUT", "SELECT"
]);
function nodeToMarkdown(node) {
if (!node) return "";
if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue.replace(/\s+/g, " ");
}
if (node.nodeType !== Node.ELEMENT_NODE) return "";
const tag = node.tagName.toUpperCase();
if (SKIP_TAGS.has(tag) || node.getAttribute("aria-hidden") === "true") return "";
if (tag === "BR") return "\n";
if (tag === "HR") return "\n\n---\n\n";
if (tag === "TABLE") return convertTable(node);
// Preserve preformatted code blocks with programming language
if (tag === "PRE") {
const codeEl = node.querySelector("code");
const cls = (codeEl && codeEl.className) || node.className || "";
const langMatch = cls.match(/language-([a-zA-Z0-9_+-]+)/);
const lang = langMatch ? langMatch[1] : "";
const rawCode = (codeEl || node).textContent || "";
return `\n\n\`\`\`${lang}\n${rawCode.replace(/\n+$/, "")}\n\`\`\`\n\n`;
}
// Headings
if (/^H[1-6]$/.test(tag)) {
const level = Number(tag[1]);
const text = Array.from(node.childNodes).map(nodeToMarkdown).join("").trim();
return text ? `\n\n${"#".repeat(level)} ${text}\n\n` : "";
}
// Links
if (tag === "A") {
const href = node.getAttribute("href");
const linkText = Array.from(node.childNodes).map(nodeToMarkdown).join("").trim();
if (!href || !linkText) return linkText;
const absUrl = new URL(href, document.baseURI).href;
return `[${linkText}](${absUrl})`;
}
// Recurse child nodes for containers
return Array.from(node.childNodes).map(nodeToMarkdown).join("");
}
This tiny, 150-line extractor handles selection clips or full-page articles cleanly and runs in under 4ms on modern websites.
3. Intelligent Tab Workspace Saving & URL Deduplication
Developers and researchers easily accumulate 50+ open tabs across multiple windows. TabPilot Pro groups tabs into named workspaces (Engineering, Research, Design) and provides a 1-click Deduplicate Tabs function.
To properly deduplicate tabs, you cannot simply compare exact URL strings—anchor links (#section-2) and trailing slashes will fool a naive check:
async function deduplicateTabs() {
const tabs = await chrome.tabs.query({ currentWindow: true });
const seenUrls = new Set();
const duplicateIds = [];
for (const tab of tabs) {
// Strip URL fragments and trailing slashes for clean matching
const normalized = (tab.url || "").split("#")[0].replace(/\/$/, "");
if (!normalized || normalized.startsWith("chrome://")) continue;
if (seenUrls.has(normalized)) {
duplicateIds.push(tab.id);
} else {
seenUrls.add(normalized);
}
}
if (duplicateIds.length > 0) {
await chrome.tabs.remove(duplicateIds);
// Flash badge showing how many tabs were purged
chrome.action.setBadgeBackgroundColor({ color: "#10b981" });
chrome.action.setBadgeText({ text: `-${duplicateIds.length}` });
setTimeout(() => chrome.action.setBadgeText({ text: "" }), 2400);
}
return { removedCount: duplicateIds.length };
}
To bind this to a global keyboard shortcut, we declare commands in manifest.json:
{
"commands": {
"quick_clip_page": {
"suggested_key": {
"default": "Alt+Shift+M"
},
"description": "Clip active webpage or selection to clean Markdown"
}
}
}
Whenever the user hits Alt+Shift+M, the background service worker extracts the page content, persists it to local storage, and flashes a confirmation badge on the toolbar icon.
Get the Full Source & Commercial License
If you want the complete, unbundled source code of TabPilot Pro—including the full Glassmorphism dark/light popup interface, fuzzy search, workspace backup/restore, and CSV export:
🚀 TabPilot & WebClipper Pro ($14 on Gumroad)
-
What's included: 100% clean, unminified source code (
manifest.json,background.js,content_clipper.js,popup.html/css/js), high-res store icons, and a full commercial license to customize, rebrand, or publish to the Chrome Web Store. -
Zero build steps: Load it directly into
chrome://extensionsvia "Load unpacked" and start developing immediately.
Looking for zero-dependency Python backend automation too?
Check out our free starter toolkit:
🎁 Download the Free Python Automation Starter Kit ($0+)
Visit our studio storefront for more production-ready engineering blueprints:
🌐 Antigravity Dev Studio Gumroad Storefront
Top comments (0)