<?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: Alex</title>
    <description>The latest articles on DEV Community by Alex (@asmyshlyaev177).</description>
    <link>https://dev.to/asmyshlyaev177</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F186893%2F2f9e908a-4e24-4761-a5c5-4c39003c4c8a.gif</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/asmyshlyaev177</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/asmyshlyaev177"/>
    <language>en</language>
    <item>
      <title>I counted every Bun bug before and after the Rust rewrite</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:09:13 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/i-counted-every-bun-bug-before-and-after-the-rust-rewrite-3cgi</link>
      <guid>https://dev.to/asmyshlyaev177/i-counted-every-bun-bug-before-and-after-the-rust-rewrite-3cgi</guid>
      <description>&lt;p&gt;On May 14, Bun merged &lt;a href="https://github.com/oven-sh/bun/pull/30412" rel="noopener noreferrer"&gt;PR #30412&lt;/a&gt;. One pull request: 1,009,257 lines added, 2,188 files changed. The entire runtime, ported from Zig to Rust in 11 days by roughly 50 parallel Claude Code workflows, at about $165,000 in API costs. Jarred Sumner figures a human team would have needed a year.&lt;/p&gt;

&lt;p&gt;Reactions split exactly how you'd guess. Andrew Kelley, Zig's creator, &lt;a href="https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.html" rel="noopener noreferrer"&gt;called it&lt;/a&gt; "a million lines of unreviewed slop". Rust people called it the future. Three months of arguing, and as far as I can tell, nobody on either side pulled the actual numbers.&lt;/p&gt;

&lt;p&gt;So I did. Every issue filed against oven-sh/bun from six months before the merge until last week: 2,981 of them, bucketed into weeks anchored on the merge timestamp, each one classified by which build the reporter was running. The dataset and scripts live in &lt;a href="https://github.com/asmyshlyaev177/bun-rust-rewrite-analysis" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;, and there's a much drier &lt;a href="https://asmyshlyaev177.github.io/bun-rust-rewrite-analysis/" rel="noopener noreferrer"&gt;interactive report&lt;/a&gt; with every chart, if you want to check my work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tracker shows
&lt;/h2&gt;

&lt;p&gt;The pitch for the rewrite was memory safety. Bun's tracker was drowning in use-after-free and double-free crashes, and in safe Rust those are compile errors. Sumner put the whole thesis in &lt;a href="https://bun.com/blog/bun-in-rust" rel="noopener noreferrer"&gt;one line&lt;/a&gt;: "Compiler errors are a better feedback loop than a style guide."&lt;/p&gt;

&lt;p&gt;Canary is where you test that claim, because canary is the only channel where the Rust build has ever shipped. Same channel, same self-selected users who run prereleases, Zig builds before May 14, Rust builds after. The cut is unusually clean: I checked every version string in six months of pre-merge issues, and not one names a 1.4.x Bun build. A grep finds nine matches, and all nine turn out to be library versions like elysia 1.4.x. The 1.4 line simply did not exist for users until the morning of the merge.&lt;/p&gt;

&lt;p&gt;Bug reports on canary went from 5.9 a week to 20.7. I don't read that as "Rust tripled the bugs", because two other things changed at the same moment: canary became the only way to get the new engine, so its population grew, and the PR literally asked people to file issues. Report volume is exposure times defect rate times willingness to report, and you only ever observe the product.&lt;/p&gt;

&lt;p&gt;The ratios don't care about population size though, and they're stubborn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before the rewrite, 43.5% of canary bug reports eventually got closed as fixed. After: 41.9%.&lt;/li&gt;
&lt;li&gt;Canary bugs fixed per week: 2.6 before, 8.7 after. Proportional to the report flood, not better.&lt;/li&gt;
&lt;li&gt;Median time to fix across all bug reports: 2.7 days before, 3.2 after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same machine, three times the input.&lt;/p&gt;

&lt;p&gt;Before you reach for the obvious objection: I stress-tested the classifier. A stricter rule that only accepts a version string in real Bun context (a build hash, or next to the template's "What version of Bun is running?") moves the counts by under 1% and leaves both fix rates identical to one decimal. If anything it makes the jump slightly bigger, 3.6x instead of 3.5x. The repo has the &lt;a href="https://github.com/asmyshlyaev177/bun-rust-rewrite-analysis/blob/master/scripts/10-classifier-robustness.py" rel="noopener noreferrer"&gt;robustness script&lt;/a&gt; and an &lt;a href="https://github.com/asmyshlyaev177/bun-rust-rewrite-analysis/blob/master/scripts/09-verify.py" rel="noopener noreferrer"&gt;independent verification&lt;/a&gt; that re-derives all 31 published figures from the frozen data.&lt;/p&gt;

&lt;p&gt;Meanwhile the process around the code seized up. Last November, only 1–3% of new Bun issues went unlabeled; triage was already slipping through spring, and since the merge roughly half of new issues never get a label at all. Bun shipped 12 stable releases in the six months before May 14. Since: zero, in 89 days. The longest gap between any two Bun releases across 2024–2026 had been 27 days. Every stable user today still runs the Zig binary the project supposedly left behind.&lt;/p&gt;

&lt;p&gt;Whether that's caution or paralysis I can't tell from outside. Probably some of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  This was Rust's best-case scenario
&lt;/h2&gt;

&lt;p&gt;The flat fix rate gets interesting once you notice the conditions it happened under.&lt;/p&gt;

&lt;p&gt;A port is the friendliest possible job for a rigid language. The spec was frozen: behave exactly like old Bun. A million-assertion TypeScript test suite sat there as an oracle, checking every step. For 11 days, the requirements did not move. Even people who can't stand Rust concede it this territory. The much-shared &lt;a href="https://loglog.games/blog/leaving-rust-gamedev/" rel="noopener noreferrer"&gt;"Leaving Rust gamedev after 3 years"&lt;/a&gt; essay, written by someone with over 100k lines of Rust behind them, says it flat out: "Rust fits very nicely in the low level algorithmic areas where one knows exactly what the problem is and just needs to solve it."&lt;/p&gt;

&lt;p&gt;So Rust got its dream conditions, plus an AI to absorb every borrow checker complaint, and delivered a fix rate identical to the Zig era and a release pipeline in a coma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your project is not a port
&lt;/h2&gt;

&lt;p&gt;This is where I stop reporting and start arguing.&lt;/p&gt;

&lt;p&gt;I've spent over a decade building software, mostly products, and almost none of it resembled that port. There was no oracle. The spec was a Slack thread and a mockup. Requirements changed the first time a user touched the feature, then changed again. Half the job was finding out what we were supposed to build, usually by building the wrong thing first.&lt;/p&gt;

&lt;p&gt;That's the regime where language rigidity hurts, and the people saying so are not beginners who bounced off the borrow checker in week one. The gamedev essay again: "the borrow checker forces a refactor at the most inconvenient times." &lt;a href="https://news.ycombinator.com/item?id=40172952" rel="noopener noreferrer"&gt;John Nagle&lt;/a&gt;, of Nagle's algorithm, some 45k lines of Rust into a metaverse client, seconds it: "It's quite common for some change to require extensive plumbing work", noting the C#/Unity teams on the same problem move faster than he does. A &lt;a href="https://deadmoney.gg/news/articles/migrating-away-from-rust" rel="noopener noreferrer"&gt;25-year veteran of Epic and Valve&lt;/a&gt; spent 14 months building his game on Rust, redid it in Unity in six weeks, and shipped. &lt;a href="https://graydon2.dreamwidth.org/307291.html" rel="noopener noreferrer"&gt;Graydon Hoare&lt;/a&gt;, the man who created Rust, wrote in 2023 that shipped Rust is far from the language he wanted, that he'd argued against explicit lifetimes, first-class borrows, and the trait system. And in &lt;a href="https://blog.rust-lang.org/2024/02/19/2023-Rust-Annual-Survey-2023-results/" rel="noopener noreferrer"&gt;Rust's own annual survey&lt;/a&gt;, the top worry among its users about the language's future is "Rust becoming too complex", at 43%.&lt;/p&gt;

&lt;p&gt;Rob Pike caught endless grief for his reason Go is simple: "our programmers are Googlers, they're not researchers... They're not capable of understanding a brilliant language but we want to use them to build good software." Every time I reread that quote it sounds less like an insult and more like the only statement in language design anchored in how teams actually work. Your teammates are busy and distracted. So are you, so am I. A language that assumes otherwise sends the bill exactly when a deadline and a requirement change arrive together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steelman break
&lt;/h2&gt;

&lt;p&gt;The counterevidence is real and I won't hide it. Google reports Rust changes in Android &lt;a href="https://blog.google/security/rust-in-android-move-fast-fix-things/" rel="noopener noreferrer"&gt;roll back 4x less often than C++&lt;/a&gt; and spend 25% less time in review; across 1,000+ of their developers they found no productivity penalty. Dropbox rewrote its sync engine, the most edge-case-infested thing they own, &lt;a href="https://dropbox.tech/infrastructure/rewriting-the-heart-of-our-sync-engine" rel="noopener noreferrer"&gt;in Rust&lt;/a&gt; and called the bet a force multiplier.&lt;/p&gt;

&lt;p&gt;Look at what those wins share, though. Android platform code and a file-sync engine are among the most precisely specified software in existence. The invariants are known before anyone writes a line. Both camps in this fight agree on the boundary without noticing they agree: when you know exactly what you're building, rigidity is scaffolding. When you don't, you're paying interest on it every sprint.&lt;/p&gt;

&lt;p&gt;The question was never "is Rust good". It's what share of your roadmap is spec-frozen execution versus "we'll understand this feature after we ship it twice". Be honest about the split. On most teams I've seen, it's one part execution to five parts figuring things out.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But AI pays the complexity tax now"
&lt;/h2&gt;

&lt;p&gt;The newest argument for maximal rigidity: language difficulty stopped mattering, because agents write the code and the compiler verifies them. There's real research behind it. An LLM looping on rustc's error messages &lt;a href="https://arxiv.org/abs/2308.05177" rel="noopener noreferrer"&gt;fixes about 74% of real-world compile errors&lt;/a&gt; on its own. Constrain the model's decoding with the type system and &lt;a href="https://arxiv.org/abs/2504.09246" rel="noopener noreferrer"&gt;compile errors drop by more than half&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bun is the largest field test of that thesis to date, and the result is lopsided: the machine half worked and the human half didn't. Claude satisfied the borrow checker a million lines' worth. Then triage collapsed, review became the choke point, and nothing shipped for three months. Kelley's jab lands here whether you like Zig or not: "It's not sufficient to catch bugs in Zig code but it is sufficient to catch bugs in [a] million lines of unreviewed slop?"&lt;/p&gt;

&lt;p&gt;Worth adding: Armin Ronacher, who ran Rust in production at Sentry for years, experiments constantly with coding agents and &lt;a href="https://lucumr.pocoo.org/2025/6/12/agentic-coding/" rel="noopener noreferrer"&gt;recommends Go for them&lt;/a&gt;, not Rust. In &lt;a href="https://lucumr.pocoo.org/2025/8/4/shitty-types/" rel="noopener noreferrer"&gt;his tests&lt;/a&gt;, putting strict type checking inside the agent loop made results worse.&lt;/p&gt;

&lt;p&gt;A compiler can prove what the code does. It can't prove that's what the code was for. That second question is the entire job, it stays with humans, and no amount of language rigidity shrinks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land
&lt;/h2&gt;

&lt;p&gt;Disclosures first, because this story is soaked in them: Anthropic acquired Bun in December 2025, the rewrite ran on a prerelease Claude model, Sumner's speed and cost numbers are self-reported, and I did this analysis with Claude's help too.&lt;/p&gt;

&lt;p&gt;But the data as of today says: in the single most favorable setup a rigid language will ever get, frozen spec, perfect oracle, tireless AI labor, it produced no measurable improvement in how bugs get handled, while the humans around it lost the ability to ship. Your project runs with none of those advantages. Your spec moves weekly. Your oracle is whatever the PM remembers agreeing to. Your test suite is not a million assertions.&lt;/p&gt;

&lt;p&gt;Pick the strict, complex language when the problem is genuinely pinned down. It will hold the line for you there. For everything else, and I think that's most software, complexity is a tax collected on every change, and the compiler's certainty is always about yesterday's requirements.&lt;/p&gt;

&lt;p&gt;The numbers, the scripts, and the full study are &lt;a href="https://github.com/asmyshlyaev177/bun-rust-rewrite-analysis" rel="noopener noreferrer"&gt;in the repo&lt;/a&gt;. If you think I've read the data wrong, that's what the comments are for.&lt;/p&gt;

</description>
      <category>bunjs</category>
      <category>rust</category>
      <category>ai</category>
      <category>refactoring</category>
    </item>
    <item>
      <title>Your SPA probably leaks memory. Your Playwright suite can catch it</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:39:35 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/your-spa-probably-leaks-memory-your-playwright-suite-can-catch-it-93l</link>
      <guid>https://dev.to/asmyshlyaev177/your-spa-probably-leaks-memory-your-playwright-suite-can-catch-it-93l</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[soak] drawer — 80 iterations
    base     heap   2.83MB  nodes    675  listeners   163  docs 1
    @   40   heap   3.50MB  nodes   5675  listeners   163  docs 1
    @   80   heap   4.18MB  nodes  10675  listeners   163  docs 1
    delta  heap +1.36MB  nodes +10000  listeners +0  docs +0
    per-iteration  heap 17.35KB  nodes 125.00
    nodes  ++++++++  +10000 total, biggest stretch 13% of it (spread out, so accumulating)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a memory leak failing a CI build. Playwright doesn't do this out of the box, and memory is a blind spot for e2e suites in general: here a store subscriber outlives the drawer it closes over, every open/close cycle keeps 125 DOM nodes alive, and the tests stay green the whole time. In development you never notice this stuff, you reload the page every few minutes anyway. The person who notices is the one with your app in a pinned tab since morning.&lt;/p&gt;

&lt;p&gt;Recently I read Den Odell's &lt;a href="https://denodell.com/blog/your-spa-is-leaking-memory-soak-test-it" rel="noopener noreferrer"&gt;Your SPA is leaking memory — soak test it&lt;/a&gt; and the framing stuck with me: stop hunting leaks with a profiler after someone complains, catch them in CI like any other regression. I liked it enough to turn it into a package. &lt;strong&gt;playwright-soak&lt;/strong&gt; runs inside the Playwright suite you already have. You give it a flow, say open a drawer and close it, and it repeats that flow a few hundred times with forced garbage collection in between, and fails the test if memory keeps piling up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; playwright-soak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug that always ships
&lt;/h2&gt;

&lt;p&gt;The leaks worth chasing are the small ones that repeat per interaction. A one-time allocation costs you a few megabytes and nobody cares. A subscriber that's recreated on every drawer open, or a listener added without a matching removal, grows without bound. And it survives code review easily, because the bug is a missing line, not a wrong one.&lt;/p&gt;

&lt;p&gt;Nothing in a typical pipeline catches it either. Unit and e2e tests run a flow once, and Lighthouse only ever sees a fresh page load. Profiling in DevTools would show it, but someone has to remember to do that, and nobody profiles the drawer they just refactored. So the leak ships and comes back weeks later as a vague complaint that the app "gets slow after a while". In the Performance Monitor the complaint looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxlsmnqsu1ik7dh3ea96.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxlsmnqsu1ik7dh3ea96.png" alt="Chrome DevTools Performance Monitor during a long SPA session: JS heap at 822 MB, DOM nodes near 100,000, event listeners near 19,000, all still climbing" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is boring on purpose: make leak detection a test, so it runs on every PR whether anyone remembers or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious test doesn't work
&lt;/h2&gt;

&lt;p&gt;If you've tried to write that test before, you probably measured the heap before and after the flow and asserted the difference is small. I started there too. It fails in both directions.&lt;/p&gt;

&lt;p&gt;It flags healthy apps, because plenty of legitimate allocation happens once and stays: a cache filling up on first use, a global listener registered on first interaction. And it trips over the browser having a life of its own. In one clean run I watched Chromium register an extra listener around iteration 50 and drop it again before iteration 75. Land that blip on your final reading and the build fails over a listener the browser itself was about to remove. Tests like that get deleted within a month, which I suspect is why most teams don't guard against leaks at all.&lt;/p&gt;

&lt;p&gt;Two numbers can't tell "grew once" apart from "grows every time", so playwright-soak takes readings all the way through and judges the shape instead. A leak repeats, which means its growth spreads evenly across the run. Caches and browser blips pile all of theirs into one moment. This gets counterintuitive: a flow that ends &lt;code&gt;+2&lt;/code&gt; listeners can pass while one that ends &lt;code&gt;+4&lt;/code&gt; fails, because the pattern matters and the size doesn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listeners ·+·····+·  +2 total, growth piled in one spot (a one-off step, passes)
listeners ·+·+·+·+·  +4 total, growth spread across the run (accumulating, fails)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what makes it stable enough to gate a PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a run looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;attachCDP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectNoLeak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatSoak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;soak&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playwright-soak&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opening and closing the drawer does not leak&lt;/span&gt;&lt;span class="dl"&gt;"&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;page&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attachCDP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;soak&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;flow&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="o"&gt;=&amp;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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&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;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="nf"&gt;formatSoak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drawer&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;expectNoLeak&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;soak&lt;/code&gt; warms up first (so one-time allocation lands before the baseline), then runs the flow 200 times with readings every 25 iterations. Each reading covers four counters, and each catches a different kind of leak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"nodes"     - detached DOM kept alive by a closure, a cache or a stale ref
"listeners" - handlers added without a matching removal
"documents" - iframes that mount per interaction and never unmount
"heap"      - everything else: closures, growing arrays, unbounded caches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last row matters more than it looks. A stale callback that closes over a component tree, or an in-memory log nobody bounds, moves zero nodes and zero listeners. Only the heap gives it away, so &lt;code&gt;expectNoLeak&lt;/code&gt; also checks the heap curve: a real leak keeps climbing at the same rate all run, while warm-up allocation flattens out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing flows aren't flaky, they're billed
&lt;/h2&gt;

&lt;p&gt;Chromium keeps roughly one DOM node per text-editing gesture. That's the browser's own bookkeeping, not your framework; a bare &lt;code&gt;document.createElement('input')&lt;/code&gt; bound to nothing shows the same growth. Type a five-letter word 200 times and that's a thousand nodes of pure browser overhead sitting on top of whatever your app does. It looks exactly like a leak. No wonder typing soak tests have a reputation for flakiness.&lt;/p&gt;

&lt;p&gt;So the package measures the browser's own rate against a throwaway control input and budgets for it:&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;const&lt;/span&gt; &lt;span class="nx"&gt;typingCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;calibrateTypingCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;soak&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flow&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="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#name&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="nf"&gt;expectNoLeak&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;typingCost&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that budget in place, typing flows are as stable as click flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smoke alarm, not debugger
&lt;/h2&gt;

&lt;p&gt;playwright-soak tells you &lt;em&gt;that&lt;/em&gt; a flow accumulates memory, and which flow. It does not tell you which object leaked or what's retaining it. That job belongs to &lt;a href="https://github.com/facebook/memlab" rel="noopener noreferrer"&gt;memlab&lt;/a&gt;, which diffs heap snapshots and gives you actual retainer paths, but is &lt;a href="https://github.com/facebook/memlab/issues/138" rel="noopener noreferrer"&gt;Puppeteer-only&lt;/a&gt; and heavy enough that nobody runs it on every PR.&lt;/p&gt;

&lt;p&gt;In practice you want both: soak tests on every PR, memlab when one of them goes red and you need to know what's actually holding the memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you have a Playwright suite and an app people keep open all day, write one soak test for your most-used flow and see what comes back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; playwright-soak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime dependencies. Chromium only, since the readings come from the Chrome DevTools Protocol. The repo ships &lt;a href="https://github.com/asmyshlyaev177/playwright-soak/tree/master/examples/tanstack-spa" rel="noopener noreferrer"&gt;a small example app&lt;/a&gt; where every kind of leak has a matched broken/fixed pair of tests, and a set of &lt;a href="https://tanstack.com/intent" rel="noopener noreferrer"&gt;TanStack Intent&lt;/a&gt; skills so a coding agent can write these tests for you. The source is on &lt;a href="https://github.com/asmyshlyaev177/playwright-soak" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT licensed. If it catches something in your app, or misses something it shouldn't have, open an issue, I want to know. And a star on the repo is the easiest way to help the next person find it.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>memoryleaks</category>
      <category>soak</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built llm-queue: one local model, one queue</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:44:15 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/i-built-llm-queue-one-local-model-one-queue-15l4</link>
      <guid>https://dev.to/asmyshlyaev177/i-built-llm-queue-one-local-model-one-queue-15l4</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE  two processes, two private queues, one small GPU

  jobbot       ─┐
                ├──▶  Ollama
  slop filter  ─┘

  both hit the model at once → reload thrash, ~4x slower


AFTER  one shared queue over HTTP, in front of one model

  jobbot       ─┐
                ├─ HTTP ─▶  llm-queue ─ fetch ─▶  Ollama
  slop filter  ─┘

  one request at a time → the model loads once and stays put
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have two side projects that both lean on a local LLM, and for a while they kept making each other slow. Fixing that turned into a small package called &lt;strong&gt;llm-queue&lt;/strong&gt;: one process that owns the model and runs every request, from every program, through a single serialized priority queue behind an OpenAI-compatible HTTP API.&lt;/p&gt;

&lt;p&gt;The two projects are jobbot, a bot that scrapes job boards and classifies each posting with a local model (is this actually a job? is it remote? does it want a language I don't speak?), and a browser extension that hides LinkedIn feed slop the same way. One's a Node cron job, the other's an MV3 service worker. Both reach for the same Ollama model on my laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: one model, one small GPU
&lt;/h2&gt;

&lt;p&gt;Two facts make concurrency a bad bet here. A local model answers one request at a time, so parallel callers don't actually run in parallel. And my GPU has 6 GB, which barely fits a single model, so there's no room to load a second one beside it.&lt;/p&gt;

&lt;p&gt;So the obvious plan, let both projects run and hit Ollama whenever they want, fell apart fast. When two processes hit it at once with different models, or even the same model at a different context size, Ollama unloads one and loads the other on every call. That reload is gigabytes off disk, and it turns out to be most of your wall-clock time. I benchmarked it to be sure: throughput dropped to about a quarter of what a single client got. Concurrency made things slower. About four times slower.&lt;/p&gt;

&lt;p&gt;Pinning both models in memory doesn't save you either. They don't fit, so they spill onto the CPU and fight over cores, and now both are slow. On this hardware, nothing beats one request at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one shared queue
&lt;/h2&gt;

&lt;p&gt;The answer is boring. Don't run requests in parallel. Put them in one line and serve them one at a time. The line just has to be global, shared across every process, and the one thing all my programs already speak is HTTP.&lt;/p&gt;

&lt;p&gt;So llm-queue is a single process that owns the Ollama endpoint and the model. Everything else talks to it over HTTP. One queue, one model, one request in flight, ever, so the model loads once and stays put.&lt;/p&gt;

&lt;p&gt;There's one wrinkle: not every request is equal. jobbot fires off dozens of background classifications while it scrapes, but when I click a job to draft an application, I'm waiting on that one answer right now. So it's a priority queue. A request marked priority jumps the waiting backlog, though it never interrupts whatever's already running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it speak OpenAI
&lt;/h2&gt;

&lt;p&gt;I didn't want to ship a client library. Everything already speaks the OpenAI API, so I made the service speak it too. Point any OpenAI client at &lt;code&gt;http://localhost:11500/v1&lt;/code&gt; and you get the queue for free, no SDK swap:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:11500/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unused&lt;/span&gt;&lt;span class="dl"&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;r&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granite4.1:8b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two knobs the queue needs don't exist in the OpenAI shape, so they ride along as extra body fields. &lt;code&gt;priority&lt;/code&gt; is the line-jump from above. &lt;code&gt;numCtx&lt;/code&gt; is the context window a client wants; the service runs at the largest size any client asks for and never shrinks back, so the model doesn't reload to a smaller window mid-day. Stock OpenAI servers ignore fields they don't recognize, so the exact same request still works against the real API. It just drops the two extras.&lt;/p&gt;

&lt;p&gt;The service also hands back the model's raw string and nothing else. No JSON parsing, no schema validation. My two consumers parse the output differently, and every time I pictured "helping" them inside the queue, I was really just guessing wrong on their behalf. The queue does the one job only it can do: serialize access, with a per-attempt timeout and one retry. The rest is the caller's problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two projects, one queue, no shared code
&lt;/h2&gt;

&lt;p&gt;The part I like most is that neither consumer depends on the package. jobbot doesn't &lt;code&gt;npm install llm-queue&lt;/code&gt;. Neither does the extension. They just POST to the running service:&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;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:11500/v1/chat/completions&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;messages&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;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageText&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json_object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// ask for JSON&lt;/span&gt;
    &lt;span class="na"&gt;numCtx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                             &lt;span class="c1"&gt;// llm-queue extension&lt;/span&gt;
  &lt;span class="p"&gt;}),&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;choices&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration. The extension, running in a completely separate browser, sends its own classifications to the same service. (Browsers block a content script from calling localhost, so that fetch lives in the service worker, and the service sends permissive CORS headers so it goes through.) The two projects have never heard of each other. They don't share a queue in code. They share one in &lt;em&gt;production&lt;/em&gt;, because there is exactly one and it's reachable over HTTP. On a 6 GB GPU that's the difference between a model that loads once and one that thrashes all day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it
&lt;/h2&gt;

&lt;p&gt;It's small on purpose: about 700 lines across the queue, the HTTP server, the fetch transport, and the CLI. TypeScript, ESM, Node 20+, almost no dependencies, talking to Ollama over plain &lt;code&gt;fetch&lt;/code&gt;. tsup builds it, vitest covers the queue and the server, and release-please turns my conventional commits into the version bump, changelog, and npm publish on merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you run a local model and more than one thing wants to use it, this is the missing piece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; llm-queue
&lt;span class="nv"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;granite4.1:8b llm-queue serve   &lt;span class="c"&gt;# → http://127.0.0.1:11500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point anything OpenAI-shaped at &lt;code&gt;http://localhost:11500/v1&lt;/code&gt;, or just &lt;code&gt;fetch&lt;/code&gt; it directly. It's on &lt;a href="https://www.npmjs.com/package/llm-queue" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, the source is on &lt;a href="https://github.com/asmyshlyaev177/llm-queue" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT licensed. If it saves your local model from thrashing the way it saved mine, a star on the repo is the easiest way to help the next person find it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Testing Next.js SSR with Playwright: record real responses instead of writing mocks</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 18 Jun 2026 11:56:04 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/testing-nextjs-ssr-with-playwright-record-real-responses-instead-of-writing-mocks-fk5</link>
      <guid>https://dev.to/asmyshlyaev177/testing-nextjs-ssr-with-playwright-record-real-responses-instead-of-writing-mocks-fk5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqac7h0fbnrdz7hpyyrph.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqac7h0fbnrdz7hpyyrph.gif" alt="Recording real API responses, then replaying them on CI with the backend turned off" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you write Playwright tests for a Next.js app, you've probably hit this wall. You reach for &lt;code&gt;page.route()&lt;/code&gt; to stub an API call, the test still fails, and after twenty minutes you realize the request you're trying to mock never went through the browser at all. Your server component fetched it during SSR. Playwright never saw it, because Playwright drives a browser, and that fetch happened in Node.&lt;/p&gt;

&lt;p&gt;This is the gap nobody upstream has closed cleanly. &lt;code&gt;routeFromHAR&lt;/code&gt; is browser only. The Next.js team has an &lt;a href="https://github.com/vercel/next.js/discussions/67136" rel="noopener noreferrer"&gt;open discussion about mocking server actions in e2e tests&lt;/a&gt; and &lt;a href="https://github.com/vercel/next.js/discussions/76436" rel="noopener noreferrer"&gt;another about middleware&lt;/a&gt;, both still unresolved. There's a &lt;a href="https://github.com/microsoft/playwright/issues/4928" rel="noopener noreferrer"&gt;four-year-old Playwright feature request&lt;/a&gt; asking for exactly the thing you want: record network requests, save them as ready-to-use mocks. Still open.&lt;/p&gt;

&lt;p&gt;So people work around it. The current options are all variations on the same idea: hand-write the mocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hand-written mock treadmill
&lt;/h2&gt;

&lt;p&gt;MSW is the popular answer. It intercepts in both the browser and Node, and since Next.js 15 it can finally see server-side calls. But you still author every handler by hand, keep its shape in sync with the real API, and update it when the backend changes. MSW also keeps its state globally, so running tests in parallel gets fiddly fast. Mocky Balboa, the experimental Next.js test proxy, &lt;code&gt;request-mocking-protocol&lt;/code&gt;, they all land in the same place. You're maintaining a second, fake copy of your API forever.&lt;/p&gt;

&lt;p&gt;I did this for a while and got tired of it. The mocks drift. The real API adds a field, your mock doesn't have it, the test passes anyway, and you ship a bug that only shows up against production data. The whole point of an e2e test is to run against something that behaves like the real thing, and a mock I typed out by hand at 2am is the opposite of that.&lt;/p&gt;

&lt;p&gt;What I actually wanted was the VCR pattern. Ruby had this nailed years ago: hit the real API once, save the interaction to a file, replay it from disk on every run after that. The JavaScript version of this was Polly.js from Netflix, but it's effectively unmaintained now (last meaningful push was a while back), and it never had a clean story for SSR plus Playwright anyway. The throne was empty. So I built &lt;strong&gt;test-proxy-recorder&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: record once, replay forever
&lt;/h2&gt;

&lt;p&gt;The model is simple. There are two modes.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;record&lt;/strong&gt; mode, your app talks to the real backend through a proxy. The proxy forwards everything and writes each request and response to disk as it goes.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;replay&lt;/strong&gt; mode, the proxy serves those saved responses straight from disk. No backend, no network, same bytes every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                        Record mode                          Replay mode

  Browser/App ──&amp;gt; Proxy ──&amp;gt; Real API        Browser/App ──&amp;gt; Proxy ──&amp;gt; Disk
                    │                                         │
                    └──&amp;gt; saves to disk                        └──&amp;gt; serves saved responses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick for SSR is that it's a proxy, not a browser hook. Your Next.js server fetches go through it just like browser fetches do, so it captures both. Browser-side requests get recorded separately as HAR files (the format Playwright already understands), and server-side requests land in &lt;code&gt;.mock.json&lt;/code&gt; files. You don't think about which is which day to day, you just record and commit.&lt;/p&gt;

&lt;p&gt;The thirty-second version of the pitch: record your tests against the real API, delete the backend, run the tests again, watch them pass. There's a short video of exactly that on the site, &lt;a href="https://test-proxy-recorder.dev" rel="noopener noreferrer"&gt;test-proxy-recorder.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into Next.js
&lt;/h2&gt;

&lt;p&gt;The only genuinely Next-specific part is letting the proxy know which test a server-side fetch belongs to. The browser request carries a header that ties it to the running test; you forward that same header on your SSR fetches. A tiny middleware does it for you:&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="c1"&gt;// proxy.ts  (Next.js 16 middleware convention)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setNextProxyHeaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-proxy-recorder/nextjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setNextProxyHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no-op in production&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&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;/((?!_next/static|_next/image|favicon.ico).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a no-op in production, so it's safe to leave in. On Next.js 15 and earlier the file is &lt;code&gt;middleware.ts&lt;/code&gt; with the function named &lt;code&gt;middleware&lt;/code&gt;, otherwise identical.&lt;/p&gt;

&lt;p&gt;Then point your API base URL env var at the proxy in dev and test only, and write a normal Playwright test:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;playwrightProxy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-proxy-recorder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Flip to 'record' to hit the real API and refresh recordings.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&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;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&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;page&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;testInfo&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;await&lt;/span&gt; &lt;span class="nx"&gt;playwrightProxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/localhost:8100/&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage loads&lt;/span&gt;&lt;span class="dl"&gt;'&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;page&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;Record once with &lt;code&gt;MODE = 'record'&lt;/code&gt;, switch it back to &lt;code&gt;'replay'&lt;/code&gt;, commit the &lt;code&gt;e2e/recordings/&lt;/code&gt; folder, and CI never touches the network again. If the setup above looks like a few too many moving parts, &lt;code&gt;npx test-proxy-recorder init&lt;/code&gt; scaffolds the config, the fixture, the teardown, and the package scripts in one shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that were actually hard
&lt;/h2&gt;

&lt;p&gt;The recording loop is the easy 80%. The interesting problems are the reasons people abandon record-replay tools, so most of my time went there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets.&lt;/strong&gt; A record-replay tool quietly tells you to commit real API responses to git, which means it's one missed review away from committing a live token. So redaction is on by default now. &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;Cookie&lt;/code&gt;, and &lt;code&gt;Set-Cookie&lt;/code&gt; get stripped before anything hits disk, and you can add your own header names or body regexes for API keys. It's safe because replay matching ignores those headers anyway, so scrubbing them never breaks playback. For login itself, the recommended pattern keeps the whole auth flow out of recordings: run it in a Playwright setup project with the proxy in transparent mode and persist &lt;code&gt;storageState&lt;/code&gt; to a gitignored file. There's a working example against a real AWS Cognito pool in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebSockets.&lt;/strong&gt; This one surprised me. Recording a socket is fine, but how do you replay it? Real time? Then a test that watches a price ticker takes as long as the recording did. So the default replays server messages as a burst on connect, which is fast and deterministic, since tests usually assert on the state that arrived, not the timing. If you do care about timing, &lt;code&gt;--ws-timing original&lt;/code&gt; re-paces the messages from their recorded timestamps. I validated it against Binance's live BTC-USD feed: record the real stream once, replay deterministic prices on CI with no exchange account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-deterministic requests.&lt;/strong&gt; Timestamps, UUIDs, CSRF tokens in the URL. This is the classic record-replay killer, and I'll be honest, it's the area still getting work. Matching is currently method plus path plus a hash of the query string, and configurable matchers for volatile params are the next thing on the list. If your URLs are stable, you won't notice. If they're full of cache-busters, that's the rough edge today.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is the wrong tool
&lt;/h2&gt;

&lt;p&gt;It's not a unit-test mock. If you want to assert "the UI shows an error when the API returns 500," hand-writing that one response with &lt;code&gt;page.route()&lt;/code&gt; is faster and clearer. test-proxy-recorder earns its keep when you have real, complicated API responses you don't want to reproduce by hand, especially across SSR and browser and sockets in the same test run. It also assumes you can reach the real backend at least once to record. Fully air-gapped from day one, it can't help you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; test-proxy-recorder
npx test-proxy-recorder init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The site is &lt;a href="https://test-proxy-recorder.dev" rel="noopener noreferrer"&gt;test-proxy-recorder.dev&lt;/a&gt;, and the repo is &lt;a href="https://github.com/asmyshlyaev177/test-proxy-recorder" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; with runnable examples for Next.js 16, a Chrome extension hitting X's API, the crypto ticker, and a real-auth Cognito app. MIT licensed.&lt;/p&gt;

&lt;p&gt;If you've been fighting Playwright over SSR mocking, give it a run against your own API and tell me where it breaks. Open an issue, send a PR, or come ask in the &lt;a href="https://discord.gg/w7rgYbY5zz" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; if you get stuck wiring it up. The honest edges above are exactly the kind of feedback that moves the roadmap.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>nextjs</category>
      <category>apimocking</category>
    </item>
    <item>
      <title>Attempt to stop npm postinstall scripts from stealing your secrets</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 14 May 2026 13:18:36 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/attempt-to-stop-npm-postinstall-scripts-from-stealing-your-secrets-3adp</link>
      <guid>https://dev.to/asmyshlyaev177/attempt-to-stop-npm-postinstall-scripts-from-stealing-your-secrets-3adp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy7fe7eb3tijq9f05k0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy7fe7eb3tijq9f05k0h.png" alt="ringfence"&gt;&lt;/a&gt;&lt;br&gt;
Every time you run &lt;code&gt;npm install&lt;/code&gt;, you're rolling dice. A postinstall script from some dependency three layers deep gets unrestricted access to your filesystem. &lt;code&gt;.env&lt;/code&gt; files, SSH keys, cloud credentials, your whole &lt;code&gt;$HOME&lt;/code&gt; directory. It can read it all, and it can ship it anywhere because the network is wide open.&lt;/p&gt;

&lt;p&gt;In May 2026, a supply chain worm compromised 84 npm packages that had valid SLSA Build Level 3 provenance. Postinstall scripts siphoned CI/CD secrets, SSH keys, cloud credentials, crypto wallets. It spread quietly because everything looked legitimate. The attack got nicknamed "Shai-Hulud" after the Dune sandworm that swallows everything in its path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ringfence&lt;/strong&gt; blocks this class of attack at the OS level. Zero config. It wraps your package manager commands in a sandbox so a compromised dependency can't see anything worth stealing.&lt;/p&gt;

&lt;p&gt;One thing to be clear about: ringfence doesn't stop you from installing malicious code. If a package is bad, it still gets installed. What ringfence does is make sure that the postinstall script running on your machine can't read your secrets, can't touch your SSH keys, can't sniff your &lt;code&gt;.env&lt;/code&gt;. It sits between the compromised package and &lt;em&gt;your&lt;/em&gt; filesystem, not between you and the registry.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Ringfence is basically a shim. It drops a handful of shell scripts at &lt;code&gt;~/.ringfence/bin/&lt;/code&gt; and shoves that directory onto the front of your &lt;code&gt;PATH&lt;/code&gt;. When you type &lt;code&gt;npm install&lt;/code&gt;, the shim catches it and asks: does this command trigger lifecycle scripts? If yes, it goes through the sandbox. If it's &lt;code&gt;npm run build&lt;/code&gt; or &lt;code&gt;npm test&lt;/code&gt; or &lt;code&gt;npx tsc&lt;/code&gt;, it passes straight through to the real binary. You pay about 80ms of Node startup overhead either way.&lt;/p&gt;

&lt;p&gt;On Linux it leans on bubblewrap. The sandbox replaces &lt;code&gt;$HOME&lt;/code&gt; with an empty tmpfs. After that it selectively brings back only what the package manager needs: &lt;code&gt;.npm&lt;/code&gt;, pnpm's store directories, yarn's cache, and registry auth files like &lt;code&gt;.npmrc&lt;/code&gt; (those are mounted read only). &lt;code&gt;~/.ssh&lt;/code&gt;, &lt;code&gt;~/.aws&lt;/code&gt;, &lt;code&gt;~/.gnupg&lt;/code&gt; stay invisible because they were never added to the blank tmpfs in the first place.&lt;/p&gt;

&lt;p&gt;Next, every secret looking file in your project directory gets masked with &lt;code&gt;/dev/null&lt;/code&gt;. &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.*&lt;/code&gt;, &lt;code&gt;*.pem&lt;/code&gt;, &lt;code&gt;id_rsa*&lt;/code&gt;, &lt;code&gt;credentials.json&lt;/code&gt;, service account JSON files, and a bunch of other patterns. The files are there, they're just zero bytes.&lt;/p&gt;

&lt;p&gt;Environment variables that smell like secrets get unset: anything with &lt;code&gt;TOKEN&lt;/code&gt;, &lt;code&gt;SECRET&lt;/code&gt;, &lt;code&gt;API_KEY&lt;/code&gt;, or &lt;code&gt;PASSWORD&lt;/code&gt; in the name, plus prefixes like &lt;code&gt;AWS_&lt;/code&gt;, &lt;code&gt;GITHUB_&lt;/code&gt;, &lt;code&gt;NPM_&lt;/code&gt;, &lt;code&gt;DATABASE_&lt;/code&gt;. The network stays open. Registries, git deps, tarball URLs all work fine. The defense isn't blocking exfiltration. It's making sure there's nothing to exfiltrate.&lt;/p&gt;

&lt;p&gt;On macOS there's no bubblewrap (no Linux namespaces), so ringfence falls back to Docker. The project directory gets copied to a temp location with all secret files filtered out, mounted into an ephemeral &lt;code&gt;node:lts&lt;/code&gt; container with &lt;code&gt;HOME=/work&lt;/code&gt;, and after the install finishes only the non-secret files sync back. It's slower than the Linux path, but the security model is the same.&lt;/p&gt;

&lt;p&gt;The test suite includes a leak probe: a synthetic malicious script that tries reading &lt;code&gt;.env&lt;/code&gt; files, SSH keys, &lt;code&gt;~/.aws/&lt;/code&gt; credentials, &lt;code&gt;~/.gnupg/&lt;/code&gt; keys, and secret shaped env vars. Under ringfence, every single probe fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; ringfence &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx ringfence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two commands. It detects your OS, installs bubblewrap if you're on Linux (using whatever package manager your distro ships), validates Docker if you're on macOS, creates shims for npm/pnpm/yarn/bun, and adds itself to your shell's &lt;code&gt;PATH&lt;/code&gt; via &lt;code&gt;.bashrc&lt;/code&gt;, &lt;code&gt;.zshrc&lt;/code&gt;, and &lt;code&gt;.profile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After that, every &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;pnpm add&lt;/code&gt;, &lt;code&gt;yarn add&lt;/code&gt;, or &lt;code&gt;bun add&lt;/code&gt; runs sandboxed. You don't set anything up per project. Commands like &lt;code&gt;npm run dev&lt;/code&gt; and &lt;code&gt;npm test&lt;/code&gt; and &lt;code&gt;npx&lt;/code&gt; go through untouched, those aren't install commands.&lt;/p&gt;

&lt;p&gt;To get rid of it: &lt;code&gt;npx ringfence-uninstall&lt;/code&gt;. Cleans up the &lt;code&gt;PATH&lt;/code&gt; entries and deletes &lt;code&gt;~/.ringfence/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff worth knowing
&lt;/h2&gt;

&lt;p&gt;Secret files become zero byte files, not missing files. Making a file absent risks breaking scripts that check for existence. An empty read is safer than &lt;code&gt;ENOENT&lt;/code&gt;. The tradeoff: if a dependency genuinely needs a &lt;code&gt;.env&lt;/code&gt; file with actual content during install, things break. The README is upfront about this.&lt;/p&gt;

&lt;p&gt;Ringfence unsets env vars and masks files. Your &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; doesn't leak through the environment just because it wasn't stored in a &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;bwrap containers launched by ringfence include &lt;code&gt;--die-with-parent&lt;/code&gt;. If ringfence crashes, the sandboxed process dies too. No orphaned containers.&lt;/p&gt;

&lt;p&gt;On macOS, packages removed during a container run stick around in &lt;code&gt;node_modules&lt;/code&gt; until you clean them manually. The sync back doesn't delete files, it only adds or overwrites. Uninstalled packages linger.&lt;/p&gt;

&lt;p&gt;Ringfence only protects install time, when lifecycle scripts fire. Once packages are on disk, &lt;code&gt;npm start&lt;/code&gt; or &lt;code&gt;node server.js&lt;/code&gt; runs with full host access. That's intentional. It's a supply chain guard, not a runtime sandbox.&lt;/p&gt;

&lt;p&gt;No Windows support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;The TanStack worm hit packages that had provenance. SLSA Build Level 3, verified. Provenance says where code came from, not what it does. That's the whole problem.&lt;/p&gt;

&lt;p&gt;Ringfence is two commands and covers npm, pnpm, yarn, and bun on Linux and macOS. If you're installing dependencies on either platform, there's not much reason to skip it.&lt;/p&gt;




&lt;p&gt;If you give ringfence a try, I'd love to hear how it goes. The macOS path in particular could use more real world testing, so feedback from Mac users is especially welcome. Bug reports, edge cases, or just "works fine on my machine" — all useful. Drop an issue on &lt;a href="https://github.com/asmyshlyaev177/ringfence/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you give ringfence a try, I'd love to hear how it goes. The macOS path in particular could use more real world testing, so feedback from Mac users is especially welcome. Bug reports, edge cases, or just "works fine on my machine" — all useful. Drop an issue on &lt;a href="https://github.com/asmyshlyaev177/ringfence/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ringfence.pages.dev/" rel="noopener noreferrer"&gt;https://ringfence.pages.dev/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>npm</category>
      <category>supplychainattack</category>
      <category>shaihulud</category>
    </item>
    <item>
      <title>How I Built state-in-url: My Journey Turning the URL Bar into Real React State</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 13 Apr 2026 13:39:54 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/how-i-built-state-in-url-my-journey-turning-the-url-bar-into-real-react-state-a67</link>
      <guid>https://dev.to/asmyshlyaev177/how-i-built-state-in-url-my-journey-turning-the-url-bar-into-real-react-state-a67</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24etxlwjhdvzujmmb2yb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24etxlwjhdvzujmmb2yb.gif" alt="state-in-url demo gif" width="600" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm the developer behind &lt;strong&gt;state-in-url&lt;/strong&gt;. The library lets you put real JavaScript values - numbers, Date objects, nested arrays, objects - directly into browser query parameters. It keeps full TypeScript support throughout. The hook behaves exactly like useState, but the state lives in the URL. It survives reloads. You can share the link and nothing gets lost.&lt;/p&gt;

&lt;p&gt;Try the live demo at &lt;a href="https://state-in-url.dev" rel="noopener noreferrer"&gt;https://state-in-url.dev&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Type in the form. Watch the URL change instantly. Refresh the page. The data stays exactly as you left it. The example sits right in the repo too.&lt;/p&gt;

&lt;p&gt;This project grew from a frustration I hit again and again while working on dashboards and larger forms. State ended up split across local variables, a store, and the URL for sharing. Other libraries came close, yet they always added too much setup or turned everything into strings and dropped my types. I decided to build the version I actually wanted to use every day.&lt;/p&gt;

&lt;p&gt;The repo now sits at 400+ stars on GitHub. Version 6.1.2 shipped a few weeks ago. It still surprises me that people use it. Here is the full story - the frustrations, the choices that worked, the testing struggles, and what stayed with me afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You have probably faced this situation. A page holds filters or a detailed form. State lives in three places at once. Local component values. Maybe a store. Plus the URL so users can bookmark or share their exact view. Syncing it all felt like constant extra work.&lt;/p&gt;

&lt;p&gt;I wanted the hook to stay dead simple. Return state and a setter, just like useState, but with TypeScript awareness of the exact shape. Real data types that survive the URL round trip. Clean support for Next.js App Router with SSR, plus React Router and Remix. No interference with other query parameters. And keep the whole thing tiny - zero runtime dependencies, under 2 KB gzipped.&lt;br&gt;
That became my starting point in spare time.&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;The choice I remain happiest about is straightforward. I used one plain JavaScript object for both default values and the type definition. Without separate schema or heavy generics. You write code like this&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;agree_to_terms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;time&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="p"&gt;}[],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;formState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass that object to the hook and everything flows. Types and defaults work out of the box.&lt;br&gt;
For shared state with useSharedState, I relied on a single JavaScript object. Components subscribe to changes, without React Context. It handled cases where unrelated parts of the app needed the same data without added complexity.&lt;br&gt;
The encoder and decoder required some tuning. The goal was a readable URL that still preserved real types. Encoding a larger object takes roughly a millisecond on my machine.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the Library and the Framework Challenges
&lt;/h2&gt;

&lt;p&gt;I structured the project as a pnpm monorepo. TypeScript dominates the codebase. Core logic lives in one package. Thin adapters cover Next.js, React Router, and Remix. Rollup manages the build. Nothing overly complicated.&lt;br&gt;
Next.js App Router created the biggest difficulties. The way searchParams moves between server and client components caused repeated issues. I added a small header workaround to carry data through on the server side. Hydration stayed intact. Remix and React Router presented fewer obstacles, yet I still needed to handle history modes, push versus replace, and debounce settings so the URL stayed calm during typing.&lt;/p&gt;

&lt;p&gt;Here is a real section from the Next.js 15 demo that runs live at state-in-url.dev&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUrlState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state-in-url/next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./formState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;()&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;urlState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUrlState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;searchParams&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
       &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;urlState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;e&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;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;curr&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="nx"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}))}&lt;/span&gt;
       &lt;span class="nx"&gt;onBlur&lt;/span&gt;&lt;span class="o"&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="nf"&gt;setUrl&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="c1"&gt;// update URL on blur for smoother typing&lt;/span&gt;
     &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* tags, dates, checkboxes - all typed correctly */&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;The code stays simple. It is just state that happens to live in the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing was hard but important
&lt;/h2&gt;

&lt;p&gt;I needed the library to feel reliable across frameworks and browsers. Real example apps for Next.js 14+, React Router v6+, and Remix live in the repo. Vitest covers the encoder and decoder basics. More real world tests cases covered with Playwright running full end-to-end tests in Chrome, Firefox, and Safari.&lt;/p&gt;

&lt;p&gt;A fix in one place often broke something elsewhere. The commit message "Tests finally passing on Next.js 15" appeared more times than I care to count. It took quite few attempts adjusting Playwright configurations and the CI matrix.&lt;br&gt;
GitHub Actions now runs linting, building, unit tests, and the complete browser suite on every push and PR. Semantic-release automates version bumps, changelog updates, GitHub releases, and npm publishing. Husky keeps commits tidy. Once the pipeline ran smoothly, maintaining the library stopped feeling like a burden.&lt;/p&gt;
&lt;h2&gt;
  
  
  Launching
&lt;/h2&gt;

&lt;p&gt;I published to npm. The README received every recipe I could gather - debounced updates, resetting to defaults, multiple independent state objects on one page. I linked the live demo and waited.&lt;br&gt;
The first stars arrived and felt surprisingly satisfying. Issues opened, mostly kind ones. Solid CI let me ship fixes quickly. I wrote the honest limits in a separate Limits.md file. URLs hold only so much - around 12 KB stays practical. Readers seemed to value the directness.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Making the API match useState exactly shaped every other decision and made the library pleasant to use.&lt;/li&gt;
&lt;li&gt;That single object for shape and defaults removed a lot of complexity.&lt;/li&gt;
&lt;li&gt;Testing inside real frameworks felt painful yet essential - skipping it would have limited adoption.&lt;/li&gt;
&lt;li&gt;Automating releases and chores with semantic-release and GitHub Actions turned maintenance into something manageable.&lt;/li&gt;
&lt;li&gt;Keeping the bundle tiny happened on purpose. I watched the size and avoided dependencies deliberately.
Would I change anything today? I might add Svelte or Astro support earlier. Hash routing could be interesting. The roadmap still has room to grow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this sound interesting give it a try&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add state-in-url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo lives at &lt;a href="https://state-in-url.dev" rel="noopener noreferrer"&gt;state-in-url.dev&lt;/a&gt;. The repository sits &lt;a href="https://github.com/asmyshlyaev177/state-in-url" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Star it if the approach fits your work.&lt;/p&gt;

&lt;p&gt;I would enjoy hearing about the URL-state problems you encounter. Leave a comment. Open an issue. Send a pull request. Sharing the process publicly became one of the most rewarding aspects.&lt;br&gt;
Happy coding, MIT license, of course. Build something useful with it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>statemanagement</category>
      <category>react</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>New portfolio website</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 15 Sep 2025 15:24:22 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/new-portfolio-website-4o5i</link>
      <guid>https://dev.to/asmyshlyaev177/new-portfolio-website-4o5i</guid>
      <description>&lt;p&gt;Tried few new things this time.&lt;/p&gt;

&lt;p&gt;Created an initial design with V0, had to refactor a lot, and change many details.&lt;/p&gt;

&lt;p&gt;Chose an Astro framework for a minimal footprint on bundle size, and the best performance.&lt;/p&gt;

&lt;p&gt;It's quite simple, but hopefully worth it. Will appreciate any feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://asmyshlyaev177.dev/" rel="noopener noreferrer"&gt;Portfolio link&lt;/a&gt;&lt;/p&gt;

</description>
      <category>portfolio</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React rendering quiz</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 08 May 2025 09:55:24 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/react-rendering-quiz-443k</link>
      <guid>https://dev.to/asmyshlyaev177/react-rendering-quiz-443k</guid>
      <description>&lt;p&gt;A simple quiz to illustrate how React rendering works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Parent&lt;/span&gt;&lt;span class="p"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useEffect&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&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;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      Parent component
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;br&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Child&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Child&lt;/span&gt;&lt;span class="p"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useEffect&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Child component&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to guess what will be printed to console.&lt;/p&gt;

&lt;p&gt;Answer&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcarx620tb7bru42a5jw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcarx620tb7bru42a5jw.png" alt="Answer" width="241" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some explanation:&lt;br&gt;
⇾ First &lt;code&gt;Parent&lt;/code&gt; is parsed and rendered, without &lt;code&gt;useEffect&lt;/code&gt;,&lt;br&gt;
⇾ after that &lt;code&gt;Child&lt;/code&gt;, when there is no more nested components is turn of async hooks&lt;br&gt;
⇾ React will run &lt;code&gt;useEffect&lt;/code&gt; from children to parents.&lt;/p&gt;

&lt;p&gt;Few more logs, probably related to dev mode.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Breakpoints variables with pure CSS</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 01 May 2025 10:28:18 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/breakpoints-variables-with-pure-css-18ln</link>
      <guid>https://dev.to/asmyshlyaev177/breakpoints-variables-with-pure-css-18ln</guid>
      <description>&lt;p&gt;In CSS can use variables most of the time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;red&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;But, can't use variables in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media" rel="noopener noreferrer"&gt;@media&lt;/a&gt; queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--br-md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="c"&gt;/* will never work */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unexpected solution is to use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@container" rel="noopener noreferrer"&gt;@container&lt;/a&gt; queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--is-tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;@media&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1200px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;--is-tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--is-tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-areas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;'header aside'&lt;/span&gt;
      &lt;span class="s2"&gt;'feed   aside'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;7.16rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;17.6rem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;7.16rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max-content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--feed-columns&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;Only downside is Firefox lacks support of it, but it doesn't do so well on many things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js usage
&lt;/h2&gt;

&lt;p&gt;In Next.js can use &lt;a href="https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media" rel="noopener noreferrer"&gt;postcss-custom-media&lt;/a&gt; plugin to achieve same outcome, for all browsers and with css modules.&lt;/p&gt;

&lt;p&gt;How it looks like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@custom-media&lt;/span&gt; &lt;span class="n"&gt;--is-desktop&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1200px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--is-desktop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-areas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;'header aside'&lt;/span&gt;
      &lt;span class="s2"&gt;'feed   aside'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;7.16rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;17.6rem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;7.16rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max-content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--feed-columns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://nextjs.org/docs/pages/guides/post-css" rel="noopener noreferrer"&gt;Official&lt;/a&gt; docs&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install all dependencies:&lt;br&gt;
&lt;code&gt;npm install --save-dev postcss autoprefixer postcss-flexbugs-fixes @csstools/postcss-global-data postcss-custom-media postcss-preset-env&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add PostCSS config to &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"postcss"&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;"plugins"&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;"postcss-flexbugs-fixes"&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;"@csstools/postcss-global-data"&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;"files"&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;need&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;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;global&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;here&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"core/styles/vars.css"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"postcss-custom-media"&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;"postcss-preset-env"&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;"autoprefixer"&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;"flexbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no-2009"&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;"stage"&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="nl"&gt;"features"&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;"custom-properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>css</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Minification bugs</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Fri, 04 Apr 2025 11:37:21 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/minification-bugs-2p9m</link>
      <guid>https://dev.to/asmyshlyaev177/minification-bugs-2p9m</guid>
      <description>&lt;p&gt;Today I learned that minification in JS is a double-edged sword.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhknespllmamj0mkhjjc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhknespllmamj0mkhjjc.png" alt="Esbuild docs" width="799" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was a weird bug, took me quite some time to figure out that problem in minification.&lt;br&gt;
Stopped using &lt;code&gt;terser&lt;/code&gt; for the same reasons.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyup1lsqr57tut71tv75e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyup1lsqr57tut71tv75e.png" alt="esbuild issue" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Next.js at refactor or extinct situation.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Fri, 13 Dec 2024 08:21:03 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/nextjs-at-refactor-or-extinct-situation-27mi</link>
      <guid>https://dev.to/asmyshlyaev177/nextjs-at-refactor-or-extinct-situation-27mi</guid>
      <description>&lt;p&gt;Next.js become a hype of the day (or year), but it's slowly suffocating under the weight of technical debt.&lt;/p&gt;

&lt;p&gt;There are many big enough pain points with Next.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow and painful dev mode, if you changed something and need to check more than 1 route, you need to wait till route compiled. Next.js server also uses few gigabytes of RAM. Turbopack complaining about perfectly working TS code, and struggle to understand &lt;code&gt;:global&lt;/code&gt; in CSS modules, still not production ready.&lt;/li&gt;
&lt;li&gt;Whole workflow is intended to be used mostly with server components, but in real world applications big chunks of applications use client side JS, because it's way faster and more responsive.&lt;/li&gt;
&lt;li&gt;Routing is a mess, pages router was good enough, but App Router it's just a wrapper around it, that lead to more bugs and complexity.&lt;/li&gt;
&lt;li&gt;Vercel service is good, but if popularity of your project going parabolic, costs will follow - &lt;a href="https://news.ycombinator.com/item?id=40618220" rel="noopener noreferrer"&gt;96k bill&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just stumbled upon a bug when &lt;code&gt;useParams&lt;/code&gt; hook periodically returning dead wrong values.&lt;/p&gt;

&lt;p&gt;Searched for existing issues, didn't find any, tried to open issue on their GitHub, can't do than without providing reproducible example. Repository is private, I can't share all of it. And I can't pinpoint the exact condition for a bug to appear in a big enough codebase.&lt;/p&gt;

&lt;p&gt;Similar situation was with &lt;code&gt;Cypress&lt;/code&gt; testing framework. For years people opened issues about flaky tests and memory leaks, all of them were closed, because can't properly reproduce them. Easy to make something works in a TODO app, real challenge is a big and complex application. Usually, people work under NDA and can't just share the whole repository.&lt;br&gt;
After some time Playwright came and Cypress isn't relevant any more.&lt;/p&gt;

&lt;p&gt;Let's see approximate quality ratio, by calculating stars to bugs for Cypress.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3ygqalkolclgbro763r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3ygqalkolclgbro763r.png" alt="Cypress github stats" width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;47600 stars / 14600 bugs = &lt;strong&gt;34 quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Cypress&lt;/code&gt;'s successor - &lt;code&gt;Playwright&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmueqcjz1w9aal975t1v0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmueqcjz1w9aal975t1v0.png" alt="Playwright github stats" width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;67_700 stars / 692 issue = &lt;strong&gt;97 quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm aware that many bugs issues it's just misunderstanding or duplicates, still, if something confusing in a documentation/example, it is a con of software.&lt;/p&gt;

&lt;p&gt;Let's see about &lt;code&gt;Next.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1mysu3v1goa6yughj4z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1mysu3v1goa6yughj4z.png" alt="Next.js github stats" width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;128_000 stars / 3000 bugs = &lt;strong&gt;42 quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's see &lt;code&gt;React.js&lt;/code&gt; repo for context&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F597zqm0gu4y2s4o0rmhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F597zqm0gu4y2s4o0rmhr.png" alt="React.js github stats" width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;230_000 stars / 754 bugs = &lt;strong&gt;305 quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;Remix.js&lt;/code&gt; repo (SSR framework from &lt;code&gt;react-router&lt;/code&gt; team)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9g5x88ve929x7892po87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9g5x88ve929x7892po87.png" alt="Remix.js github stats" width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;30_300 stars / 295 bugs = &lt;strong&gt;102 quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All of this just my opinions, but it's backed by years of experience with different frameworks/libraries and observations.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Beyond console.log</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sun, 24 Nov 2024 09:49:53 +0000</pubDate>
      <link>https://dev.to/asmyshlyaev177/beyond-consolelog-1h40</link>
      <guid>https://dev.to/asmyshlyaev177/beyond-consolelog-1h40</guid>
      <description>&lt;h2&gt;
  
  
  intro
&lt;/h2&gt;

&lt;p&gt;As much as I hope that things will always work from first try, better to be prepared to debug it. &lt;code&gt;console.log&lt;/code&gt; remains the main tool to do it, because it is the simplest and fastest.&lt;/p&gt;

&lt;p&gt;Few console methods are known to everybody, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going deeper
&lt;/h2&gt;

&lt;p&gt;There are more tricks to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;count&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For example, if you need to count how many times component re-render can use &lt;code&gt;console.count&lt;/code&gt;, e.g. &lt;code&gt;console.count('comp1')&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furymtdynzh46ldlb96ga.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furymtdynzh46ldlb96ga.png" alt="console.count" width="663" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;group&lt;/code&gt; and &lt;code&gt;groupCollapsed&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Can log some info in a collapsed group with &lt;code&gt;console.groupCollapsed&lt;/code&gt;, great in case of many error messages, when they printed collapsed it's less distracting, but Node.js/Sentry probably will print all expanded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjeclio3a0crk4gl59oby.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjeclio3a0crk4gl59oby.png" alt="console.groupCollapsed" width="675" height="218"&gt;&lt;/a&gt;&lt;br&gt;
After click &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1dig323rwr0pmg998xh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1dig323rwr0pmg998xh.png" alt="console.groupCollapsed expanded" width="675" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  styling
&lt;/h3&gt;

&lt;p&gt;Can use some CSS&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv19au70e4gi4lpuyn9mt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv19au70e4gi4lpuyn9mt.png" alt="console.log with CSS styles" width="675" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;dir&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Can log properties of an object with &lt;code&gt;console.dir&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Famahwwee45em2018xmuv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Famahwwee45em2018xmuv.png" alt="console.dir" width="408" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  objects
&lt;/h3&gt;

&lt;p&gt;Can log few objects with curly braces like &lt;code&gt;console.log({ obj1, obj2 })&lt;/code&gt;, instead of &lt;code&gt;console.log('Ojb1', obj1)&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;assert&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Can assert things with &lt;code&gt;console.assert&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4390c1ubs0a4yezgf501.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4390c1ubs0a4yezgf501.png" alt="console.assert" width="675" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;time&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Can measure how long some operation takes with &lt;code&gt;console.time&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuhzjqa4estugyguph38r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuhzjqa4estugyguph38r.png" alt="console.time" width="656" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;trace&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Print a stack trace with &lt;code&gt;console.trace()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvq1o0nkxsawrssgb4mpy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvq1o0nkxsawrssgb4mpy.png" alt="console.trace" width="644" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;clear&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Clear things up with &lt;code&gt;console.clear()&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;p&gt;Check out great old post - &lt;a href="https://dev.to/lissy93/fun-with-consolelog-3i59"&gt;https://dev.to/lissy93/fun-with-consolelog-3i59&lt;/a&gt; &lt;/p&gt;

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