<?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: leftzzzz</title>
    <description>The latest articles on DEV Community by leftzzzz (@leftzzzz).</description>
    <link>https://dev.to/leftzzzz</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%2F4062640%2Ff2f14a78-b2ef-4edb-aef3-01f432b6b3db.png</url>
      <title>DEV Community: leftzzzz</title>
      <link>https://dev.to/leftzzzz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leftzzzz"/>
    <language>en</language>
    <item>
      <title>navigator.connection Lied to Me Twice: Shipping a 5.5 MB Python Runtime to Slow Networks</title>
      <dc:creator>leftzzzz</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:30:18 +0000</pubDate>
      <link>https://dev.to/leftzzzz/navigatorconnection-lied-to-me-twice-shipping-a-55-mb-python-runtime-to-slow-networks-22kf</link>
      <guid>https://dev.to/leftzzzz/navigatorconnection-lied-to-me-twice-shipping-a-55-mb-python-runtime-to-slow-networks-22kf</guid>
      <description>&lt;p&gt;Our browser-based Python playground runs CPython via Pyodide — no backend, no container, everything client-side. That means every first-time visitor downloads about &lt;strong&gt;5.55 MB&lt;/strong&gt; of runtime before they can execute a single line.&lt;/p&gt;

&lt;p&gt;For most people that's a few seconds. For the rest, it was a silent failure.&lt;/p&gt;

&lt;p&gt;Fixing it took three attempts, because the first two were built on measurements that turned out to be wrong. This post is about those wrong measurements — they're the part that generalizes past our particular stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sizing the problem, after distrusting the dashboard
&lt;/h2&gt;

&lt;p&gt;Over 14 days we had &lt;strong&gt;626 unresolved Sentry events&lt;/strong&gt;. The dashboard sorted them into a dozen-odd issues, which is exactly the view that makes you start at the top and work down.&lt;/p&gt;

&lt;p&gt;Instead we regrouped every issue by actual root cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;Events&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A. Runtime fetch failed&lt;/td&gt;
&lt;td&gt;~334&lt;/td&gt;
&lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. Cascading errors from A&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. Empty message / fatal with no info&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D. Third-party injection &amp;amp; abort noise&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E. Storage / filesystem robustness&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A and B are the same bug.&lt;/strong&gt; One problem was producing 69% of our error volume, and the default issue list didn't show it that way — it showed five separate-looking issues in family A alone.&lt;/p&gt;

&lt;p&gt;Two more things fell out of the grouping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;135 of 137 samples came from a single locale of a single tool page.&lt;/strong&gt; Not spread across the site. One page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS split: Windows 114 / Mac 12 / Android 10.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That Windows number is the one that stuck with us. It's wildly out of line with a normal desktop/mobile mix, and it's the kind of skew you only see if you group by root cause first and &lt;em&gt;then&lt;/em&gt; look at the dimensions. Sorted by issue count, it's invisible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trap #1: the error title was lying
&lt;/h2&gt;

&lt;p&gt;Our largest issue was titled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;undefined is not an object (evaluating 'str.length')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That reads like a null-safety bug. Someone forgot a guard, right?&lt;/p&gt;

&lt;p&gt;We sampled 100 events from that issue and counted the &lt;em&gt;actual&lt;/em&gt; &lt;code&gt;metadata.value&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Real message&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PYODIDE_INIT_TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;importScripts load failure&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;str.length&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cannot read properties of undefined ('length')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;89% of the events had nothing to do with the title.&lt;/strong&gt; The title was just whichever error happened to arrive first and name the group.&lt;/p&gt;

&lt;p&gt;The cause was our own code:&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="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An explicit fingerprint overrides Sentry's default grouping. We had told it to group by &lt;em&gt;where&lt;/em&gt; the error happened, not &lt;em&gt;what&lt;/em&gt; happened — so timeouts, network failures, and genuine type errors all collapsed into one issue wearing the name of the first arrival.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; the moment you set an explicit &lt;code&gt;fingerprint&lt;/code&gt;, the issue title stops being evidence. Before prioritizing off a dashboard, sample the events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/0/issues/{id}/events/?full=true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and count the real distribution of &lt;code&gt;metadata.value&lt;/code&gt;. We nearly spent a sprint chasing a &lt;code&gt;str.length&lt;/code&gt; bug that was 4% of the volume.&lt;/p&gt;

&lt;p&gt;The fix: add an &lt;code&gt;error_kind&lt;/code&gt; field and put &lt;em&gt;that&lt;/em&gt; in the fingerprint. The single mega-issue immediately split into distinct groups for init-timeout vs. runtime-error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trap #2: &lt;code&gt;navigator.connection.downlink&lt;/code&gt; is a placeholder before it's a measurement
&lt;/h2&gt;

&lt;p&gt;With the real problem identified — slow links timing out — the obvious move was to stop eagerly preloading the runtime for people on slow connections.&lt;/p&gt;

&lt;p&gt;First version gated on bandwidth:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;downlink&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;preloadRuntime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a normal office connection, Chrome reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;downlink&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.7&lt;/span&gt;
&lt;span class="na"&gt;rtt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Constant. Unchanged over 8 seconds. On a connection doing far better than 1.7 Mbps.&lt;/p&gt;

&lt;p&gt;That pair — &lt;code&gt;1.7 / 250&lt;/code&gt; — is the &lt;strong&gt;default 4g placeholder&lt;/strong&gt; Chrome returns when it has no throughput history to draw on. A brand-new profile on its first visit gets the placeholder, not a measurement. And a first-time visitor is &lt;em&gt;exactly&lt;/em&gt; the person who has to download the whole 5.55 MB.&lt;/p&gt;

&lt;p&gt;So the gate had it precisely backwards: it excluded first-time visitors on fast connections from an optimization designed for them. It turned 8 existing e2e tests red immediately.&lt;/p&gt;

&lt;p&gt;Worth being fair to the API: the &lt;code&gt;downlink&lt;/code&gt; values arriving from real users in production &lt;strong&gt;were&lt;/strong&gt; meaningful — 3g clustered at 0.15–1.55, 2g at 0.05–0.25, rtt ranging from 50 ms on 4g to 2700 ms on slow-2g. The field isn't garbage. It's untrustworthy specifically on a cold profile, which is the case you most need it for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trap #3: &lt;code&gt;effectiveType&lt;/code&gt; oscillates, and sampling it once is a coin flip
&lt;/h2&gt;

&lt;p&gt;Second version switched to the coarser signal:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slow-2g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effectiveType&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;preloadRuntime&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;Deployed. Production e2e: &lt;strong&gt;7/7 green.&lt;/strong&gt; Ship it.&lt;/p&gt;

&lt;p&gt;Then we ran the same suite three more times and &lt;em&gt;none&lt;/em&gt; of those runs preloaded.&lt;/p&gt;

&lt;p&gt;Instrumenting an identical page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;at load:     effectiveType = 3g
+3 seconds:  effectiveType = 4g
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full sequence oscillates &lt;code&gt;4g → 3g → 4g&lt;/code&gt; over the first few seconds of page life as Chrome revises its estimate.&lt;/p&gt;

&lt;p&gt;Our gate ran inside a single &lt;code&gt;requestIdleCallback&lt;/code&gt;. One sample. If that callback fired during the &lt;code&gt;3g&lt;/code&gt; dip, the visitor silently lost preloading for the whole session — on a perfectly fast connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 7/7 green run wasn't a passing test. It was a sampling accident.&lt;/strong&gt; A single green run against a value that moves tells you nothing. We only caught it by re-running the unchanged suite three more times.&lt;/p&gt;

&lt;p&gt;The shipped version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;skips preload when &lt;code&gt;effectiveType ∈ {slow-2g, 2g, 3g}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;subscribes to &lt;code&gt;connection.change&lt;/code&gt;&lt;/strong&gt; and re-evaluates, instead of sampling once&lt;/li&gt;
&lt;li&gt;additionally consults a persisted "this device actually timed out before" flag, cleared once a manual run succeeds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;No new self-hosted assets — outbound bytes only went down:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Init budget&lt;/td&gt;
&lt;td&gt;Two-stage: 20s quiet period before bulk download (that phase is only ~246 KB), then a bandwidth-derived 60–120s once large files start, plus a 12s no-byte-progress kill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;Wrapped &lt;code&gt;fetch&lt;/code&gt; inside the Worker, turning a previously silent 5.5 MB into byte-level progress that feeds both the heartbeat and the progress bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching&lt;/td&gt;
&lt;td&gt;Write to Cache Storage after successful init; repeat visits download nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For reference, the transfer breakdown (brotli, via jsDelivr):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Raw&lt;/th&gt;
&lt;th&gt;Compressed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pyodide.asm.wasm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.09 MB&lt;/td&gt;
&lt;td&gt;2.99 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python_stdlib.zip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.34 MB&lt;/td&gt;
&lt;td&gt;2.31 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pyodide.asm.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.23 MB&lt;/td&gt;
&lt;td&gt;0.24 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The stdlib zip is the interesting row: &lt;strong&gt;2.34 MB compresses to 2.31 MB.&lt;/strong&gt; It's already-compressed data, so brotli buys you essentially nothing. If you're budgeting a Pyodide load, don't assume one compression ratio across the whole payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  The retry that makes things worse
&lt;/h3&gt;

&lt;p&gt;One change is worth calling out separately, because the instinct runs the other way.&lt;/p&gt;

&lt;p&gt;Our original fallback logic was the standard one: on timeout, switch to a backup CDN and retry. That's correct for &lt;em&gt;unreachable source&lt;/em&gt; — DNS failure, 403, regional block.&lt;/p&gt;

&lt;p&gt;It is actively harmful for &lt;em&gt;slow link&lt;/em&gt;. If you've already been streaming the 5.5 MB for ninety seconds and you switch sources, you don't resume — you download 5.5 MB again from zero, on the same slow connection that couldn't finish it the first time. The retry makes the user's situation strictly worse and doubles your egress.&lt;/p&gt;

&lt;p&gt;So the rule became: &lt;strong&gt;once bulk transfer has begun, a timeout never switches sources.&lt;/strong&gt; Source-switching is only allowed during the discovery phase, before large files start moving. Past that point a timeout is diagnosed as a bandwidth problem and handled by extending the budget, not by starting over.&lt;/p&gt;

&lt;p&gt;Generalizes cleanly: retry is for &lt;em&gt;the source is broken&lt;/em&gt;, not for &lt;em&gt;the pipe is narrow&lt;/em&gt;. Retrying a narrow pipe just re-runs the thing that was already failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus trap: the error path had its own error
&lt;/h2&gt;

&lt;p&gt;This one didn't cause the outage, but it made the outage unreadable — and it explains a good chunk of Family C ("empty message / fatal with no info") above.&lt;/p&gt;

&lt;p&gt;We render a tooltip per init stage:&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="nf"&gt;getStageTooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// → t(`runtime.stage.${stage}`)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default branch interpolated the stage name straight into an i18n key. When we added new stages during this very fix, the new names had no translation entry — so the lookup threw &lt;code&gt;MISSING_MESSAGE&lt;/code&gt;. And because that call happens during render, the throw landed &lt;strong&gt;inside the error boundary that was trying to display the original runtime error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Result: the real error was replaced by a translation error, in the exact scenario where you most need the real error. Users saw a blank fatal; we saw events with no message.&lt;/p&gt;

&lt;p&gt;Fix: the default branch became a whitelist lookup, returning a safe fallback for any unknown stage rather than constructing a key that might not exist.&lt;/p&gt;

&lt;p&gt;The general shape: &lt;strong&gt;anything on the error-display path must not be able to fail.&lt;/strong&gt; If your error boundary, your logger, or your fallback UI can throw, then your worst incidents are also your least observable ones. Audit that path for dynamic key construction, non-null assertions, and anything that assumes data that a failure state might not have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two bugs the e2e suite caught that no amount of reading would have
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The heartbeat killed slow links.&lt;/strong&gt; We added a 12-second "no progress = dead" check. But &lt;code&gt;pyodide.asm.js&lt;/code&gt; loads via a &lt;em&gt;synchronous&lt;/em&gt; &lt;code&gt;importScripts&lt;/code&gt;, which blocks the Worker thread — it cannot emit progress even while working perfectly. On a slow link that phase exceeds 12s, so the watchdog killed healthy loads. Fix: the stall detector only arms after bulk download begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cache backfill on the hot path caused backpressure.&lt;/strong&gt; Writing to Cache Storage with &lt;code&gt;clone()&lt;/code&gt; &lt;em&gt;during&lt;/em&gt; init dragged initialization from &lt;strong&gt;8 seconds to 2 minutes&lt;/strong&gt;. The existing e2e suite went from 1.2 minutes to 7.3 minutes with 8/11 failing.&lt;/p&gt;

&lt;p&gt;The second one is worth dwelling on, because the symptom — "everything is slow now" — pointed nowhere in particular. We found it by &lt;strong&gt;re-running the identical suite against &lt;code&gt;origin/master&lt;/code&gt;&lt;/strong&gt; to establish a baseline. The comparison localized it immediately.&lt;/p&gt;

&lt;p&gt;When a change makes things globally slower, a baseline run beats reading the diff. Diff-reading biases you toward the code you &lt;em&gt;think&lt;/em&gt; is hot; a baseline tells you how much slower, which narrows the candidates far faster.&lt;/p&gt;

&lt;p&gt;Fix: move the backfill to after init succeeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Production e2e: 8/8 passing&lt;/li&gt;
&lt;li&gt;Auto-preload: 3/3 stable across consecutive runs — the metric that actually mattered, given trap #3&lt;/li&gt;
&lt;li&gt;Stage sequence: &lt;code&gt;loading_script → script_loaded → downloading_runtime → initialized → ready&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cache Storage populated; zero large-file requests on reload&lt;/li&gt;
&lt;li&gt;Sentry: the former mega-issue now splits by &lt;code&gt;error_kind&lt;/code&gt; into distinct init-timeout and runtime-error groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One testing note that saved a lot of noise: to verify the new &lt;code&gt;error_kind&lt;/code&gt; tag actually reached Sentry, we used Playwright to intercept &lt;code&gt;*.sentry.io/**/envelope/&lt;/code&gt;, read the payload, then &lt;code&gt;abort()&lt;/code&gt; the request. You can assert on exact tags without writing test events into production Sentry.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;The three traps are the same shape: &lt;strong&gt;a signal that looks like a measurement but is actually a default, an alias, or a snapshot of something moving.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Sentry title was an alias for a grouping we defined ourselves&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;downlink&lt;/code&gt; was a default standing in for a measurement&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;effectiveType&lt;/code&gt; was a snapshot of a value changing underneath us&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the tell was the same all three times: &lt;strong&gt;things agreed too readily.&lt;/strong&gt; The dashboard handed us one tidy issue. The bandwidth number never moved across 8 seconds. The test suite went green on the first run. In each case the friction we should have hit was missing — and that absence was the signal we kept failing to read.&lt;/p&gt;

&lt;p&gt;The cheap habit that would have caught all three: ask what the value looks like when the system has &lt;em&gt;nothing to report&lt;/em&gt;. A default placeholder, a first-arrival label, an early estimate. Then check whether you can tell that state apart from a real reading. In all three cases here, we couldn't — and that's the bug, before any of the specific numbers matter.&lt;/p&gt;

&lt;p&gt;If you're building on the Network Information API specifically: treat it as a hint you subscribe to, never a gate you sample once — and remember it has no useful history on exactly the first visit you're trying to optimize.&lt;/p&gt;

&lt;p&gt;You can poke at the thing this was all about — &lt;a href="https://www.ilovedevtool.com/en-US/tools/python-playground" rel="noopener noreferrer"&gt;Python Playground&lt;/a&gt; — browser-only, no signup. First load pulls the runtime; reload after that should be instant, which is the entire point of the work above.&lt;/p&gt;

&lt;p&gt;Curious how others are handling Pyodide cold starts — particularly if you've found a preload signal more reliable than &lt;code&gt;effectiveType&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>python</category>
    </item>
  </channel>
</rss>
