<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yahya Muhammad</title>
    <description>The latest articles on DEV Community by Yahya Muhammad (@yahya_muhammad_50fb354d7d).</description>
    <link>https://dev.to/yahya_muhammad_50fb354d7d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4147202%2Fd76d5433-3dba-4556-a1f5-17278a03a4ce.png</url>
      <title>DEV Community: Yahya Muhammad</title>
      <link>https://dev.to/yahya_muhammad_50fb354d7d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yahya_muhammad_50fb354d7d"/>
    <language>en</language>
    <item>
      <title>What breaks when you move a Chrome extension to Manifest V3, and the order I fix it in</title>
      <dc:creator>Yahya Muhammad</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:18:59 +0000</pubDate>
      <link>https://dev.to/yahya_muhammad_50fb354d7d/what-breaks-when-you-move-a-chrome-extension-to-manifest-v3-and-the-order-i-fix-it-in-pn3</link>
      <guid>https://dev.to/yahya_muhammad_50fb354d7d/what-breaks-when-you-move-a-chrome-extension-to-manifest-v3-and-the-order-i-fix-it-in-pn3</guid>
      <description>&lt;p&gt;I have moved a fair number of extensions from Manifest V2 to V3 now. The code changes are not the hard part. The hard part is that four things break at once and people fix them in the wrong order, so they spend a week chasing a bug that comes from step one.&lt;/p&gt;

&lt;p&gt;This is the order that works for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The background stops remembering things
&lt;/h2&gt;

&lt;p&gt;In V2 the background page stayed alive. In V3 it is a service worker and Chrome shuts it down after about thirty seconds of nothing happening. Every global variable you had is gone when it wakes up.&lt;/p&gt;

&lt;p&gt;So I do this first, before touching anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every listener registered at the top level of the file, not inside a callback&lt;/li&gt;
&lt;li&gt;every global turned into a &lt;code&gt;chrome.storage&lt;/code&gt; read&lt;/li&gt;
&lt;li&gt;every &lt;code&gt;setInterval&lt;/code&gt; longer than a few seconds turned into &lt;code&gt;chrome.alarms&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wrong: lost when the worker sleeps&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastSync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// right: survives a restart&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;periodInMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onAlarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastSync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lastSync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;lastSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the extension works with the popup open and fails when it is closed, this is the reason nine times out of ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Blocking network requests in JavaScript is gone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;webRequest&lt;/code&gt; blocking does not run any more. Rules move to &lt;code&gt;declarativeNetRequest&lt;/code&gt;, a JSON file for the fixed ones and &lt;code&gt;updateDynamicRules&lt;/code&gt; for the ones that change. It is less flexible. For most extensions it is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A service worker has no DOM
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DOMParser&lt;/code&gt;, clipboard and audio do not exist in the worker. In Chrome and Edge they go into an offscreen document. You give it one reason when you create it, and the store reviewer reads that line, so write a real one.&lt;/p&gt;

&lt;p&gt;Firefox has no offscreen API. My code checks for it and skips that part instead of throwing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Permissions
&lt;/h2&gt;

&lt;p&gt;What the extension needs at install goes in &lt;code&gt;host_permissions&lt;/code&gt;. Everything else goes in &lt;code&gt;optional_host_permissions&lt;/code&gt; and gets requested at the moment the user does the thing. Reviews go faster this way. It is also the step people skip, and then the listing sits in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two stores, one codebase
&lt;/h2&gt;

&lt;p&gt;I do not keep a second manifest for Firefox. I keep a small overrides file and merge it at build time, so the two never drift apart. One command gives me a zip for Chrome and Edge and a zip for Firefox.&lt;/p&gt;

&lt;p&gt;Before every submission I run &lt;code&gt;web-ext lint&lt;/code&gt;. The Firefox reviewers run the same tool, so there is no point finding out from them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skeleton
&lt;/h2&gt;

&lt;p&gt;I put the whole thing in a repo so I do not rebuild it each time: background, offscreen document, rules file, content script, the Firefox overrides and the build script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/INNERLUXES/mv3-extension-starter" rel="noopener noreferrer"&gt;mv3-extension-starter on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not perfect. The offscreen part does nothing useful on Firefox, and the rules file only covers the common cases. But it starts from a place where the worker restarting does not break anything, which is where most migrations go wrong.&lt;/p&gt;

&lt;p&gt;If you would rather hand the migration over, my team does this work: &lt;a href="https://innerluxes.dev/browser-development/chrome-extensions" rel="noopener noreferrer"&gt;Chrome extension developers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What broke first in your migration? I am curious whether it was the worker for you too.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>chrome</category>
      <category>extensions</category>
    </item>
  </channel>
</rss>
