<?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: Extinde</title>
    <description>The latest articles on DEV Community by Extinde (@extinde).</description>
    <link>https://dev.to/extinde</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3978370%2Feb15eb2a-c43d-4117-b60a-5959c324b4cf.png</url>
      <title>DEV Community: Extinde</title>
      <link>https://dev.to/extinde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/extinde"/>
    <language>en</language>
    <item>
      <title>MV3 Chrome Extension Tutorial: What Changed and How to Build It Right</title>
      <dc:creator>Extinde</dc:creator>
      <pubDate>Wed, 10 Jun 2026 21:16:39 +0000</pubDate>
      <link>https://dev.to/extinde/mv3-chrome-extension-tutorial-what-changed-and-how-to-build-it-right-3pp6</link>
      <guid>https://dev.to/extinde/mv3-chrome-extension-tutorial-what-changed-and-how-to-build-it-right-3pp6</guid>
      <description>&lt;h2&gt;
  
  
  MV2 vs MV3 — The Four Breaking Changes
&lt;/h2&gt;

&lt;p&gt;These are the four changes that will actually break your code or reject your submission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Service worker replaces the persistent background page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MV2 background pages stayed alive indefinitely and held state in memory. MV3 replaces them with a service worker that terminates when idle. Any state stored in a JS variable is gone when it wakes back up. You need &lt;code&gt;chrome.storage&lt;/code&gt; for anything that has to persist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;action&lt;/code&gt; replaces &lt;code&gt;browser_action&lt;/code&gt; and &lt;code&gt;page_action&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two separate APIs collapsed into one. If your manifest still declares &lt;code&gt;browser_action&lt;/code&gt;, it will be ignored. Use &lt;code&gt;"action"&lt;/code&gt; in MV3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;declarativeNetRequest&lt;/code&gt; replaces &lt;code&gt;webRequest&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one that breaks ad blockers and request-intercepting tools. &lt;code&gt;webRequest&lt;/code&gt; gave extensions real-time access to modify or block network requests. &lt;code&gt;declarativeNetRequest&lt;/code&gt; uses a pre-declared rules file instead — the browser does the matching, your extension never sees the raw request. More performant, far less flexible. If you are building anything that intercepts requests dynamically, plan for this early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Remote code execution is blocked entirely&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eval()&lt;/code&gt;, &lt;code&gt;new Function()&lt;/code&gt;, and injecting remote scripts via &lt;code&gt;&amp;lt;script src="https://..."&amp;gt;&lt;/code&gt; in extension pages are gone. All logic must be bundled locally. If you were loading a CDN library in your popup HTML, it needs to be vendored.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MV3 File Structure
&lt;/h2&gt;

&lt;p&gt;A minimal MV3 extension looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-extension/
├── manifest.json
├── popup.html
├── popup.css
├── popup.js
├── background.js    (optional — service worker)
├── content.js       (optional — injected into pages)
└── icons/
    ├── icon16.png
    ├── icon48.png
    └── icon128.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;manifest.json&lt;/code&gt; is the only required file — everything else is declared from it. If your extension has no popup, skip &lt;code&gt;popup.html&lt;/code&gt;. If it doesn't need to run on pages, skip &lt;code&gt;content.js&lt;/code&gt;. Keep the structure flat until you have a reason to introduce subdirectories.&lt;/p&gt;




&lt;h2&gt;
  
  
  Complete MV3 manifest.json
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Must&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;submissions&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Extension"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What it does in one sentence."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Replaces&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;browser_action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;page_action&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_popup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"popup.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"16"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon16.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"48"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon48.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"128"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/icon128.png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"service_worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"background.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scripts"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;that's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MV&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;syntax&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"content_scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;don't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"run_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document_idle"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"host_permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://api.yourservice.com/*"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Required&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fetch()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;external&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URLs&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;chrome.storage.local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.sync&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"activeTab"&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;grants&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;access&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gesture&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  popup.js — The Two Things That Trip Everyone
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Wrap everything in &lt;code&gt;DOMContentLoaded&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your popup HTML loads, the browser parses it, then runs the linked script. If your JS tries to query a DOM element before the HTML is parsed, &lt;code&gt;document.getElementById()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; and nothing works. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myButton&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// your logic here&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;Without this wrapper, you will spend time debugging what looks like a broken event listener when the actual issue is a null reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use &lt;code&gt;chrome.storage.local&lt;/code&gt;, not &lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; works in a normal browser tab. In an extension popup, the popup is its own isolated page — it opens, does its work, and closes. Any &lt;code&gt;localStorage&lt;/code&gt; data is scoped to that page's origin and, more critically, is not accessible from background service workers or content scripts. Use &lt;code&gt;chrome.storage.local&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Write&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;userPreference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Read&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="s1"&gt;userPreference&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="nx"&gt;result&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPreference&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;&lt;code&gt;chrome.storage&lt;/code&gt; is accessible from every extension context (popup, service worker, content script). &lt;code&gt;localStorage&lt;/code&gt; is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Load Unpacked + Debug
&lt;/h2&gt;

&lt;p&gt;Go to &lt;code&gt;chrome://extensions&lt;/code&gt;, enable Developer mode (toggle top right), click &lt;strong&gt;Load unpacked&lt;/strong&gt;, and select your extension folder. The extension loads immediately — no packaging needed. To debug the popup, right-click the extension icon and choose &lt;strong&gt;Inspect Popup&lt;/strong&gt;. This opens a standard DevTools window scoped to popup.js. For the service worker, click the &lt;strong&gt;Service Worker&lt;/strong&gt; link directly on the extensions card in &lt;code&gt;chrome://extensions&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mistake I See Most Often in Generated Extension Code
&lt;/h2&gt;

&lt;p&gt;Building &lt;a href="https://extinde.com" rel="noopener noreferrer"&gt;Extinde&lt;/a&gt;, a tool that generates MV3 extensions from plain English prompts, means reviewing a lot of generated and hand-written extension code. These four patterns cause the most silent failures and review rejections:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;"matches": ["&amp;lt;all_urls&amp;gt;"]&lt;/code&gt; in content_scripts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This triggers the broadest permission warning shown to users on install. Most extensions don't actually need to run on every URL. Scope it to the domains you need. Chrome Web Store reviewers flag over-permissioning, and users see the warning before installing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;chrome.storage.sync&lt;/code&gt; when you need &lt;code&gt;chrome.storage.local&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sync&lt;/code&gt; has a 100KB total quota and an 8KB per-item limit. It's designed for small preference flags that sync across a user's devices. If you're storing anything substantial — cached data, user-generated content, API responses — use &lt;code&gt;local&lt;/code&gt;, which has a 10MB quota.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;host_permissions&lt;/code&gt; when &lt;code&gt;fetch()&lt;/code&gt; is used in a content script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your content script calls &lt;code&gt;fetch('https://api.example.com/...')&lt;/code&gt; and you haven't declared that host in &lt;code&gt;"host_permissions"&lt;/code&gt;, the request will fail silently in some cases and throw a CORS error in others. The fix is a single line in your manifest — but it's easy to miss because the error message doesn't always point back to the manifest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong path in &lt;code&gt;"service_worker"&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;background.js&lt;/code&gt; sits in a &lt;code&gt;src/&lt;/code&gt; folder and your manifest says &lt;code&gt;"service_worker": "background.js"&lt;/code&gt;, the service worker silently fails to register. No error on load, no console output — the worker just doesn't exist. Double-check that the path in the manifest matches exactly where the file lives relative to the manifest root.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build Without the Boilerplate
&lt;/h2&gt;

&lt;p&gt;Full publish walkthrough — Chrome Web Store submission, Firefox AMO, Edge Add-ons — is on the Extinde blog (link in canonical above). If you want to skip writing the manifest, file structure, and wiring from scratch, &lt;a href="https://extinde.com/create" rel="noopener noreferrer"&gt;extinde.com/create&lt;/a&gt; generates MV3-compliant extensions from a plain English prompt. Free tier includes 25 credits, no card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: &lt;code&gt;chrome&lt;/code&gt; &lt;code&gt;webdev&lt;/code&gt; &lt;code&gt;tutorial&lt;/code&gt; &lt;code&gt;javascript&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

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