Learnings from migrating Accessibility Insights for Web to Chrome’s Manifest V3
Since February 2022, the Accessibility Insights team has been migrating Accessibility Insights for Web–our Chrome and Edge extension introduced in Jacqueline’s February 14, 2022, post–from Manifest V2 (MV2) to Manifest V3 (MV3). We wanted to share learnings and takeaways from our migration journey with a walkthrough of the largest changes and considerations.
As Chrome deprecates previous manifest versions and enforces updates to MV3, the Accessibility Insights for Web extension required large architectural changes. These changes are complete and our team has updated the Accessibility Insights for Web extension to use MV3.
Background pages to service workers
The largest change between MV2 and MV3 is the switch from background pages to service workers. This is a fundamental change to the extension architecture since, unlike background pages which are persistent, service workers are shutdown when they go idle and restarted to respond to extension events.
The possibility of a service worker going idle, timing out, and shutting down while the extension is operating means that we needed to make significant changes in how we handled storage and event listeners in our extension.
Prior to MV3 changes, our extension relied on background pages that were persistent and stored necessary data in memory in the background process. Once we began using service workers, this was no longer feasible, as data stored in-process in service workers is lost as soon as the service worker times out and is shut down.
As a result, we began backing up necessary data in Chromium’s IndexedDB, a transactional database API to persist across service worker lifetimes and extension runs.
Backing up data in a storage API was crucial as a result of MV3 changes and came with some downsides needing to be addressed. For one, it introduced a small performance overhead that was not present when we were simply maintaining data in process. We solved this by minimizing calls to the storage API. In our MV3 extension, the only time we fetch IndexedDB data is on service worker startup. For the extent of the process’s lifetime, we maintain that data in process. While it is necessary to write to the IndexedDB every time we update in-process data to ensure that the data is not lost if the service worker shuts down unexpectedly, we have found that the performance overhead on these data updates is acceptable.
As the IndexedDB database is persistent across extension updates, these changes also required extra consideration for backwards compatibility. We had to consider the possibility of a data structure used in the IndexedDB stored data being updated such that it would not be compatible with existing persisted stored data. As this is an issue all future maintainers will have to keep in mind, we addressed it by documenting the issue in our repo and adding a bot to comment on pull requests that update store data structures to remind PR authors of this issue.
Event listener registration
In MV3, it is important that all browser event listeners are registered in the first event loop of the service worker and before any asynchronous calls. Otherwise, events may be missed if they are received during service worker initialization.
In our extension, some asynchronous setup work must be completed before the service worker can start responding to messages. Our solution includes wrapping browser event handling in an object that would register “placeholder” event handlers on the first event loop. Any events that these handlers receive are deferred to be handled later. Once the required asynchronous work is completed, we register our event listeners to this wrapper object, which passes the deferred events, and any events received afterwards, along to the new event listeners.
Event listener responses
The largest part of migrating to MV3 involved ensuring that service workers do not shutdown prematurely. Service workers can be shut down for several reasons, including watchdog timeouts. The browser may shut down an active service worker if the event loop is blocked for over 30 seconds, a runtime.onMessage callback returns an asynchronous response that does not resolve or reject within 5 minutes, or a runtime.connect connection is left open for over 5 minutes.
To prevent these scenarios, we audited our runtime.onMessage handling and removed all instances of `runtime.connect` from our extension. Previously, our content scripts maintained an open `runtime.connect` connection from the content scripts to the background and listened for the `runtime.onDisconnect` event to know when the extension was disabled and the content script should be cleaned up.
In our new MV3 extension, the content scripts periodically query runtime.id instead, which returns null if the extension is disabled. This solution’s benefits include not requiring regular messages to the background service worker and allowing the service worker to shut down if no work is being done. The browser may also shut down the service worker if it appears idle meaning that with MV3 it is important to properly track and await any asynchronous browser event listeners. Prior to the MV3 migration, our extension architecture could frequently invoke synchronous runtime.onMessage listeners which would kick off asynchronous work and return immediately. With MV2 background pages, this was not a problem, but with MV3 means the browser might not recognize the async work and prematurely consider the service worker as idle. As a result, most of the changes in our migration focused upon properly tracking async work by the browser listeners.
To address this, we began by wrapping any synchronous listeners that kicked off async work–what we called “fire and forget” listeners–with a two-minute timeout. This allowed us to fake an async response to the browser wherein, after the timeout, we returned an empty response. This change was a temporary measure allowing us to go through each “fire and forget” listener one by one and convert it to be async, thereby properly tracking any async work. Once all the listeners were correctly tracking their work, we removed the “fire and forget” timeout and replaced it with a real async response that resolves as soon as the work is complete.
During this work, we discovered that, if a browser devtools instance is open against an extension page or the extension service worker, the service worker will not shut down even if it is idle. This complicated our development process because opening and looking at console logs while the extension was running changed the service worker’s behavior. We kept this in mind while testing our MV3 extension and made sure we observed the service worker’s lifetime as a user would experience it.
Build and release
As maintenance and releases of our existing MV2 extension continued while the migration to MV3 was underway, we chose to dual build and test the two extensions in the same repo. This dual approach allowed us to avoid costly and risky branch merges while also allowing us to catch bugs in the migration changes when they appeared in the more commonly used MV2 extensions.
Similarly, we created extension release channels and pipelines for the in-progress versions of the MV3 extension that were separate but mirrors of those already existing for MV2. This proved vital to the migration effort since, as part of the depreciation of MV2, the Chrome store no longer allows publishing new MV2 extensions or downgrading MV3 extensions to MV2. We wanted to avoid upgrading any of our mainline extensions until we finished the migration and testing, since we would not be able to go back to MV2 once we upgraded.
Finally, Chrome’s switch to MV3 included many smaller–though still impactful–changes that we needed to address. These included several instances of restructuring and renaming in the chrome APIs that extensions can call into and changes to the manifest schema itself.
Also, due to the nature of service workers, our background process initialization performance became much more important. With MV2, background pages were only initialized once, so performance was not much of a concern, but with MV3, service workers are initialized repeatedly. To address this, we minimized initialization time as it could become significant.
With that, we wrap up the notable changes that were required to migrate to Chrome’s Manifest V3. Try out the MV3 version of Accessibility Insights for Web by downloading it from the Chrome webstore or Edge Add-ons page, or check out all the Accessibility Insights products and documentation at https://accessibilityinsights.io.