Building a Browser Extension: What We Learned
Posted: November 13, 2026 · 6 min read
Why we are writing this
Building a browser extension is a different discipline from building a web app. The constraints are different. The debugging tools are different. The distribution model is different. And the documentation, while improving, still has significant gaps.
This is a technical retrospective on building manyCalendars. What decisions we made, why we made them, what worked, and what we would reconsider. If you are building or considering building a browser extension, we hope this saves you some of the lessons we learned the hard way.
The decision to use vanilla JS
This was the first major architectural choice, and it remains one of the most consequential. We chose to build manyCalendars without a frontend framework. No React, no Vue, no Svelte. Just vanilla JavaScript, HTML, and CSS.
The reasoning was straightforward. Browser extensions have strict size constraints. Every kilobyte matters, both for initial install size and for runtime memory. A React runtime adds weight that a vanilla implementation does not. For a tool that runs persistently in the background, that weight compounds.
The practical benefit has been significant. Our extension bundle is small. Startup is fast. Memory usage is low. We have full control over the DOM without a virtual DOM layer adding abstraction. When we need to optimize a render path, we can optimize it directly.
The cost is real, though. Component reuse requires discipline that a framework enforces automatically. State management is manual. New contributors (if we ever have them) face a steeper onboarding curve because there is no framework-imposed structure to follow. Every pattern is our pattern, and patterns only work when they are documented and consistently applied.
Would we make the same choice again? Yes, but with a caveat. For an extension of this size and scope, vanilla JS was the right call. For a significantly larger extension with a team of five or more developers, the framework overhead would likely be worth the structural benefits.
Manifest V3: the migration that keeps on giving
Chrome's Manifest V3 migration has been, to put it diplomatically, an evolving experience. The shift from persistent background pages to service workers fundamentally changed how extensions maintain state. Service workers can be terminated at any time, which means you cannot rely on in-memory state persisting between events.
For manyCalendars, this meant rethinking how we store calendar data. In Manifest V2, we could keep parsed calendar data in memory in the background page. In V3, that data needs to live in chrome.storage or IndexedDB, with the service worker reconstructing its working state from storage every time it wakes up.
The alarm API became our heartbeat. We use chrome.alarms to trigger periodic syncs, which wake the service worker, pull fresh data from ICS feeds, update storage, and then let the worker go back to sleep. It works, but it is a fundamentally different programming model from "run this code and keep it running."
The content script isolation model in V3 also affected our Tab Sync. Content scripts run in an isolated world by default, which means they cannot access the page's JavaScript context directly. We use DOM observation instead, watching for changes in the calendar UI's HTML structure rather than hooking into the application's JavaScript. This is more fragile (UI changes can break scrapers) but works within V3's security model.
Cross-browser compatibility: Chrome, Firefox, Edge
Supporting three browsers sounds like it should be straightforward. It is not. Chrome and Edge share the Chromium engine, so most Chrome extension APIs work identically on Edge. Firefox is the outlier.
Firefox uses a different extension API namespace (browser.* instead of chrome.*). It supports Promises natively where Chrome still uses callbacks in many APIs (though this is improving). Its implementation of certain APIs differs in subtle ways. The storage API, for instance, has different size limits and different behavior around concurrent writes.
Our approach was to write a thin compatibility layer that normalizes these differences. It maps browser.* to chrome.* where needed, wraps callback-based APIs in Promises, and handles the edge cases we have discovered through testing. This layer adds minimal code but saves enormous debugging time.
The store submission process also differs significantly. Chrome Web Store reviews are automated and usually fast (hours to days). Firefox Add-on reviews involve manual code review by Mozilla staff and can take weeks. Edge Add-ons sits somewhere in between. Plan your release cadence accordingly.
Tab Sync architecture
Tab Sync is manyCalendars's most distinctive technical feature, and it was the hardest to get right. The concept is simple: read calendar events directly from an open browser tab. The implementation is anything but.
The challenge is that calendar web apps render events as DOM elements with no standardized structure. Google Calendar uses a different HTML layout than Outlook Web, which uses a different layout than any other calendar. Each one changes their DOM structure periodically as they ship updates. A sync engine that works today might break next month.
Our solution is a sync engine architecture with provider-specific modules. Each module knows how to find events in a particular calendar UI: where the event elements live in the DOM, how to extract the title, start time, end time, and location, and how to handle the various view modes (day, week, month). When a calendar provider changes their UI, we update the relevant module without touching the rest of the system.
We use MutationObserver to detect DOM changes in real time. When the user navigates to a different week or an event gets added, the observer fires, the sync engine re-reads the affected portion of the DOM, and manyCalendars updates its internal event store. This gives us near-real-time sync without polling, which keeps CPU usage low.
Performance lessons
Extensions share resources with the browser and every other extension the user has installed. Being a good citizen means being frugal with CPU, memory, and storage.
Our biggest performance lesson was about rendering. Early versions of manyCalendars re-rendered the entire calendar grid whenever any event changed. For a user with three calendars and 50 events visible, that meant rebuilding hundreds of DOM elements for a single event update. We switched to targeted updates: only the affected time slots get re-rendered. The difference was dramatic, especially on lower-powered machines.
Storage management was another lesson. IndexedDB is fast for reads and writes, but it is not free. We implemented a retention policy that automatically cleans up events older than a configurable threshold. Without this, the storage footprint grew linearly with usage and never shrank.
The service worker lifecycle in Manifest V3 taught us to be aggressive about releasing references. Objects held in memory when the service worker is idle prevent it from being garbage collected. We audit our service worker code regularly for leaked references and unnecessary closures.
What we would do the same, and what we would change
Same: Vanilla JS. Local-first storage. Tab Sync as a first-class data source. The extension popup as a lightweight dashboard. ICS parsing in the client instead of on a server.
Change: We would invest in automated UI testing earlier. Manual testing across three browsers, multiple calendar providers, and various view configurations is time-consuming and error-prone. We would also design our sync engine modules with more formal versioning from the start, to make it easier to track which sync engine version corresponds to which calendar UI version.
Building a browser extension is a craft with its own rules, and most of those rules are not written down anywhere. We wrote manyCalendars to solve our own problem, and we learned more about browser internals in the process than we expected. If you are building one yourself, start small, test across browsers early, and respect the service worker lifecycle. It will save you weeks.
And if you just want the result without building anything, well, that is what manyCalendars is for. We did the hard part so you can skip straight to the unified calendar.