<?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: Dmitry</title>
    <description>The latest articles on DEV Community by Dmitry (@dmitry_cherstvyi_0c87fabd).</description>
    <link>https://dev.to/dmitry_cherstvyi_0c87fabd</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%2F4201367%2Feef76e71-390c-4a9c-b5db-a25cb72d3b0a.png</url>
      <title>DEV Community: Dmitry</title>
      <link>https://dev.to/dmitry_cherstvyi_0c87fabd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmitry_cherstvyi_0c87fabd"/>
    <language>en</language>
    <item>
      <title>Write Your Chrome MV3 Background Worker Like It Never Dies</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Tue, 03 Nov 2026 23:27:00 +0000</pubDate>
      <link>https://dev.to/dmitry_cherstvyi_0c87fabd/write-your-chrome-mv3-background-worker-like-it-never-dies-11dj</link>
      <guid>https://dev.to/dmitry_cherstvyi_0c87fabd/write-your-chrome-mv3-background-worker-like-it-never-dies-11dj</guid>
      <description>&lt;p&gt;&lt;em&gt;How a build-time AST transform finally makes Manifest V3 state persistence painless&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Manifest V3 didn’t just change the extension model — it actively punishes normal JavaScript patterns.&lt;/p&gt;

&lt;p&gt;Your background service worker is killed after roughly 30 seconds of inactivity. When Chrome restarts it, every top-level &lt;code&gt;let&lt;/code&gt;, every &lt;code&gt;Map&lt;/code&gt;, every pending &lt;code&gt;setTimeout&lt;/code&gt; is gone. The next event handler starts from a clean slate.&lt;/p&gt;

&lt;p&gt;The official workaround is brutal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually serialize every piece of state to &lt;code&gt;chrome.storage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await&lt;/code&gt; it back on every event&lt;/li&gt;
&lt;li&gt;Rewrite long-running timers as &lt;code&gt;chrome.alarms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Maintain careful locking so concurrent restarts don’t cause duplicate work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is hundreds of lines of defensive plumbing that has nothing to do with your actual feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  A different approach: transform the code, not the developer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;vite-mv3-sw-compile-persist&lt;/strong&gt; takes a build-time approach. You keep writing ordinary JavaScript. The plugin rewrites your background entry so that state and timers actually survive restarts.&lt;/p&gt;

&lt;p&gt;You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isSyncing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onClicked&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;cache&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="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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the transform you effectively get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic dirty tracking on every mutation&lt;/li&gt;
&lt;li&gt;Restoration from &lt;code&gt;chrome.storage.session&lt;/code&gt; (or &lt;code&gt;local&lt;/code&gt;) on startup&lt;/li&gt;
&lt;li&gt;Long timers backed by &lt;code&gt;chrome.alarms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Recovery of registered callbacks after a worker death&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No runtime library to import. No decorators. No wrapper APIs. The source stays clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the transform actually works
&lt;/h3&gt;

&lt;p&gt;The plugin runs three passes over the background entry:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Analyze (Babel AST)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It walks the AST and collects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-level &lt;code&gt;let&lt;/code&gt; / &lt;code&gt;var&lt;/code&gt; declarations (with special handling for &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;, arrays, and plain objects)&lt;/li&gt;
&lt;li&gt;Every assignment and method call that mutates those variables&lt;/li&gt;
&lt;li&gt;All &lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; / &lt;code&gt;clearTimeout&lt;/code&gt; / &lt;code&gt;clearInterval&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;Event handlers registered via &lt;code&gt;.addListener&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Function declarations (needed for callback recovery)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is careful about control flow. Bare bodies of &lt;code&gt;if&lt;/code&gt; / &lt;code&gt;for&lt;/code&gt; / &lt;code&gt;while&lt;/code&gt; are detected so the later injection of dirty-tracking code remains inside the correct branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Transform (magic-string)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After every relevant assignment or mutation it injects &lt;code&gt;__mv3_markDirty('varName')&lt;/code&gt;. When necessary it wraps the statement in braces first.&lt;/li&gt;
&lt;li&gt;Long timers (default threshold: 60 000 ms) are rewritten to &lt;code&gt;__mv3_setTimeout&lt;/code&gt; / &lt;code&gt;__mv3_setInterval&lt;/code&gt; wrappers that use &lt;code&gt;chrome.alarms&lt;/code&gt; under the hood.&lt;/li&gt;
&lt;li&gt;Short timers stay native (with optional offscreen-document keepalive).&lt;/li&gt;
&lt;li&gt;Event handlers that do not return a value are made &lt;code&gt;async&lt;/code&gt; and receive an &lt;code&gt;await __mv3_init&lt;/code&gt; so restored state is guaranteed to be ready before your logic runs.&lt;/li&gt;
&lt;li&gt;Handlers that &lt;em&gt;do&lt;/em&gt; return a value (classic &lt;code&gt;onMessage&lt;/code&gt; &lt;code&gt;return true&lt;/code&gt;, blocking &lt;code&gt;webRequest&lt;/code&gt;, etc.) are left completely untouched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Generate runtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A virtual module is emitted and inlined. It contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State restore on startup&lt;/li&gt;
&lt;li&gt;Debounced flush of dirty variables (default 2 s)&lt;/li&gt;
&lt;li&gt;Serialization / deserialization for &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;, and arrays&lt;/li&gt;
&lt;li&gt;Alarm registration and callback recovery via a function registry&lt;/li&gt;
&lt;li&gt;Stale-lock recovery for boolean flags (so an &lt;code&gt;isRunning = true&lt;/code&gt; that never got cleared doesn’t deadlock the next incarnation)&lt;/li&gt;
&lt;li&gt;Optional Proxy-based reactivity when &lt;code&gt;stateStrategy: 'proxy'&lt;/code&gt; is enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common path stays fast — almost nothing runs unless the worker actually restarts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration that matters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;mv3Persistent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;backgroundEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/background&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;index&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;ts$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stateStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;proxy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// catch nested + Map/Set/Date mutations&lt;/span&gt;
  &lt;span class="na"&gt;storageArea&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// or 'local'&lt;/span&gt;
  &lt;span class="na"&gt;alarmThresholdMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoFlushMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;keepaliveStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alarm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// 'alarm' | 'offscreen' | 'none'&lt;/span&gt;
  &lt;span class="na"&gt;staleLockThresholdMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;promiseResolverStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;namedAlarmHandlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;            &lt;span class="c1"&gt;// for callbacks that can’t be auto-recovered&lt;/span&gt;
    &lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;runCleanup&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stateStrategy: 'static'&lt;/code&gt; is faster and sufficient for simple scalar / top-level reassignment patterns.&lt;br&gt;&lt;br&gt;
&lt;code&gt;proxy&lt;/code&gt; is the recommended setting for real applications that mutate objects, Maps, or Sets.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it deliberately does &lt;em&gt;not&lt;/em&gt; break
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chrome.runtime.onMessage&lt;/code&gt; handlers that return &lt;code&gt;true&lt;/code&gt; (or any value) stay synchronous.&lt;/li&gt;
&lt;li&gt;Blocking &lt;code&gt;webRequest&lt;/code&gt; listeners are left alone.&lt;/li&gt;
&lt;li&gt;Source maps are preserved.&lt;/li&gt;
&lt;li&gt;The generated runtime is small and only activated on the restart path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Current limitations (honest list)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Destructuring declarations (&lt;code&gt;let { a, b } = …&lt;/code&gt;) are not supported for persistence — use individual &lt;code&gt;let&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;Anonymous timer callbacks that close over non-persisted local variables cannot be reliably recovered after a restart. Prefer named functions or register them via &lt;code&gt;namedAlarmHandlers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;new Promise(resolve =&amp;gt; setTimeout(resolve, …))&lt;/code&gt; is inherently fragile when the worker can die. The plugin warns and offers strategies, but there is no perfect solution.&lt;/li&gt;
&lt;li&gt;Requires Vite ≥ 8 and the &lt;code&gt;"storage"&lt;/code&gt; (plus &lt;code&gt;"alarms"&lt;/code&gt; when using long timers) permission.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this approach feels different
&lt;/h3&gt;

&lt;p&gt;Most MV3 advice forces you to change &lt;em&gt;how&lt;/em&gt; you write code.&lt;br&gt;&lt;br&gt;
This plugin changes the &lt;em&gt;compiled output&lt;/em&gt; so that ordinary code starts behaving correctly.&lt;/p&gt;

&lt;p&gt;The difference in day-to-day development is large: you stop thinking about the service-worker lifecycle on every line and start focusing on the feature again.&lt;/p&gt;




&lt;p&gt;If you maintain a non-trivial MV3 extension, try it on a side branch. The first time you kill the worker from &lt;code&gt;chrome://serviceworker-internals&lt;/code&gt; and watch your counter keep incrementing, the value becomes obvious.&lt;br&gt;
Here’s the improved ending with a clear GitHub link and a proper call for stars:&lt;/p&gt;




&lt;p&gt;If you maintain a non-trivial MV3 extension, try it on a side branch. The first time you kill the worker from &lt;code&gt;chrome://serviceworker-internals&lt;/code&gt; and watch your counter keep incrementing, the value becomes obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/GameGC/vite-mv3-sw-compile-persist" rel="noopener noreferrer"&gt;https://github.com/GameGC/vite-mv3-sw-compile-persist&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this plugin saves you from the MV3 state-management hell, &lt;strong&gt;please give it a ⭐&lt;/strong&gt;. It really helps more people find it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>vite</category>
      <category>extention</category>
      <category>node</category>
    </item>
  </channel>
</rss>
