<?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: haibo kan</title>
    <description>The latest articles on DEV Community by haibo kan (@haibo_kan_9d238a8ee4215b9).</description>
    <link>https://dev.to/haibo_kan_9d238a8ee4215b9</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%2F4229531%2Ff56649eb-9e44-4256-8cdc-67ac7046eb01.png</url>
      <title>DEV Community: haibo kan</title>
      <link>https://dev.to/haibo_kan_9d238a8ee4215b9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/haibo_kan_9d238a8ee4215b9"/>
    <language>en</language>
    <item>
      <title>Keeping a game widget from eating CPU when 40 tabs are open</title>
      <dc:creator>haibo kan</dc:creator>
      <pubDate>Fri, 06 Nov 2026 05:47:33 +0000</pubDate>
      <link>https://dev.to/haibo_kan_9d238a8ee4215b9/keeping-a-game-widget-from-eating-cpu-when-40-tabs-are-open-44aj</link>
      <guid>https://dev.to/haibo_kan_9d238a8ee4215b9/keeping-a-game-widget-from-eating-cpu-when-40-tabs-are-open-44aj</guid>
      <description>&lt;p&gt;Collapse, a hidden tab, and switching games all get called "pause". They need three different code paths, and lumping them together is where the state bugs come from.&lt;/p&gt;

&lt;p&gt;The widget is a small draggable ball in the corner of every page: click it, pick a game, press Space, click again to fold it away with the round intact. Across forty open tabs, taken literally, that's forty game loops. The CSS half of keeping that ball on arbitrary pages is in &lt;a href="https://dev.to/haibo_kan_9d238a8ee4215b9/injecting-a-game-widget-into-every-page-without-breaking-the-host-sites-css-3h19"&gt;an earlier write-up&lt;/a&gt;. This is the other half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The widget is everywhere; the game isn't
&lt;/h2&gt;

&lt;p&gt;The content script mounts one host element, a Shadow DOM root and a drag handler. It does no work when idle. Pick a game and only then does an iframe get created, the bundle load, and the loop start. Switch games or collapse the panel and the iframe is destroyed. No persistent background page holding a loop, no service worker ticking on a timer.&lt;/p&gt;

&lt;p&gt;At most one loop per active widget, usually zero. Forty tabs means forty idle elements, not forty &lt;code&gt;requestAnimationFrame&lt;/code&gt; loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three kinds of pause
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Collapse / Esc&lt;/strong&gt; — explicit user action. Freeze immediately, keep state. This one has to feel instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab hidden&lt;/strong&gt; — the browser already throttles &lt;code&gt;requestAnimationFrame&lt;/code&gt; in background tabs. A loop driven by rAF stops advancing on its own; tearing anything down here just adds a state-restore bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switching games&lt;/strong&gt; — not a pause. Tear down the iframe, start clean. Keeping a hidden game alive "just in case" is exactly how you end up with forty loops.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Store the smallest thing that restores a round
&lt;/h2&gt;

&lt;p&gt;Serializing the whole game object is tempting. Most of it is derived — sprite positions, timers, DOM nodes — and rehydrating it is brittle. What needs to survive:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;           &lt;span class="c1"&gt;// board state only&lt;/span&gt;
  &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nextDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1840&lt;/span&gt;             &lt;span class="c1"&gt;// logical ticks elapsed, not wall-clock ms&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;tick&lt;/code&gt; instead of &lt;code&gt;Date.now()&lt;/code&gt; is the part I'd keep even if everything else changed. Derive your speed curve from wall-clock time and a round that sat paused for twenty minutes comes back at a different difficulty. A logical counter only advances inside the loop.&lt;/p&gt;

&lt;p&gt;The other trap is writing on every frame. &lt;code&gt;chrome.storage.local&lt;/code&gt; isn't a frame buffer: pause, game over, and a coarse interval, never inside the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  One exception, again
&lt;/h2&gt;

&lt;p&gt;Click-through means the widget stops intercepting the mouse entirely. &lt;code&gt;pointer-events: none&lt;/code&gt; on the widget surface is the obvious implementation, but the drag handle still needs events. So the handle keeps &lt;code&gt;pointer-events: auto&lt;/code&gt; and the rest is disabled as a layer — toggling one global flag leaves you wondering why the ball can't be moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;"Paused" should have been three states from day one. And the save-state shape should have been designed before the game code, not after — deciding what to serialize late is how the game object ends up leaking into storage, versioning a format nobody designed.&lt;/p&gt;

&lt;p&gt;If you want to see it running, the extension is InTab Arcade. It's free, with no account and no subscription; a small sponsored banner sits on the game-selection screen.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>chrome</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Injecting a game widget into every page without breaking the host site's CSS</title>
      <dc:creator>haibo kan</dc:creator>
      <pubDate>Fri, 06 Nov 2026 03:34:23 +0000</pubDate>
      <link>https://dev.to/haibo_kan_9d238a8ee4215b9/injecting-a-game-widget-into-every-page-without-breaking-the-host-sites-css-3h19</link>
      <guid>https://dev.to/haibo_kan_9d238a8ee4215b9/injecting-a-game-widget-into-every-page-without-breaking-the-host-sites-css-3h19</guid>
      <description>&lt;p&gt;The widget inherits &lt;code&gt;font&lt;/code&gt; from whatever page it's on. Shadow DOM doesn't stop that. Neither does it stop &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;line-height&lt;/code&gt; or &lt;code&gt;text-align&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's a small draggable ball in the corner of every page. Click it, pick a game, press Space. Click again or hit Esc and it folds away, pausing the game with its state intact. You never open a new tab — so the widget has to coexist with global resets, &lt;code&gt;!important&lt;/code&gt; rules, and frameworks with opinions about base styles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Shadow DOM stops
&lt;/h2&gt;

&lt;p&gt;A rule targeting &lt;code&gt;.button&lt;/code&gt; on the host page won't reach the markup inside my root, and mine won't leak out. That part works as advertised.&lt;/p&gt;

&lt;p&gt;Inheritable properties still arrive, though. On a page that sets &lt;code&gt;font-family: "Comic Sans MS"&lt;/code&gt; or a cramped base &lt;code&gt;line-height&lt;/code&gt;, the widget picks it up. I reset the inheritable set explicitly on &lt;code&gt;:host&lt;/code&gt; and put &lt;code&gt;all: initial&lt;/code&gt; inside the root before any of my own styles.&lt;/p&gt;

&lt;p&gt;Custom properties were the one I didn't expect. They inherit across the boundary by design, so a page with &lt;code&gt;--primary&lt;/code&gt; or &lt;code&gt;--radius&lt;/code&gt; will quietly restyle your widget if you use either name. Everything is namespaced now — &lt;code&gt;--ita-primary&lt;/code&gt; and so on. &lt;code&gt;all: unset&lt;/code&gt; on the host side changes nothing; the leak is on the way in.&lt;/p&gt;

&lt;p&gt;Then there's the host element itself. It's still a &lt;code&gt;div&lt;/code&gt; in someone else's DOM, so &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;[class]&lt;/code&gt; and broad structural selectors can hit it. Keeping that surface to one element, no classes, styles only on &lt;code&gt;:host&lt;/code&gt;, leaves very little to match.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;!important&lt;/code&gt; from the host doesn't reach in. I test against a handful of real sites with aggressive resets anyway — &lt;code&gt;@layer&lt;/code&gt; and constructable stylesheets are worth seeing in practice rather than reasoning about from the spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forty tabs
&lt;/h2&gt;

&lt;p&gt;The quieter failure mode is every open tab paying for a game nobody's playing. Games load on demand into iframes; the iframe is destroyed when you switch away or collapse the panel. State lives in &lt;code&gt;chrome.storage&lt;/code&gt;, so teardown doesn't lose the round.&lt;/p&gt;

&lt;p&gt;That leaves the content script with a host element, an iframe lifecycle and a drag handler. Nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  One exception to pointer-events
&lt;/h2&gt;

&lt;p&gt;Click-through mode stops the widget stealing a click from the page underneath. &lt;code&gt;pointer-events: none&lt;/code&gt; on the shadow host looks like a one-line fix, except the drag handle still needs events — and children can't reliably opt back in with &lt;code&gt;pointer-events: auto&lt;/code&gt; in every engine.&lt;/p&gt;

&lt;p&gt;The host stays interactive. The inner surface is transparent to pointers, and the handle is the single explicitly interactive child. Otherwise the ball sits there and won't move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest
&lt;/h2&gt;

&lt;p&gt;The extension is completely free — no subscriptions and no account. A small sponsored banner sits on the game-selection screen; its SDK measures impressions and clicks anonymously, with no personal data and no third-party cookies. Widget position, theme and panel size stay local in &lt;code&gt;chrome.storage&lt;/code&gt;. It doesn't read browsing history, page content or form data.&lt;/p&gt;

&lt;p&gt;The install is heavier than I'd like — the emulator bundle dominates it. Lazy-loading that separately is on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions
&lt;/h2&gt;

&lt;p&gt;Is Shadow DOM still the right call for injected content in 2026, or is there a cleaner isolation pattern I'm missing?&lt;/p&gt;

&lt;p&gt;And how do others handle the inherited-property reset without shipping a giant &lt;code&gt;all: initial&lt;/code&gt; block?&lt;/p&gt;

&lt;p&gt;The extension is &lt;a href="https://chromewebstore.google.com/detail/intab-arcade/hpoanmnpjfkfcaidboedpibieinglaio" rel="noopener noreferrer"&gt;InTab Arcade&lt;/a&gt; if you want to poke at it.&lt;/p&gt;

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