DEV Community

Cover image for Manifest V3 Without Bloat: Building a Tab Workspace Manager, Markdown Web Clipper & Service Worker Pomodoro in Pure JS
Antigravity Dev Studio
Antigravity Dev Studio

Posted on

Manifest V3 Without Bloat: Building a Tab Workspace Manager, Markdown Web Clipper & Service Worker Pomodoro in Pure JS

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:

  1. Keeping timers and state alive when the Background Service Worker sleeps (via chrome.alarms).
  2. Extracting clean, structured GitHub-Flavored Markdown directly from live webpage DOMs (without 100KB libraries like Turndown).
  3. 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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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`;
}
Enter fullscreen mode Exit fullscreen mode

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("");
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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://extensions via "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)