<?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: Luigi Davide Micca</title>
    <description>The latest articles on DEV Community by Luigi Davide Micca (@luigidavidemicca).</description>
    <link>https://dev.to/luigidavidemicca</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4239198%2Fd907ebe5-580d-4e47-b997-8cfbd710cabf.jpg</url>
      <title>DEV Community: Luigi Davide Micca</title>
      <link>https://dev.to/luigidavidemicca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luigidavidemicca"/>
    <language>en</language>
    <item>
      <title>Your dependencies are the attack surface: what 2026’s npm worms taught us</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Wed, 11 Nov 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/your-dependencies-are-the-attack-surface-what-2026s-npm-worms-taught-us-35bj</link>
      <guid>https://dev.to/luigidavidemicca/your-dependencies-are-the-attack-surface-what-2026s-npm-worms-taught-us-35bj</guid>
      <description>&lt;p&gt;On 11 May, between 19:20 and 19:26 UTC, 84 malicious versions of 42 &lt;code&gt;@tanstack/*&lt;/code&gt; packages landed on npm. Six minutes. Every one of them carried a valid SLSA Build Level 3 provenance attestation — the cryptographic receipt that says "this was built by the project's own pipeline". It was true. The pipeline had been turned against its owners.&lt;/p&gt;

&lt;p&gt;Eight days later the same family of worm, Mini Shai-Hulud, published 639 poisoned versions of 323 packages in about an hour, most of them in the &lt;code&gt;@antv&lt;/code&gt; ecosystem. Within 48 hours of the first wave it had already spread to 172 packages across npm and PyPI.&lt;/p&gt;

&lt;p&gt;If your product is built with JavaScript, those numbers are not someone else's news. Your &lt;code&gt;node_modules&lt;/code&gt; folder is the largest codebase you ship, and you wrote almost none of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the two waves worked
&lt;/h2&gt;

&lt;p&gt;The TanStack wave is the one to study, because it beat the controls the industry has been asking for. According to the post-mortems, the attacker opened a pull request from a fork that poisoned the GitHub Actions cache. The project's release workflow later restored that cache, and the attacker's binaries read the short-lived OIDC publish token straight out of the runner's memory. The tests failed and the publish step was skipped — and the signed packages reached the registry anyway.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@antv&lt;/code&gt; wave was cruder and just as effective. Each infected package gained a &lt;code&gt;preinstall&lt;/code&gt; hook that ran a script the moment anyone installed it. The script collected what a developer machine or a CI runner keeps lying around — GitHub and npm tokens, AWS credentials, Kubernetes and Vault secrets, SSH keys, Docker auth, database connection strings — and sent it away. Then it used the stolen npm token to list every package the victim could publish, injected itself into them, bumped the version and republished them under the victim's name. That is what makes it a worm: each infection publishes the next ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the controls did not stop it
&lt;/h2&gt;

&lt;p&gt;npm has tightened a lot since late 2025. Classic tokens were revoked for good on 9 December 2025. A new write token now expires after seven days by default, and after ninety at most. Publishing enforces 2FA. Trusted publishing replaces stored tokens with credentials minted for a single CI job. All of that is good, and all of it was in place for at least part of what happened in May.&lt;/p&gt;

&lt;p&gt;The pattern is the uncomfortable part. Each control closes the door the last attack used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2FA&lt;/strong&gt; stops a stolen password. The worm does not need one; it uses tokens that already passed 2FA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived tokens&lt;/strong&gt; limit how long a stolen token works. An hour is plenty for a worm that republishes in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance&lt;/strong&gt; proves where a package was built. It says nothing about whether the build was hijacked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusted publishing&lt;/strong&gt; removes stored secrets, but its token is only as narrow as the workflow you allow to request it. In the TanStack case it was the whole repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is useless. They are necessary, and they are not sufficient. As one researcher put it after the TanStack incident, the control that actually mattered was the scope of the OIDC trust — not provenance, not 2FA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five settings that would have helped
&lt;/h2&gt;

&lt;p&gt;Most teams are not publishers; they are consumers. The good news is that the consumer side has cheap, boring defences, and they are exactly the kind that hold up against an attack nobody has seen yet.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install from the lockfile, nothing else.&lt;/strong&gt; In CI, &lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; or &lt;code&gt;bun install --frozen-lockfile&lt;/code&gt;. A poisoned version can only reach you when something resolves a new version; a frozen install never does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not run your dependencies' install scripts by default.&lt;/strong&gt; The &lt;code&gt;@antv&lt;/code&gt; payload lived in a &lt;code&gt;preinstall&lt;/code&gt; hook. pnpm 10 and Bun already refuse to run dependency lifecycle scripts unless you allow a package by name; with npm, set &lt;code&gt;ignore-scripts=true&lt;/code&gt; in &lt;code&gt;.npmrc&lt;/code&gt; and allowlist the few packages that genuinely need a build step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait before you upgrade.&lt;/strong&gt; Both waves were detected within minutes to an hour. A cooldown that refuses versions younger than a day would have skipped them entirely, at the cost of getting patches a day later. pnpm 11 now does this by default, waiting 24 hours; npm (&lt;code&gt;min-release-age&lt;/code&gt;), Bun and Yarn have their own setting for it, and Renovate can hold its pull requests the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets away from the job that installs.&lt;/strong&gt; Any script that runs during an install can read the environment it runs in. The job that runs &lt;code&gt;install&lt;/code&gt; and the tests should not hold your cloud keys or publish rights. If you publish, grant &lt;code&gt;id-token: write&lt;/code&gt; only to the publish job, scope trusted publishing to one workflow on a protected branch, and never let a fork's code share a cache with your release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have fewer dependencies.&lt;/strong&gt; Every package is a maintainer account you are trusting with your build machine. The cheapest dependency to secure is the one you removed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Honesty corner
&lt;/h2&gt;

&lt;p&gt;None of this makes you immune. A lockfile pins a version that may already be bad; a cooldown protects you from fast-moving worms, not from a patient attacker who waits a week. The goal is not zero risk. It is to make sure that the next wave — and there will be one, the cadence this year has been monthly — finds nothing to run and nothing to steal on your machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The business version
&lt;/h2&gt;

&lt;p&gt;If you run a company rather than a codebase, three questions to ask your team or your vendor this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does CI install strictly from the lockfile, with install scripts off by default?&lt;/li&gt;
&lt;li&gt;Which secrets does the job that installs dependencies have access to — and why?&lt;/li&gt;
&lt;li&gt;If a maintainer we depend on were compromised tonight, how would we know, and how long would it take us to rotate everything?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A team that answers all three without hesitating has done the work. A team that has to think about it has a weekend of configuration ahead, which is far cheaper than the alternative.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm an independent software architect focused on front-end work. &lt;a href="https://luigimicca.com/en/services/technical-consulting" rel="noopener noreferrer"&gt;Technical audits&lt;/a&gt; of JavaScript codebases — dependencies, build pipelines, the things that only get looked at after an incident — are a good part of what I do. If you'd like a second pair of eyes on yours, &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: a 30-minute intro call is free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>AI writes your code faster. Who pays for the maintenance?</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Mon, 26 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/ai-writes-your-code-faster-who-pays-for-the-maintenance-5dh8</link>
      <guid>https://dev.to/luigidavidemicca/ai-writes-your-code-faster-who-pays-for-the-maintenance-5dh8</guid>
      <description>&lt;p&gt;Every engineering leader I talk to this year tells me the same two things: the team ships noticeably faster with AI assistants, and nobody is quite sure what all that speed is doing to the codebase. Both are correct, and now there's data for the second one.&lt;/p&gt;

&lt;p&gt;The most careful study so far comes from Carnegie Mellon, presented at MSR 2026. It compared 807 open-source projects that adopted the Cursor editor with 1,380 similar projects that didn't. The speed was real and short-lived: commits rose 55% in the first month and the effect had largely faded by the second. What didn't fade: static-analysis warnings up about 30%, code complexity up about 41%, and both still there months later. The authors' conclusion is the line to underline — the extra warnings and complexity are what drag the velocity back down.&lt;/p&gt;

&lt;p&gt;It isn't the only signal. CodeRabbit's December 2025 report, comparing AI-coauthored pull requests with human-only ones, found about 1.7 times more issues in the AI ones, with logic errors up 75%. CodeRabbit sells code review, so read it with that in mind — but it points the same way as the academic one. The compounding kind of expensive, not the linear kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this debt is different
&lt;/h2&gt;

&lt;p&gt;Technical debt isn't new; I've spent a good part of my career &lt;a href="https://luigimicca.com/en/services/technical-consulting" rel="noopener noreferrer"&gt;cleaning it up&lt;/a&gt;. But the classic kind was at least &lt;em&gt;chosen&lt;/em&gt;: a developer took a shortcut, knew it, and someone could write "TODO: fix properly" above it. AI-generated debt has a nastier property — nobody knows the shortcut was taken.&lt;/p&gt;

&lt;p&gt;The assistant produces code that compiles, passes the tests and reads plausibly. What it lacks is context: it doesn't know your architecture's boundaries, why the last team abandoned that pattern, or that three other modules already solve this problem. So it happily writes a fourth solution, slightly different from the other three. I once wrote that a fast-growing codebase breeds five slightly different implementations of the same thing; AI does that at machine speed, politely, in perfect syntax.&lt;/p&gt;

&lt;p&gt;Review was supposed to catch this. In practice, plausible code is precisely the hardest kind to review — nothing looks wrong, so approval becomes a reflex. The result is what the industry calls "code smells": issues that don't break anything today and quietly raise the cost of every change tomorrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honesty corner
&lt;/h2&gt;

&lt;p&gt;I use AI assistants daily, and they're genuinely good — for scaffolding, for tests, for the boring parts I used to postpone. This isn't an argument for abstinence. The dividing line isn't &lt;em&gt;assisted versus manual&lt;/em&gt;; it's &lt;em&gt;assisted versus unattended&lt;/em&gt;. Speed that produces code nobody understands isn't velocity, it's deferred cost with excellent syntax highlighting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is process, not heroics
&lt;/h2&gt;

&lt;p&gt;What works, in the teams I've seen manage this well, is boring and effective:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Someone owns the architecture.&lt;/strong&gt; A human decides where the boundaries are and which patterns are canonical — and reviews AI output &lt;em&gt;against those decisions&lt;/em&gt;, not against "does it work".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review AI code like a junior developer wrote it.&lt;/strong&gt; Not hostility — scrutiny. The question isn't "is this broken?" but "should this exist, here, in this shape?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static analysis and tests as the contract.&lt;/strong&gt; Machines are consistent reviewers of machine output. Wire the thresholds into CI so "it passes" keeps meaning something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Periodic debt audits.&lt;/strong&gt; Measure time-to-change and defect rates per area. The 30% and 41% from the study aren't destiny — they're what happens when nobody looks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete duplicates on sight.&lt;/strong&gt; The fourth implementation is cheapest to remove the week it appears.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The business version
&lt;/h2&gt;

&lt;p&gt;If you run a company rather than a codebase, here's the translation: AI-assisted development is a real productivity gain &lt;em&gt;only if&lt;/em&gt; the second year doesn't eat the first. When you evaluate a team or a vendor, ask how they review AI-generated code, what their duplication and defect metrics look like, and who owns the architecture. Silence on those questions is the answer.&lt;/p&gt;

&lt;p&gt;Faster is good. Faster with the meter running quietly in the background is how you end up hiring someone like me two years later — which, full disclosure, is fine for me and expensive for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm an independent software architect focused on front-end work — &lt;a href="https://luigimicca.com/en/services/technical-consulting" rel="noopener noreferrer"&gt;audits and modernisation&lt;/a&gt; of codebases that grew faster than their structure, human-written or otherwise. If your team ships fast and sleeps badly, &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: a 30-minute intro call is free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codequality</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>A realtime channel is a door, and most apps leave it open</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/a-realtime-channel-is-a-door-and-most-apps-leave-it-open-5gdi</link>
      <guid>https://dev.to/luigidavidemicca/a-realtime-channel-is-a-door-and-most-apps-leave-it-open-5gdi</guid>
      <description>&lt;p&gt;I spent a week this autumn attacking a realtime relay I had built myself, and the first thing that fell was not clever. A client with no cookie opened a socket, asked for a topic called &lt;code&gt;channel:3&lt;/code&gt;, and received every message posted there — plus a helpful &lt;code&gt;__count&lt;/code&gt; event telling it how many real users were in the room. No exploit. No race. Just a name that was easy to guess and a server that never asked who was asking.&lt;/p&gt;

&lt;p&gt;The relay was "correct" in every way the tests measured. It checked the browser's &lt;code&gt;Origin&lt;/code&gt;, it capped message sizes, it closed misbehaving sockets with the right codes. What it did not do was the only thing that mattered: decide whether this socket, carrying this session, may hold this topic. The handshake was a door with a lock on the outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the decision has to live
&lt;/h2&gt;

&lt;p&gt;A WebSocket has exactly one moment that looks like an HTTP request: the handshake. After the &lt;code&gt;101&lt;/code&gt;, there are no headers, no cookies re-sent, no middleware runs — just frames on a wire that stays open for hours. Whatever authorization exists must happen &lt;em&gt;at the upgrade&lt;/em&gt;, with the session already verified, per topic the socket asks for. If the decision is anywhere else, it is nowhere.&lt;/p&gt;

&lt;p&gt;This is also why "we authorize in the API routes" does not transfer. Your &lt;code&gt;/api/channels/3/messages&lt;/code&gt; endpoint checks membership on every call because every call is a request. The socket subscribed once, weeks ago in socket-time, and the relay has been forwarding ever since. The two paths look like the same feature to the product and are completely different surfaces to an attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four rules I apply now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The topic name is not a secret.&lt;/strong&gt; &lt;code&gt;dm:ana:luigi&lt;/code&gt; is a routing key, and routing keys are enumerable. Scoping by unguessable id is a delay, not a wall. The rule has to be &lt;em&gt;the session may hold this topic&lt;/em&gt;, evaluated against what the session proves — membership, ownership, role — not against how hard the string is to type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Presence is data.&lt;/strong&gt; The subscriber count felt like harmless UI sugar until it told an anonymous client how many people were in a private room. Anything the relay emits about a topic is part of that topic's confidentiality. If a topic is private, its count is too, and so is the fact that it exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refuse by default in production, say so in development.&lt;/strong&gt; A relay with no rule configured should refuse every subscription when deployed, loudly, and admit everything only in development, where the startup banner says so in a way you cannot miss. The dangerous configuration is the silent one: open by default, with a note in the docs that nobody reads before the incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revocation has a latency, and it should be named.&lt;/strong&gt; A socket authorized at its handshake keeps its topics until it closes. When you revoke a user — log out everywhere, remove them from a workspace — their open sockets are still subscribed. Decide what that means: cut the socket, push a "you are gone" event, or accept that the next handshake is the boundary. All three are defensible. Not deciding is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good looks like from the application side
&lt;/h2&gt;

&lt;p&gt;The shape I ended up with is small enough to read in one sitting. The application registers a function — &lt;em&gt;given this request and this topic name, yes or no&lt;/em&gt; — and the relay calls it once per topic at the handshake, with the session the framework already verified. Publishing from the browser gets a second function or inherits the first. There is no annotation, no role matrix, no configuration file: a function, in the language the rest of the authorization is written in, next to the handler that creates the channel in the first place.&lt;/p&gt;

&lt;p&gt;It took an afternoon to add and it closed the worst finding of a two-day stress test. The relay had been "done" for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question to take to your own stack
&lt;/h2&gt;

&lt;p&gt;Open the file that handles your WebSocket upgrade, or your SSE endpoint, or whatever your realtime layer is called. Find the line where a subscription is accepted. Now ask what, on that line, proves the subscriber may receive what follows — not what proves the browser is on your domain, not what proves the message is well-formed. If the answer is "the name of the topic", you have a door with the lock on the outside, and the people who find those do not file issues.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This piece comes out of building &lt;a href="https://github.com/LuigiDavideMicca/borgo" rel="noopener noreferrer"&gt;borgo&lt;/a&gt;, an open-source React and Go framework I maintain. Its own realtime layer had exactly this hole until version 0.30 closed it along these lines.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>Next.js and the cost of magic</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Tue, 08 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/nextjs-and-the-cost-of-magic-4j0h</link>
      <guid>https://dev.to/luigidavidemicca/nextjs-and-the-cost-of-magic-4j0h</guid>
      <description>&lt;p&gt;I have built more products on Next.js than on anything else, and I would still reach for it in plenty of situations. That is exactly why its costs are worth naming precisely. Not the Twitter version — "Next bad, framework heavy" — but the specific bills that arrive months later, itemised, when the honeymoon is over and the app is load-bearing.&lt;/p&gt;

&lt;p&gt;They all trace back to the same trade. Next made hard things easy by taking decisions away from you. That is what magic is. And every decision it took away is one you eventually need back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache you never asked for
&lt;/h2&gt;

&lt;p&gt;For two major versions, &lt;code&gt;fetch()&lt;/code&gt; inside Next did not behave like &lt;code&gt;fetch()&lt;/code&gt;. Responses were cached by default — aggressively, invisibly, at several layers with different names. Teams shipped dashboards that showed yesterday's data and spent days learning the difference between the Data Cache, the Full Route Cache, the Router Cache and the browser's own, each with its own invalidation story.&lt;/p&gt;

&lt;p&gt;Then Next 15 flipped the default. Uncached, like the platform. The flip was the right call — and it was also an admission, and a migration, and a re-audit of every data read in every app that had learned the old rules. When a framework overrides a web primitive and later un-overrides it, both moves are breaking changes, and both were yours to absorb.&lt;/p&gt;

&lt;p&gt;The lesson is not "caching is hard", although it is. The lesson is that implicit caching is a loan. You ship faster today because something cached for you; you pay later when you need to know &lt;em&gt;exactly&lt;/em&gt; what is cached, and the answer is spread across four layers of documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A router that changed under your feet
&lt;/h2&gt;

&lt;p&gt;The Pages Router was small and legible: a file is a route, &lt;code&gt;getServerSideProps&lt;/code&gt; runs on the server, props arrive in your component. You could hold it in your head.&lt;/p&gt;

&lt;p&gt;The App Router is more capable and much bigger: layouts, nested streaming, server components, client boundaries, parallel routes, intercepting routes. Some of that power is real. But it did not arrive as an addition — it arrived as &lt;em&gt;the future&lt;/em&gt;, with the old router in maintenance mode and every new feature landing on one side of the line. Migration was not a codemod; it was a rewrite of your data flow, your error handling and your mental model, undertaken not because your product needed it but because your framework moved.&lt;/p&gt;

&lt;p&gt;There is a special tiredness in teams that have done this twice. It shows up as a question in planning meetings: "is this a Next thing or an us thing?" When that question comes up often, the framework has stopped being infrastructure and started being weather.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;"use client"&lt;/code&gt; is a confession
&lt;/h2&gt;

&lt;p&gt;The directive itself is fine. What it confesses is not: the split between server components and client components pushed a distributed-systems question — &lt;em&gt;where does this code run?&lt;/em&gt; — into every file of an application, to be answered by every developer, forever, one directive at a time.&lt;/p&gt;

&lt;p&gt;Get it wrong in one direction and you ship a server secret to the browser. Get it wrong in the other and your bundle quietly grows, because one &lt;code&gt;"use client"&lt;/code&gt; at the top of a tree drags everything under it across the line. The framework cannot fully check either mistake, which is why every team I know has a folklore document about it — and folklore is what you write when the type system can't help you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The best version lives on one host
&lt;/h2&gt;

&lt;p&gt;Run Next on its home platform and everything works: image optimisation, ISR, the edge pieces, the caches. Run it on your own boxes — which is where plenty of European clients contractually need it — and you meet the asterisks. Self-hosted ISR wants shared storage you now operate. Image optimisation wants a service you now provision. Some behaviours differ just enough that your staging environment tells you less than it should.&lt;/p&gt;

&lt;p&gt;None of this is sinister; a company funds the framework and the framework fits the company. But it means the honest comparison is never "Next versus something else". It is "Next on their infrastructure, or a different set of trade-offs on yours" — and that second option deserves to be evaluated on purpose, not discovered during a procurement review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want
&lt;/h2&gt;

&lt;p&gt;Fewer layers I have to trust and more I can read. Data fetching that behaves like the platform primitive it wraps. A router that fits in working memory. Costs that appear where I can see them — in my code — rather than where I cannot: in a cache with four names, on a host I did not choose.&lt;/p&gt;

&lt;p&gt;Easy setup is worth one afternoon. Legibility is worth every afternoon after that. Magic optimises the first day of a project; the bill is presented across the following thousand. Whatever you build on, check who holds the loan.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I maintain &lt;a href="https://github.com/LuigiDavideMicca/borgo" rel="noopener noreferrer"&gt;borgo&lt;/a&gt;, an open-source, self-hosted React framework, so I have a horse in this race. Every cost above can be checked without it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>nextjs</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>The quiet cost of running a Node server</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Sat, 22 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/the-quiet-cost-of-running-a-node-server-fhb</link>
      <guid>https://dev.to/luigidavidemicca/the-quiet-cost-of-running-a-node-server-fhb</guid>
      <description>&lt;p&gt;Every Node server I have inherited came with the same folklore. "It restarts at night, that's normal." "Don't run the export on the same box as the API." "If it gets slow, add a replica." Nobody could say why any of this was true. They had simply learned, one incident at a time, to work around a runtime that was never designed to be a server in the first place.&lt;/p&gt;

&lt;p&gt;I say that with affection. I have shipped a lot of Node. But after a few years of operating it in production next to other things, the costs are consistent enough to name.&lt;/p&gt;

&lt;h2&gt;
  
  
  You pay for memory you never use
&lt;/h2&gt;

&lt;p&gt;A Node process that does nothing sits at 40–60 MB. Add a web framework, an ORM, a validation library and a logger — the ordinary furniture of a backend — and the idle footprint is 100–200 MB before the first request arrives. Under load the V8 heap grows toward whatever &lt;code&gt;--max-old-space-size&lt;/code&gt; allows, and it does not hand that memory back to the OS with any enthusiasm.&lt;/p&gt;

&lt;p&gt;This matters in a specific, unglamorous way: it sets the floor on how small your instances can be. A service that could run comfortably on 128 MB needs a 512 MB slot because of what it is written in, not what it does. Multiply by environments, by replicas, by the "one more microservice" that every team accumulates, and the bill is real long before the traffic is.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thread, shared with everyone
&lt;/h2&gt;

&lt;p&gt;The event loop is Node's great idea and its defining constraint. Every request, every timer, every database callback is scheduled onto a single thread. As long as every piece of work is short, this is remarkably efficient. The moment one piece of work is long — parsing a 40 MB JSON upload, rendering a big page, compressing or hashing with a synchronous call — every other request on that process waits behind it.&lt;/p&gt;

&lt;p&gt;The symptom is not a crash. It is latency that spikes for reasons that never correlate with the request that suffered. The cure everyone reaches for is worker threads or a second process, which means you are now operating a small distributed system to get what other runtimes give you with a single keyword.&lt;/p&gt;

&lt;p&gt;There is a second-order effect that is easy to miss: a CPU-bound task does not just slow responses, it &lt;strong&gt;stops timers&lt;/strong&gt;. A keep-alive that should fire every two seconds fires when the loop gets around to it. A health check answers late. A connection that looks alive from the outside is simply waiting its turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restarts as a feature
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pm2&lt;/code&gt;, &lt;code&gt;forever&lt;/code&gt;, a systemd unit with &lt;code&gt;Restart=always&lt;/code&gt; — every production Node setup grows a supervisor, and most of them grow a scheduled restart too. Ask why and the answer is some version of "memory creeps". Sometimes it is a real leak. More often it is fragmentation, or a cache that never evicts, or a promise chain that holds a reference a little too long. Nobody finds it because restarting is cheaper than finding it.&lt;/p&gt;

&lt;p&gt;This is a rational decision in the moment and a slow poison over years. A system that needs to be restarted to stay healthy cannot be reasoned about, and the nightly restart becomes the thing that masks the next bug, and the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Node is genuinely right
&lt;/h2&gt;

&lt;p&gt;None of this argues against Node for what it is unmatched at: turning components into HTML, running the same code on the server and in the browser, the ecosystem of tooling around that job. Rendering is bursty, stateless and short — exactly the shape the event loop was built for.&lt;/p&gt;

&lt;p&gt;The trouble starts when the same process is also asked to be the thing that holds the database pool, enforces the rate limit, streams the export, keeps the websocket subscriptions and talks to the payment provider. Those are long-lived, stateful, occasionally CPU-heavy jobs, and they belong on a runtime that has threads, a small fixed footprint and a garbage collector tuned for servers rather than browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape that works
&lt;/h2&gt;

&lt;p&gt;The setups I trust most have stopped trying to make one runtime do both jobs. The front — rendering, hydration, the browser-facing edge — runs where the component model lives. The back — persistence, business rules, anything long-running — runs on something compiled, with a predictable memory ceiling and real concurrency. The two talk over a narrow, typed seam, and each is allowed to be good at one thing.&lt;/p&gt;

&lt;p&gt;It is less novel than it sounds. It is how most serious systems were built before "JavaScript everywhere" made a single runtime feel like a simplification. It was never one. It was a deferral, and the nightly restart is where the interest gets paid.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>Persistent state in React and Next.js, without a store</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/persistent-state-in-react-and-nextjs-without-a-store-2g4k</link>
      <guid>https://dev.to/luigidavidemicca/persistent-state-in-react-and-nextjs-without-a-store-2g4k</guid>
      <description>&lt;p&gt;Somewhere in every React codebase there's a hook called &lt;code&gt;useLocalStorage&lt;/code&gt;, written in ten minutes, copied from a gist, and trusted with the theme, the auth token and half the checkout. I've audited enough front-ends to state this as a law of nature.&lt;/p&gt;

&lt;p&gt;Here's what that innocent hook is actually signing up for — and why "persistence" is a real engineering problem, not a &lt;code&gt;useEffect&lt;/code&gt; afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six lines everyone writes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useLocalStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;T&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="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'null'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;
  &lt;span class="p"&gt;)&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="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&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="n"&gt;value&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="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setValue&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Five production problems, in ascending order of how late they bite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Next.js kills it on sight.&lt;/strong&gt; &lt;code&gt;localStorage&lt;/code&gt; doesn't exist on the server — and even guarded, reading it during render makes the first client render differ from the server HTML — hello, hydration mismatch warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrupt data throws.&lt;/strong&gt; Storage is user-editable and outlives your deploys. One malformed entry and &lt;code&gt;JSON.parse&lt;/code&gt; takes the component tree down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components disagree.&lt;/strong&gt; Two components using the same key each hold their own &lt;code&gt;useState&lt;/code&gt; copy: change one, the other doesn't move. Most teams "fix" this with Context — a provider, for a string in storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs disagree, values rot.&lt;/strong&gt; No &lt;code&gt;storage&lt;/code&gt; event handling, no expiry: last month's draft and yesterday's token are treated as fresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write storms.&lt;/strong&gt; Persisting a text input on every keystroke hammers a synchronous main-thread API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic. It's the same checklist, in every app, usually rediscovered one incident at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist, packaged
&lt;/h2&gt;

&lt;p&gt;This is the itch behind &lt;a href="https://github.com/LuigiDavideMicca/smart-state" rel="noopener noreferrer"&gt;&lt;code&gt;smart-state&lt;/code&gt;&lt;/a&gt;, a small open-source hook I maintain (~3 kB min+gzip, zero dependencies — full disclosure, it's mine):&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="n"&gt;useSmartState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'smart-state'&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="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;useSmartState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'light'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'dark'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'light'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'theme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;syncTabs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;Drop-in &lt;code&gt;useState&lt;/code&gt; semantics — lazy initializer, functional updater — with the whole checklist handled: SSR-safe hydration (no mismatch warnings), shared state across every component using the key &lt;em&gt;without a provider&lt;/em&gt; (it's built on &lt;code&gt;useSyncExternalStore&lt;/code&gt;), cross-tab sync, and error handling that degrades instead of throwing.&lt;/p&gt;

&lt;p&gt;The production details are options, not rewrites:&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="c1"&gt;// Storage is untrusted input: validate it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;useSmartState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// zod, valibot, anything that throws&lt;/span&gt;
  &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&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;from&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;upgradeV1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A session that expires on its own&lt;/span&gt;
&lt;span class="nf"&gt;useSmartState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="c1"&gt;// A draft that doesn't hammer storage on every keystroke&lt;/span&gt;
&lt;span class="nf"&gt;useSmartState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writeDebounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&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 &lt;code&gt;parse&lt;/code&gt;/&lt;code&gt;version&lt;/code&gt;/&lt;code&gt;migrate&lt;/code&gt; trio is the part I care most about: persisted data crosses your release boundaries, so it deserves the same suspicion as any external input. Very few libraries treat it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Shouldn't this just be Zustand?"
&lt;/h2&gt;

&lt;p&gt;Honesty corner: if your app has genuinely complex shared state — many writers, derived data, devtools time-travel — a store like Zustand or Redux Toolkit earns its place, and &lt;code&gt;persist&lt;/code&gt; middleware exists. But adopting a store &lt;em&gt;because you need localStorage&lt;/em&gt; is backwards: you're taking on an architecture to get a side effect. For the theme, the filters, the draft and the cart, a persistence hook is the right size — and at about 3 kB it costs less than the store's readme.&lt;/p&gt;

&lt;p&gt;It's the same lesson I keep repeating in &lt;a href="https://luigimicca.com/en/services/technical-consulting" rel="noopener noreferrer"&gt;architecture reviews&lt;/a&gt;: name the actual requirement first. "Sharing", "persisting", "syncing" and "caching" are four different problems that all get called "state management" in meetings.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm an independent software architect focused on front-end work — smart-state has siblings for Vue and Nuxt that share its storage format. If your React codebase has three hand-rolled useLocalStorage hooks and a hydration warning nobody investigates, &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: the intro call is free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>nextjs</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The European Accessibility Act is being enforced. Is your website in scope?</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/the-european-accessibility-act-is-being-enforced-is-your-website-in-scope-1aj0</link>
      <guid>https://dev.to/luigidavidemicca/the-european-accessibility-act-is-being-enforced-is-your-website-in-scope-1aj0</guid>
      <description>&lt;p&gt;For years, web accessibility in Europe lived in a comfortable limbo: everyone agreed it mattered, almost nobody was forced to act. That limbo ended on 28 June 2025, when the European Accessibility Act became applicable — and 2026 is turning out to be the year the machinery actually starts moving. Market surveillance authorities are running inspections, advocacy groups are filing complaints, law firms are sending warning letters, and the first lawsuits were filed in France in November 2025. The fines are on the books even though none has been made public yet: Germany allows fines of up to €100,000 per violation; in France they range from €5,000 to €250,000, plus €25,000 per year for a missing accessibility statement.&lt;/p&gt;

&lt;p&gt;If you sell to EU consumers, this is now an engineering deadline that has already passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is actually in scope
&lt;/h2&gt;

&lt;p&gt;The EAA covers businesses with 10 or more employees or more than €2 million in annual turnover that sell covered products or services to EU consumers. In practice, that includes most of the web that takes money: e-commerce shops, banking and payment flows, booking systems, transport ticketing, consumer SaaS.&lt;/p&gt;

&lt;p&gt;Microenterprises providing services are formally exempt — but don't relax yet: if a larger company is your client, their compliance obligations flow down into what you supply them. I've already seen accessibility clauses appear in procurement checklists the same way GDPR clauses did in 2018.&lt;/p&gt;

&lt;h2&gt;
  
  
  What compliance actually means
&lt;/h2&gt;

&lt;p&gt;The standard behind the law is EN 301 549, which in web terms boils down to WCAG at level AA. Strip away the acronyms and it's a concrete engineering checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard.&lt;/strong&gt; Every flow — menu, filters, checkout — works without a mouse, with a visible focus indicator and no traps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forms.&lt;/strong&gt; Real labels, and error messages that say what to fix, not just "invalid input".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast and text.&lt;/strong&gt; Sufficient contrast ratios, text that scales to 200% without breaking the layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure.&lt;/strong&gt; Semantic HTML: headings that outline the page, buttons that are buttons, links that are links. Screen readers navigate structure, not pixels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media.&lt;/strong&gt; Alt text that describes function, captions on videos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A published accessibility statement&lt;/strong&gt; and a feedback channel — the absence of this document is the single easiest violation for a regulator to spot, and in France it has its own recurring fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. It's the same discipline as performance work: measurable, testable and mostly invisible when done right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honesty corner: the overlay shortcut
&lt;/h2&gt;

&lt;p&gt;There's a whole industry selling one-line JavaScript widgets that promise compliance overnight. They don't deliver it. Overlays can't fix missing labels, broken keyboard flows or divs pretending to be buttons — and several enforcement actions and lawsuits have explicitly named sites &lt;em&gt;with&lt;/em&gt; overlays installed. If a vendor promises EAA compliance for €49 a month and one script tag, you're buying a false sense of security, and the regulator knows it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upside nobody mentions
&lt;/h2&gt;

&lt;p&gt;Accessibility work overlaps almost perfectly with things you already want. Semantic structure is what search engines parse. The faster, lighter pages it pushes you towards help everyone. Clear forms with described errors convert better for everyone, not just for the roughly 87 million people in the EU living with some form of disability — a market larger than Germany.&lt;/p&gt;

&lt;p&gt;In my experience, teams that treat accessibility as a quality attribute — like performance, like security — end up with better front-ends across the board. Teams that treat it as a box-ticking exercise do it twice: once badly, once properly after the complaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd start
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit honestly.&lt;/strong&gt; Automated scanners catch maybe a third of real issues; the rest requires a human with a keyboard and a screen reader walking your critical flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritise by user impact.&lt;/strong&gt; A broken checkout matters more than a decorative image in the footer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix in normal sprints.&lt;/strong&gt; Most fixes are small; the expensive part is finding the problems, not fixing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the statement&lt;/strong&gt; and wire accessibility checks into your definition of done, so the work doesn't rot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The EAA didn't invent any of this — it just attached a price tag to ignoring it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm an independent software architect focused on front-end work, and accessibility reviews are part of every &lt;a href="https://luigimicca.com/en/services/technical-consulting" rel="noopener noreferrer"&gt;audit I run&lt;/a&gt;. If you're not sure where your site stands, &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: a 30-minute intro call is free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>accessibility</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Persistent state in Vue 3, without reaching for Pinia</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/persistent-state-in-vue-3-without-reaching-for-pinia-3k64</link>
      <guid>https://dev.to/luigidavidemicca/persistent-state-in-vue-3-without-reaching-for-pinia-3k64</guid>
      <description>&lt;p&gt;Every Vue app eventually grows a handful of values that must outlive a page reload: the chosen theme, a search filter, a half-written form, a cart. And every team eventually has the same conversation about them: "should we add Pinia for this?"&lt;/p&gt;

&lt;p&gt;Usually, no. A store solves &lt;em&gt;shared&lt;/em&gt; state; these are &lt;em&gt;persistent&lt;/em&gt; values — and gluing persistence onto a store still leaves you writing the same plumbing. Let's look at what that plumbing actually involves, because the details are where the bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hand-rolled version (and its traps)
&lt;/h2&gt;

&lt;p&gt;The naive approach fits in six lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'vue'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'theme'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'"light"'&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="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'theme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&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="n"&gt;value&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="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;It works — on your machine, today. In production it has at least five holes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SSR crashes.&lt;/strong&gt; On Nuxt or any server render, &lt;code&gt;localStorage&lt;/code&gt; doesn't exist. First hydration error of the sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrupt data throws.&lt;/strong&gt; One malformed value in storage (an old release, an extension, a curious user) and &lt;code&gt;JSON.parse&lt;/code&gt; takes your component down with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs disagree.&lt;/strong&gt; Change the theme in one tab and the other five keep the old one until reload. The &lt;code&gt;storage&lt;/code&gt; event fixes it — if you remember it, and remember to remove the listener — otherwise you've built a memory leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale values live forever.&lt;/strong&gt; A cached token, a draft from last month: without an expiry, storage becomes a junk drawer your app trusts blindly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write storms.&lt;/strong&gt; Persisting a text input on every keystroke hammers a synchronous API on the main thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these is hard. All of them together, done in every component that needs them, is exactly the kind of code that grows five slightly different implementations in one codebase — I audit front-ends for a living, and I can promise you it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;This is the itch behind &lt;a href="https://github.com/LuigiDavideMicca/vue-smart-state" rel="noopener noreferrer"&gt;&lt;code&gt;vue-smart-state&lt;/code&gt;&lt;/a&gt;, a small open-source composable I maintain (~2.4 kB, zero dependencies — full disclosure, this is my package):&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="n"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'vue-smart-state'&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="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'light'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'dark'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'light'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'theme'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;syncTabs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;One call gives you the whole checklist: restore on load, persist on change, cross-tab sync with proper listener cleanup, SSR safety, and error handling that degrades instead of throwing. The API deliberately mirrors React's &lt;code&gt;useState&lt;/code&gt; — a tuple plus a functional updater (&lt;code&gt;setTheme(t =&amp;gt; …)&lt;/code&gt;) — because half the developers joining a Vue team carry that muscle memory with them.&lt;/p&gt;

&lt;p&gt;The production details are options away:&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="c1"&gt;// A session that expires on its own&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A draft that doesn't hammer storage on every keystroke&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;writeDebounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&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="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;And for Nuxt there's a one-liner module, &lt;a href="https://github.com/LuigiDavideMicca/nuxt-smart-state" rel="noopener noreferrer"&gt;&lt;code&gt;nuxt-smart-state&lt;/code&gt;&lt;/a&gt;, that auto-imports the composable as &lt;code&gt;useSmartState&lt;/code&gt; — aliased on purpose, since Nuxt's own &lt;code&gt;useState&lt;/code&gt; solves a different problem (per-request server state, not browser persistence).&lt;/p&gt;

&lt;h2&gt;
  
  
  When you &lt;em&gt;do&lt;/em&gt; want a store
&lt;/h2&gt;

&lt;p&gt;Honesty corner: if many distant components mutate the same complex state, if you need devtools time-travel, or if your team benefits from one blessed pattern for app-wide data — use Pinia — it's excellent. The point isn't "stores are bad"; it's that &lt;strong&gt;persistence is not a reason to adopt one&lt;/strong&gt;. Pick the store for sharing, pick a persistence composable for persisting, and let each do its job.&lt;/p&gt;

&lt;p&gt;The wider lesson is one I keep repeating in &lt;a href="https://luigimicca.com/en/services/technical-consulting" rel="noopener noreferrer"&gt;architecture work&lt;/a&gt;: most "we need a state management solution" conversations dissolve once you name the actual requirement. Sharing, persisting, syncing and caching are four different problems wearing the same T-shirt.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm an independent software architect focused on front-end work — and I occasionally package the boring parts well, like the composable above. If your Vue codebase has grown five ways of doing the same thing, &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: a 30-minute intro call is free.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your website's speed is a business number</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/your-websites-speed-is-a-business-number-16kp</link>
      <guid>https://dev.to/luigidavidemicca/your-websites-speed-is-a-business-number-16kp</guid>
      <description>&lt;p&gt;Nobody's website is slow on the office Wi-Fi, on a developer's laptop, freshly cached. It's slow on a three-year-old Android, on mobile data, held by someone who has never visited before — which is to say: it's slow exactly where your next customer lives.&lt;/p&gt;

&lt;p&gt;I've spent years making storefronts and web apps fast, and the pattern is always the same: speed is treated as a technical nicety until someone puts it next to a revenue number. Then it becomes urgent. Let's skip to that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tax you don't see on any invoice
&lt;/h2&gt;

&lt;p&gt;Page speed charges you three times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conversion.&lt;/strong&gt; The relationship is brutally consistent across every study and every project I've measured: as load time grows from one second to three, a meaningful share of visitors simply leaves. On paid traffic this is money burned twice — you paid for the click &lt;em&gt;and&lt;/em&gt; for the bounce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO.&lt;/strong&gt; Core Web Vitals are a ranking signal, and more importantly they gate how Google's crawler experiences you. You rarely see a "penalty"; you see competitors quietly outranking you on queries you used to own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perception.&lt;/strong&gt; A site that responds instantly reads as competent. One that stutters reads as neglected — and visitors extend that judgement to the business behind it. Unfair, universal, measurable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure it like Google does, not like your laptop does
&lt;/h2&gt;

&lt;p&gt;The single most common mistake: judging speed by how the site feels to you, or by a lab score run from a fast machine. Two rules fix this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use field data first.&lt;/strong&gt; Google publishes what real Chrome users actually experienced on your site — the Chrome UX Report, surfaced in PageSpeed Insights under "Discover what your real users are experiencing". That's the number that ranks you, and the number that matches your customers' reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch three metrics.&lt;/strong&gt; LCP (when the main content appears — aim under 2.5s), INP (how fast the page reacts to taps and clicks — under 200ms), CLS (how much the layout jumps around — under 0.1). Everything else is diagnostics; these three are the verdict.&lt;/p&gt;

&lt;p&gt;If your field data is green, congratulations — close this tab. If it's amber or red on mobile, that's not a technical detail. That's the tax from the previous section, running daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moves the needle
&lt;/h2&gt;

&lt;p&gt;After years of performance work, the honest ranking of fixes by impact-per-euro:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Images.&lt;/strong&gt; Still the number one offender on most sites: oversized, unoptimised, loaded all at once. Modern formats, correct sizes per device and lazy loading below the fold routinely cut page weight by more than half. Boring, effective, cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript.&lt;/strong&gt; Every marketing tag, chat widget and carousel library rides on your customer's CPU. The fastest JavaScript is the JavaScript you delete; what remains should load after the content, not before it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts.&lt;/strong&gt; Two families, the weights you actually use, self-hosted and preloaded. Fonts are why so many sites flash, jump or show invisible text for a full second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The architecture.&lt;/strong&gt; The ceiling on everything above. A page that must consult a slow server on every visit starts losing before the first byte arrives; a page served pre-built from a CDN starts from near zero. This is the difference you decide once, when the site is built — which is why speed is cheapest on day one and priciest as a retrofit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; high on the list: switching hosting providers, "performance plugins", and most of what gets sold as a quick fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable summary
&lt;/h2&gt;

&lt;p&gt;A slow site is rarely slow for mysterious reasons. It's slow because speed was nobody's requirement — not in the brief, not in the contract, not in the acceptance checklist. The fix starts there: write it down. "Core Web Vitals green in field data, on mobile" is one line in a project brief, and it changes every technical decision made after it.&lt;/p&gt;

&lt;p&gt;(This site loads in well under a second on a mid-range phone. Not because it's small — because that line was in its brief.)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build websites and web applications with performance budgets written into the contract, and I audit existing sites that have grown slow. If your field data is amber — or you've never looked — &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: a 30-minute intro call is free, and I'll tell you honestly whether it's a one-day fix or an architecture problem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Taming a front-end codebase that grew too fast</title>
      <dc:creator>Luigi Davide Micca</dc:creator>
      <pubDate>Sat, 04 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/luigidavidemicca/taming-a-front-end-codebase-that-grew-too-fast-pj6</link>
      <guid>https://dev.to/luigidavidemicca/taming-a-front-end-codebase-that-grew-too-fast-pj6</guid>
      <description>&lt;p&gt;There is a specific kind of codebase that only successful products have. It shipped fast, it won customers, and every sprint someone bolted a new feature onto whatever was nearest. Three years later the product is thriving — and the front-end is a haunted house. Every developer has a wing they refuse to enter.&lt;/p&gt;

&lt;p&gt;If that sounds familiar: congratulations, and my condolences. Here is the playbook I use when a team calls me in at that exact moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, name the actual problem
&lt;/h2&gt;

&lt;p&gt;"Tech debt" is a useless diagnosis — it points everywhere and therefore nowhere. In practice, fast-grown front-ends almost always hurt in a handful of specific places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State lives everywhere.&lt;/strong&gt; Server cache, global store, component state and the URL all disagree about what is true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components do too much.&lt;/strong&gt; The 900-line component that fetches, transforms, renders and navigates — and is imported in eleven places.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patterns diverge by author.&lt;/strong&gt; Three ways to call the API, four ways to handle errors, two competing folder structures — depending on who wrote it and in which year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody trusts a change.&lt;/strong&gt; No tests where it matters, so every release is a small act of courage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spend a week measuring instead of refactoring: where do bugs cluster? Which files change in every single feature? That map — change frequency × defect density — tells you where the leverage is. It is usually 4–6 files, not four hundred.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules of engagement
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No rewrite.&lt;/strong&gt; A rewrite trades problems you know for problems you don't, and freezes the roadmap while you rediscover them. The business that funded your growth will not fund a year of "nothing new, we promise it's better inside".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The boy-scout rule is not a strategy.&lt;/strong&gt;"Leave it better than you found it" produces random acts of tidiness. Improvement has to follow the leverage map, not the developer's mood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every step ships.&lt;/strong&gt; If a refactor can't reach production independently, it's too big. Cut it down until it can.&lt;/p&gt;

&lt;h2&gt;
  
  
  The playbook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Freeze the bleeding
&lt;/h3&gt;

&lt;p&gt;Before improving anything, stop making it worse. Agree on &lt;em&gt;one&lt;/em&gt; blessed pattern for the big four — data fetching, state, error handling, folder structure — and enforce it on &lt;strong&gt;new code only&lt;/strong&gt; , in review and with lint rules. The old code stays untouched for now. This costs almost nothing and immediately splits the codebase into "legacy" and "the way out".&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build the paved road
&lt;/h3&gt;

&lt;p&gt;Take the most common thing your team builds — a list-plus-detail screen, a form flow — and create the reference implementation: typed API layer, error boundaries, loading states, tests. Not a wiki page. Working code in the repo that people copy. A pattern that isn't easier to follow than to ignore will be ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Strangle by traffic, not by tidiness
&lt;/h3&gt;

&lt;p&gt;Now walk the leverage map. The high-churn, high-defect modules get rebuilt onto the paved road first — one at a time, each shipped to production behind the same URL. Low-churn ugly code &lt;em&gt;stays ugly&lt;/em&gt;. It has earned the right: it works and nobody touches it. Aesthetic refactoring of stable code is how modernisation budgets die.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make the trend visible
&lt;/h3&gt;

&lt;p&gt;Pick two or three numbers the whole team can see move: type coverage, bundle size, defects per release, time-to-merge. Post them monthly. Teams that watch a trendline defend it; teams that don't watch it slide back within a quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in real life
&lt;/h2&gt;

&lt;p&gt;On a large e-commerce storefront whose front-end team I led, we did exactly this: standards enforced in review from week one, strict TypeScript as the paved road, and the core catalogue components — the highest-churn area of the codebase — rebuilt incrementally while the release train kept running weekly. No feature freeze, no big-bang branch, and within two quarters the team was measurably faster &lt;em&gt;and calmer&lt;/em&gt;. The second part matters more than it sounds: a team that stops fearing its own codebase makes better decisions everywhere else too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest timeline
&lt;/h2&gt;

&lt;p&gt;For a mid-size product front-end, expect the turnaround to be a &lt;strong&gt;quarters game, not a weeks game&lt;/strong&gt; — but with the curve bending early: the bleeding stops in week one, the paved road exists within a month, and the first high-leverage module ships soon after. If someone promises you a transformed codebase in three weeks, they are describing a demo.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I work as a software architect with a front-end focus — modernising products that grew faster than their code, without stopping them. If your front-end has become the part of the roadmap everyone routes around, &lt;a href="https://luigimicca.com/en/contact" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;: a 30-minute intro call is free and usually clarifying.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
