<?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: Richard Lemon</title>
    <description>The latest articles on DEV Community by Richard Lemon (@richardlemon).</description>
    <link>https://dev.to/richardlemon</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%2F3798270%2F7fc64f22-b7f0-471f-9ac1-e15050494121.jpeg</url>
      <title>DEV Community: Richard Lemon</title>
      <link>https://dev.to/richardlemon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richardlemon"/>
    <language>en</language>
    <item>
      <title>The Accessibility Audit That Slapped Me Awake</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:16:17 +0000</pubDate>
      <link>https://dev.to/richardlemon/the-accessibility-audit-that-slapped-me-awake-2e1o</link>
      <guid>https://dev.to/richardlemon/the-accessibility-audit-that-slapped-me-awake-2e1o</guid>
      <description>&lt;h2&gt;The setup: I thought I was "pretty good" at a11y&lt;/h2&gt;

&lt;p&gt;I shipped a client site earlier this year that I was genuinely proud of. Fast, clean, nice motion, sensible structure. I had aria labels sprinkled in. Focus states visible. Color contrast checked with a browser extension.&lt;/p&gt;

&lt;p&gt;I walked around thinking: this is solid. Not perfect, but above average. Better than the usual marketing-page glitter I see launch on Product Hunt.&lt;/p&gt;

&lt;p&gt;The client then told me they had booked an external accessibility audit. Corporate policy. Third-party vendor. Full WCAG report.&lt;/p&gt;

&lt;p&gt;I said "nice, happy to collaborate". What I meant was "sure, let them rubber-stamp my genius".&lt;/p&gt;

&lt;p&gt;Yeah. That did not happen.&lt;/p&gt;

&lt;h2&gt;The report that wrecked my ego&lt;/h2&gt;

&lt;p&gt;The PDF landed in my inbox on a Thursday. 43 pages. That already hurt. The executive summary used the phrase "significant barriers" twice, which hurt more.&lt;/p&gt;

&lt;p&gt;I did what every developer does with long reports. Scanned for screenshots of my worst sins first. There they were. Highlight annotations. Red circles. Yellow boxes. My UI looked like a football coach diagram.&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable bit. Nothing in that report was "clever". No edge-case academic stuff. It was all basic things I should have caught.&lt;/p&gt;

&lt;p&gt;I am going to walk through the worst ones and how they changed my process, because theory-level accessibility feels abstract. Getting slapped with real user failures is not.&lt;/p&gt;

&lt;h2&gt;1. My fancy focus trap was an actual trap&lt;/h2&gt;

&lt;p&gt;The site had a beautiful modal. Blurred background, springy animation, keyboard trap implemented with a tiny utility I had used before. I was proud of it.&lt;/p&gt;

&lt;p&gt;The auditor flagged it as a critical issue.&lt;/p&gt;

&lt;p&gt;During keyboard navigation tests the focus would enter the modal, cycle through the fields, but the close button was &lt;em&gt;not&lt;/em&gt; reachable with Tab. Why? Because I had added a little micro-interaction that hid the close label visually on small screens and replaced it with an icon-only button.&lt;/p&gt;

&lt;p&gt;The button itself was still there, but my focus ring was styled only for &lt;code&gt;button:focus-visible&lt;/code&gt;. On that element I had a weird outline offset. Combined with the layout, the visible outline ended up outside the viewport on some sizes.&lt;/p&gt;

&lt;p&gt;So technically the element got focus. Practically the user saw &lt;strong&gt;nothing&lt;/strong&gt;. It looked frozen.&lt;/p&gt;

&lt;p&gt;What I had never done before, and what the auditor did, was this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Turn off the mouse completely.&lt;/li&gt;
  &lt;li&gt;Navigate the entire site with Tab and Shift+Tab at 1x speed. No cheating, no skipping.&lt;/li&gt;
  &lt;li&gt;Try to &lt;em&gt;escape&lt;/em&gt; every overlay, dialog, and menu with only the keyboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I repeated that, slowly, I realised how brittle my focus handling really was. It worked in my happy path short test. It fell apart when you used it like a real person who is stuck inside a component.&lt;/p&gt;

&lt;p&gt;Change in my workflow: I now do a "keyboard only" pass on every feature. I literally put my trackpad out of reach and try to get stuck. If I can trap myself, I fix it before launch.&lt;/p&gt;

&lt;h2&gt;2. Screen reader order did not match the visual order&lt;/h2&gt;

&lt;p&gt;The homepage hero was a pretty typical pattern. Left column had a headline, paragraph, CTA buttons. Right column had a product mockup with some floating badges.&lt;/p&gt;

&lt;p&gt;I had built the layout using CSS grid and some reordering. For mobile I wanted the image first visually, so I used &lt;code&gt;order&lt;/code&gt; on flex items in one breakpoint. It looked great.&lt;/p&gt;

&lt;p&gt;The auditor ran this through NVDA and VoiceOver. Their note was blunt. The reading order was: image, decorative badge, secondary badge, then suddenly footer navigation, then only after that the main hero copy.&lt;/p&gt;

&lt;p&gt;I had wrapped the visual stuff in too many nested containers. The DOM structure was not in the logical content order. I relied on CSS to rearrange what the user saw, but the assistive tech still obeyed the DOM.&lt;/p&gt;

&lt;p&gt;This is the kind of bug you do not catch with Lighthouse or simple checklists. You have to actually turn on a screen reader and listen to your page. Like a podcast of your mistakes.&lt;/p&gt;

&lt;p&gt;Change in my workflow: I now treat DOM order as the source of truth. If I have to reorder visually, I pause and ask why. Can I instead structure the HTML in the same order as the content should be consumed, and use layout only for spacing and alignment?&lt;/p&gt;

&lt;p&gt;Most of the time, yes. My old approach was just lazy. Flexbox gave me a hammer and I reordered everything.&lt;/p&gt;

&lt;h2&gt;3. Low-contrast on active states, not static ones&lt;/h2&gt;

&lt;p&gt;I had checked the color palette with a standard 4.5:1 AA contrast checker. Base text, buttons, links. All fine.&lt;/p&gt;

&lt;p&gt;The auditor did not just check static UI. They checked states. That part annoyed me, because I knew they were right and I had never actually done it properly.&lt;/p&gt;

&lt;p&gt;On hover I darkened some buttons. On focus I lightened outlines. On form fields I added a subtle colored border and a glow when active. Subtle was the problem.&lt;/p&gt;

&lt;p&gt;On one primary button, the normal state contrast was 4.7:1. The hover state dropped to 3.2:1 because I loved how that slightly softer shade looked against the background image.&lt;/p&gt;

&lt;p&gt;Looks great to me. Useless if you have low vision and rely on the difference between states to know where you are.&lt;/p&gt;

&lt;p&gt;The same happened with inline error messages. Neutral grey text that turned red on error. That red had worse contrast against the light background than the original grey did, especially in the small labels.&lt;/p&gt;

&lt;p&gt;Change in my workflow: I started checking contrast &lt;strong&gt;per state&lt;/strong&gt;. Normal, hover, focus, disabled. If a designer gives me a Figma file with 9 different button variants, I run the text color vs background for each one.&lt;/p&gt;

&lt;p&gt;It is boring. It also exposed that some of the prettiest variants were the most hostile to real users. We adjusted the palette. The world kept turning. Nobody complained that the shade of red was not "brand authentic".&lt;/p&gt;

&lt;h2&gt;4. Interactive elements pretending to be divs&lt;/h2&gt;

&lt;p&gt;This one was straight up laziness. I thought I had grown out of custom clickable divs. Apparently not.&lt;/p&gt;

&lt;p&gt;We had a row of feature cards. Clickable, nice hover animation, whole card usable as a hit area. I built it with a &lt;code&gt;&amp;lt;div role="button"&amp;gt;&lt;/code&gt; wrapped around some content, added a &lt;code&gt;click&lt;/code&gt; handler, and made it submit a filter.&lt;/p&gt;

&lt;p&gt;The auditor flagged it as a keyboard trap because you could Tab into the card, press Enter, nothing happened. Space triggered click, Enter did not. Also there was no semantic connection between that "button" and the filter results that updated below. No ARIA live region, no announcement of the change.&lt;/p&gt;

&lt;p&gt;The simple fix: use a real &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;. Or a &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; link if it navigates. Let the browser give you the semantics, keyboard handling, and basic announcements for free.&lt;/p&gt;

&lt;p&gt;I know this. You know this. We all still ship rogue divs under time pressure.&lt;/p&gt;

&lt;p&gt;Change in my workflow: I now scan my own code for &lt;code&gt;role="button"&lt;/code&gt; during review. If I see it, I treat it as a smell. Ninety percent of the time, it is just a button that should have been a button.&lt;/p&gt;

&lt;h2&gt;5. Hidden labels behind clever UI&lt;/h2&gt;

&lt;p&gt;The search input in the header was the part of the design I felt most smug about. Minimal placeholder text, small icon, expands nicely on focus. Very tidy.&lt;/p&gt;

&lt;p&gt;The auditor noted that the input had no accessible name. I had hidden the visual label and not actually left a label element tied to the input with &lt;code&gt;for&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had gone for the quick hack: placeholder as label. Screen readers treat that poorly, and placeholder text vanishes once you type. For someone using dictation or a screen reader, that label is how you know what the field is for when you come back to it.&lt;/p&gt;

&lt;p&gt;They showed me a screen reader log that literally read out: "Edit text, blank". No indication that it was a product search.&lt;/p&gt;

&lt;p&gt;Change in my workflow: now every input has a label. If the design wants it hidden, I use a visually hidden utility class, not an absent label. I also stopped using placeholder text as the only instruction. It is now, at best, secondary.&lt;/p&gt;

&lt;h2&gt;6. Motion that cannot be turned off&lt;/h2&gt;

&lt;p&gt;This one hurt because I love motion. Micro interactions are my candy. I spend time tuning easing curves and stagger timings.&lt;/p&gt;

&lt;p&gt;The site had a scroll-triggered animation sequence. Elements sliding in, fading, parallax on hero images. It all respected &lt;code&gt;prefers-reduced-motion&lt;/code&gt; in theory. Or so I thought.&lt;/p&gt;

&lt;p&gt;I had wrapped the main animations in a media query that checks for reduced motion. Cool. Except we later added a third-party library for one section that did not care about the user preference.&lt;/p&gt;

&lt;p&gt;The auditor tested with reduced motion set on the OS. The site still threw in a bunch of autoplaying motion as you scrolled through that section. Annoying for some people. Nausea-inducing for others.&lt;/p&gt;

&lt;p&gt;Change in my workflow: I now test with reduced motion enabled on my own machine at least once per feature. Not just trust my CSS. If I bring in a library, I check how to disable or tame its animations. If I cannot, I reconsider using it.&lt;/p&gt;

&lt;h2&gt;7. Error handling that assumes everyone can see red&lt;/h2&gt;

&lt;p&gt;Form errors were my silent shame. They worked visually. Red text below the field. Little icon. Subtle shake animation on submit.&lt;/p&gt;

&lt;p&gt;The auditor tested with color blindness simulation and with a screen reader. Two problems appeared immediately.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Color was the only indicator for error state. No icon or pattern or text like "Error:".&lt;/li&gt;
  &lt;li&gt;Focus did not move to the first invalid field when the form failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Screen reader users hit submit, heard "Form submission failed", then nothing helpful. They had to hunt manually for what went wrong.&lt;/p&gt;

&lt;p&gt;The fix was mechanical. Wrap errors in &lt;code&gt;aria-live="polite"&lt;/code&gt; regions, move focus to the first error, add a clear text label like "Error: Email is required" instead of a vague "This field is required".&lt;/p&gt;

&lt;p&gt;Change in my workflow: I now test form failure states with a keyboard and with the dev tools color blindness filters. If I cannot find the error instantly without relying on red, it is not good enough.&lt;/p&gt;

&lt;h2&gt;What changed permanently in my process&lt;/h2&gt;

&lt;p&gt;The biggest lesson was not a single bug. It was the realisation that my informal "I know the basics" attitude created invisible cliffs for actual people.&lt;/p&gt;

&lt;p&gt;Here is what stuck and what I now do almost by default on client work.&lt;/p&gt;

&lt;h3&gt;1. One manual pass per feature&lt;/h3&gt;

&lt;p&gt;Every major feature now gets a dedicated accessibility pass before I call it done. Ten to fifteen minutes. Keyboard only. Reduced motion on. Screen reader on for the key flows.&lt;/p&gt;

&lt;p&gt;It is not a full audit. It is enough to catch the worst sins before an external auditor has to embarrass me again.&lt;/p&gt;

&lt;h3&gt;2. DOM order first, layout second&lt;/h3&gt;

&lt;p&gt;I stopped using &lt;code&gt;order&lt;/code&gt; and clever grid tricks to rearrange content just to hit a visual spec. I talk to designers earlier now.&lt;/p&gt;

&lt;p&gt;If the Figma layout forces weird DOM gymnastics, I push back. Not aggressively. Just with a clear explanation: "If we do it like this, screen readers will read the footer before the main content." That line works every time.&lt;/p&gt;

&lt;h3&gt;3. No more divs pretending to be controls&lt;/h3&gt;

&lt;p&gt;This became a hard rule for myself. If it is clickable and not a drag handle or some weird canvas thing, it must be a real button or a link.&lt;/p&gt;

&lt;p&gt;I am fine with extra wrappers for styling. I am not fine with rebuilding basic semantics from scratch because I am chasing a tiny CSS convenience.&lt;/p&gt;

&lt;h3&gt;4. States, not just components&lt;/h3&gt;

&lt;p&gt;Design systems love showing the perfect, static state of a component. Accessibility issues show up in the in-between states. Hover, active, loading, error.&lt;/p&gt;

&lt;p&gt;So I started building a little "state story" for important components. Button in all states, form in success and failure, dialog open and closing. Then I run through them with keyboard and screen reader.&lt;/p&gt;

&lt;h2&gt;The humility piece&lt;/h2&gt;

&lt;p&gt;Getting that audit back was not fun. It made me feel like a junior again. I had shipped a polished-looking site that still put real barriers in front of real users.&lt;/p&gt;

&lt;p&gt;On the other hand, that discomfort did something useful. It pushed accessibility out of the "nice to have" mental category and into "part of being a competent frontend dev" for me.&lt;/p&gt;

&lt;p&gt;I do not think you become good at this by reading long guidelines. You become good by having someone else try to use your work while you watch your assumptions crumble.&lt;/p&gt;

&lt;p&gt;If you have never had an external audit on something you are proud of, I recommend it. Not because it feels good. Because it will flatten the ego you have built around "I know the basics" and replace it with actual, practical habits.&lt;/p&gt;

&lt;p&gt;I know mine did.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why I Still Test In Safari First (And The Bugs It Catches Early)</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:14:58 +0000</pubDate>
      <link>https://dev.to/richardlemon/why-i-still-test-in-safari-first-and-the-bugs-it-catches-early-58oh</link>
      <guid>https://dev.to/richardlemon/why-i-still-test-in-safari-first-and-the-bugs-it-catches-early-58oh</guid>
      <description>&lt;h2&gt;Yes, I Still Open Safari First&lt;/h2&gt;

&lt;p&gt;I build and test new frontend work in Safari first. On purpose. Repeatedly. Sober.&lt;/p&gt;

&lt;p&gt;I know that sounds backwards. Everyone else ships for Chrome, patches for Firefox, then grudgingly opens Safari when QA files a bug with a YouTube link and zero details.&lt;/p&gt;

&lt;p&gt;I used to work like that too. Then I got tired of having my nicest builds break on the client’s actual devices. Which were almost always iPhones and iPads running whatever Safari Apple felt like shipping that month.&lt;/p&gt;

&lt;p&gt;So I flipped my workflow. Safari first, Chrome second, everything else after. It feels contrarian, but it has paid for itself in stress avoided and bugs caught before they became someone else’s problem.&lt;/p&gt;

&lt;h2&gt;Why Safari Is A Useful Enemy&lt;/h2&gt;

&lt;p&gt;Safari is stubborn. It sticks to specs when Chrome is “helpful”. It ships stuff slower. It has some… opinions.&lt;/p&gt;

&lt;p&gt;I treat that as a feature, not a flaw. Safari is a strict teacher. If something feels fragile in Safari, it usually &lt;em&gt;is&lt;/em&gt; fragile. Chrome just hides it with sugar and duct tape.&lt;/p&gt;

&lt;p&gt;Rough pattern I see:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Chrome: “Looks fine.”&lt;/li&gt;
  &lt;li&gt;Firefox: “That’s technically wrong but I will try.”&lt;/li&gt;
  &lt;li&gt;Safari: “You broke the rules. No.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That “No” is valuable. I want to hear it on day one, on my own machine, not in a client email that starts with “so we noticed on mobile…”&lt;/p&gt;

&lt;h2&gt;A Real Example: The Layout That Only Broke On iPad&lt;/h2&gt;

&lt;p&gt;A while back I shipped a fancy editorial layout. CSS Grid, variable fonts, a bit of scroll-linked animation. It looked beautiful in Chrome. Pixel perfect in Firefox. I was happy.&lt;/p&gt;

&lt;p&gt;Then I opened it in Safari on an iPad. The primary grid column collapsed to something like 40% width on orientation change. The sidebar became a weird floating island. Touch scroll felt sticky.&lt;/p&gt;

&lt;p&gt;The root cause was boring. I had a combination of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;minmax()&lt;/code&gt; grid tracks with percentages&lt;/li&gt;
  &lt;li&gt;a flex wrapper around the grid for a layout experiment I had abandoned&lt;/li&gt;
  &lt;li&gt;some &lt;code&gt;height: 100vh&lt;/code&gt; elements that hated mobile Safari’s dynamic toolbar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chrome guessed my intent and kept the layout together. Safari followed the spec and smoked out all three problems in one go.&lt;/p&gt;

&lt;p&gt;If I had built in Chrome only, that bug would have surfaced &lt;em&gt;after&lt;/em&gt; going through content population, QA rounds, and probably some UAT demo. That kind of late-stage layout bug is brutal to fix quietly.&lt;/p&gt;

&lt;p&gt;By starting in Safari, I was forced to simplify the layout rules. I dropped the pointless flex wrapper, replaced the 100vh stuff with logical properties and calc, and tightened my min/max values. The layout got more robust on every browser as a side effect.&lt;/p&gt;

&lt;h2&gt;Safari Makes Me Choose My CSS More Carefully&lt;/h2&gt;

&lt;p&gt;When I start in Chrome, I reach for the shiny thing first. Subgrid, fancy masking, weird blend modes, random viewport units.&lt;/p&gt;

&lt;p&gt;When I start in Safari, I ask a different question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What is the smallest, most boring CSS that still gets me the experience I want?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That question saves me from myself. A few patterns Safari-first testing has pushed into my workflow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I ship more layouts with plain Flexbox and classic Grid, less “spec tourism”.&lt;/li&gt;
  &lt;li&gt;I lean harder on intrinsic sizing and content flow instead of fixed heights and magic numbers.&lt;/li&gt;
  &lt;li&gt;I use fewer overlapping transform stacks and less hacky nesting that relies on “Chrome vibes” rather than predictable behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result is a codebase that feels boring when you skim it, but keeps working when a browser team changes something in the rendering engine.&lt;/p&gt;

&lt;h2&gt;Forms And Inputs: Where Safari Refuses To Babysit You&lt;/h2&gt;

&lt;p&gt;Forms are where Safari-first really shines. Chrome is absurdly forgiving here. You can mess up types, attributes, focus states, and still get something “fine”.&lt;/p&gt;

&lt;p&gt;A few specific bugs that Safari surfaced early for me:&lt;/p&gt;

&lt;h3&gt;Focus Outlines And Custom Styles&lt;/h3&gt;

&lt;p&gt;I had a custom input style system. Tailored focus rings, accessible color contrast, nice transitions. Everything felt tight in Chrome.&lt;/p&gt;

&lt;p&gt;On Safari, tabbing through the form felt wrong. Focus outlines flickered, border-radius changed mid-focus, and some inputs lost their ring altogether on keyboard nav.&lt;/p&gt;

&lt;p&gt;The reason: I had hacked away default outlines inconsistently and relied on Chrome’s focus heuristics plus some internal timing. Safari did not play that game.&lt;/p&gt;

&lt;p&gt;Safari first forced me to implement a simple rule: I handle all focus styles myself, across the board, with predictable states. No mixing and matching browser defaults by accident.&lt;/p&gt;

&lt;h3&gt;Autofill, Dark Mode, And Input Backgrounds&lt;/h3&gt;

&lt;p&gt;Safari’s autofill and dark mode behavior is opinionated. Chrome quietly lets you live with slightly mismatched background and text colors. Safari goes straight to unreadable text if you are sloppy.&lt;/p&gt;

&lt;p&gt;By starting there, I fix color and background mismatches right away. The autofilled value has to be visible in the harshest condition on Safari first. Chrome ends up looking better as a side effect.&lt;/p&gt;

&lt;h2&gt;Mobile Safari Performance Bugs Chrome Hides&lt;/h2&gt;

&lt;p&gt;Desktop Chrome on a good machine is a lie. You can throw irresponsible amounts of JS, repaint-heavy transitions, and parallax nonsense at it and still get 60fps. Then you hand it to someone on an older iPhone and watch it chew their battery.&lt;/p&gt;

&lt;p&gt;Safari on iOS feels closer to reality. Less RAM. Tighter limits. Harsher scheduling. If something feels janky there with a cold cache, you built something heavy.&lt;/p&gt;

&lt;p&gt;Examples of what Safari-first performance checks have caught for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Scroll-linked animations&lt;/strong&gt; that hammered the main thread. Chrome smoothed them. Safari stuttered, especially with heavy images.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Oversized shadow and blur effects&lt;/strong&gt; on large elements that looked nice on desktop, but murdered Safari’s GPU on mobile.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Over-eager third-party scripts&lt;/strong&gt; loading in the critical path. Chrome absorbed the punch. Mobile Safari visibly froze for a moment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My rule now: if it feels smooth in Safari on a mid-range iPhone, I am allowed to start tuning it for Chrome “delight”. Not the other way around.&lt;/p&gt;

&lt;h2&gt;The iOS Viewport Pain You Either Face Early Or Late&lt;/h2&gt;

&lt;p&gt;The viewport situation on iOS Safari is chaos. Dynamic toolbars. Keyboard pushing everything. Viewport units that tell the truth only half the time. Old news, but still painful.&lt;/p&gt;

&lt;p&gt;If you only ever test in desktop Chrome until late in the project, you will 100% get hit by this. The classic examples:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“Full height” sections with &lt;code&gt;100vh&lt;/code&gt; that clip behind the address bar on iOS.&lt;/li&gt;
  &lt;li&gt;Sticky headers that un-stick while scrolling due to weird viewport changes.&lt;/li&gt;
  &lt;li&gt;Modals that are perfectly centered in Chrome and half off-screen when the iOS keyboard shows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I start in Safari, those are not “edge cases”. They are the baseline. I build the layout with the dynamic viewport in mind right away. That has pushed me to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Favor content-driven vertical layouts over full-screen hero sections unless they truly earn it.&lt;/li&gt;
  &lt;li&gt;Use modern viewport units and fallbacks in a deliberate way instead of sprinkling &lt;code&gt;100vh&lt;/code&gt; everywhere.&lt;/li&gt;
  &lt;li&gt;Test keyboard + focus flows on iOS while the form markup is still easy to change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is annoying. But less annoying than redesigning a hero or modal flow after a client shows you an iPhone screenshot in a meeting.&lt;/p&gt;

&lt;h2&gt;Why Chrome-First Gives You A False Sense Of Security&lt;/h2&gt;

&lt;p&gt;Chrome is a confident liar. It tells you your layout is solid when it is secretly held together with heuristics and internal smoothing.&lt;/p&gt;

&lt;p&gt;The classic trap: you get a thumbs-up from DevTools, Lighthouse, and your own eyeballs in Chrome. You feel safe. Then reality hits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A design glitch report from a client demo on an iPad.&lt;/li&gt;
  &lt;li&gt;Strange tap targets on iOS because of unexpected relative positioning.&lt;/li&gt;
  &lt;li&gt;A weird gap or overlap that only happens at a specific Safari breakpoint with real content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those bugs are expensive, not because they are technically hard, but because they arrive late. Late means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Copy and imagery are already in.&lt;/li&gt;
  &lt;li&gt;Stakeholders are already attached to specific visuals.&lt;/li&gt;
  &lt;li&gt;Your own mental model of the layout is frozen, so deep changes feel dangerous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Safari-first flips the script. I assume the strict parent is watching me from day one. It keeps the code simple and my ego in check.&lt;/p&gt;

&lt;h2&gt;My Actual Workflow, Step By Step&lt;/h2&gt;

&lt;p&gt;This is how I run things in practice on a new feature or layout.&lt;/p&gt;

&lt;h3&gt;1. Start In Safari Desktop&lt;/h3&gt;

&lt;p&gt;I open Safari on macOS, not Chrome, and build the layout there first. No feature flags, no prefixing, just raw HTML and CSS.&lt;/p&gt;

&lt;p&gt;I test:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Resizing the viewport aggressively. Small to huge, landscape to portrait.&lt;/li&gt;
  &lt;li&gt;Keyboard navigation, focus outlines, skip links.&lt;/li&gt;
  &lt;li&gt;Performance while throttling network and CPU in the dev tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I catch myself thinking “ah, Chrome will probably handle this”, I stop and refactor the layout until Safari handles it cleanly.&lt;/p&gt;

&lt;h3&gt;2. Switch To iOS Safari Early&lt;/h3&gt;

&lt;p&gt;Before things look “done”, I airdrop the URL or use a local tunnel and open it on an iPhone and iPad. Real hardware, not just responsive mode.&lt;/p&gt;

&lt;p&gt;I check:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Orientation changes.&lt;/li&gt;
  &lt;li&gt;Scrolling, pull-to-refresh behavior, sticky stuff.&lt;/li&gt;
  &lt;li&gt;Forms with the keyboard open and closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something breaks here, I accept that as feedback on my architecture, not a “mobile quirk”. That mindset shift helps.&lt;/p&gt;

&lt;h3&gt;3. Then I Reward Myself With Chrome&lt;/h3&gt;

&lt;p&gt;Only when it feels solid in Safari do I open Chrome. That is when I let myself add the polish that Chrome does so well:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Subtle transforms.&lt;/li&gt;
  &lt;li&gt;Smoother transitions.&lt;/li&gt;
  &lt;li&gt;Optional enhancements that don’t break the core layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By that point, the hard structural problems are solved. Chrome is just the candy coating.&lt;/p&gt;

&lt;h2&gt;But What About Feature Gaps?&lt;/h2&gt;

&lt;p&gt;Sometimes Safari simply does not have the shiny new CSS thing I want. Container queries used to be like that. Certain advanced mask and filter combos still are.&lt;/p&gt;

&lt;p&gt;My rule is simple. If the feature is non-essential, I treat it as progressive enhancement and move it behind a feature check or class. The core layout must work without it in Safari.&lt;/p&gt;

&lt;p&gt;If the feature is essential to the concept, I think hard about whether the concept is actually worth tying to a single browser engine. Usually it is not. When it is, it is a deliberate tradeoff, not a surprise late in the project.&lt;/p&gt;

&lt;h2&gt;Safari First, Not Safari Only&lt;/h2&gt;

&lt;p&gt;I am not a Safari fanboy. I keep multiple Chromes installed. I like Firefox a lot. I care about Edge for corporate contexts. This is not religion.&lt;/p&gt;

&lt;p&gt;Testing in Safari first is just a constraint that raises the floor. It forces me to write CSS and markup that can survive a stricter interpretation of the rules and the wonkiest mobile viewport in mainstream use.&lt;/p&gt;

&lt;p&gt;The upside is simple. I get fewer “urgent” bug tickets about layout issues. My builds age better when browser teams ship new stuff. Clients trust the work more because it behaves on the actual devices they hold in their hands.&lt;/p&gt;

&lt;p&gt;If you are tired of last-minute Safari bugs blowing up your timelines, try it for one project. Close Chrome. Open Safari. Build the next page there first.&lt;/p&gt;

&lt;p&gt;You will hate it for two days. Then the bugs it catches early will pay you back for months.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>ios</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Lazy Loading Images Almost Tanked My Core Web Vitals</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:14:39 +0000</pubDate>
      <link>https://dev.to/richardlemon/lazy-loading-images-almost-tanked-my-core-web-vitals-4n4p</link>
      <guid>https://dev.to/richardlemon/lazy-loading-images-almost-tanked-my-core-web-vitals-4n4p</guid>
      <description>&lt;h2&gt;Lazy loading that made everything slower&lt;/h2&gt;

&lt;p&gt;I had a week where my Core Web Vitals looked like a slow-motion car crash.&lt;/p&gt;

&lt;p&gt;New layout. New images. New lazy loading strategy I felt pretty smart about. Lighthouse loved it locally. Then the real-world data arrived.&lt;/p&gt;

&lt;p&gt;Chrome UX Report came back with my LCP in the &lt;strong&gt;3.4s to 3.7s&lt;/strong&gt; range on mobile for some key pages. Before the redesign I was sitting around &lt;strong&gt;2.1s to 2.4s&lt;/strong&gt;. So I “optimized” and lost over a full second of LCP.&lt;/p&gt;

&lt;p&gt;The culprit was not big JavaScript. It was not fonts. It was me getting too aggressive with &lt;code&gt;loading="lazy"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;The setup: what I changed that broke LCP&lt;/h2&gt;

&lt;p&gt;The site is simple.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Static content&lt;/li&gt;
  &lt;li&gt;Next.js, no heavy client-side routing&lt;/li&gt;
  &lt;li&gt;Mostly text, a few images per page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The redesign added more visuals. Hero images, small illustrations, some inline screenshots. Classic trap: once you have more images, you feel obligated to lazy load everything because “performance”.&lt;/p&gt;

&lt;p&gt;So I did exactly that. I shipped a release where &lt;strong&gt;every image&lt;/strong&gt; had one of these attached:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;img src="/images/post-hero.jpg" 
     alt="Post hero" 
     loading="lazy" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No exception for the hero. No exception for the image that always becomes LCP. Just blanket lazy loading, sprinkled like salt.&lt;/p&gt;

&lt;h2&gt;The real numbers: before and after the mess&lt;/h2&gt;

&lt;p&gt;This is the part I actually care about when reading posts like this, so here is mine.&lt;/p&gt;

&lt;h3&gt;Before the redesign&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Homepage LCP (CrUX, mobile, 28-day median): &lt;strong&gt;2.2s&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Blog post template LCP (mobile): &lt;strong&gt;2.3s&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Good LCP share on mobile: around &lt;strong&gt;85% to 88%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My LCP element was usually a heading or a small thumbnail. Nothing dramatic. Images were small and loaded early.&lt;/p&gt;

&lt;h3&gt;After the redesign + lazy everything&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Homepage LCP (mobile): &lt;strong&gt;3.5s&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Blog post template LCP (mobile): &lt;strong&gt;3.4s&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Good LCP share on mobile dropped to around &lt;strong&gt;58% to 62%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the moment you stop feeling clever and start reading HAR files.&lt;/p&gt;

&lt;h3&gt;After the fix&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Homepage LCP (mobile): &lt;strong&gt;2.3s to 2.5s&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Blog post template LCP (mobile): &lt;strong&gt;2.4s to 2.6s&lt;/strong&gt;
&lt;/li&gt;
  &lt;li&gt;Good LCP share on mobile recovered to around &lt;strong&gt;86% to 90%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not remove lazy loading. I just stopped being stupid with it.&lt;/p&gt;

&lt;h2&gt;How lazy loading actually interacts with LCP&lt;/h2&gt;

&lt;p&gt;Largest Contentful Paint is brutally simple. Chrome picks the largest visual element in the viewport and measures when it is fully rendered.&lt;/p&gt;

&lt;p&gt;On my pages that element was almost always the hero image. By putting &lt;code&gt;loading="lazy"&lt;/code&gt; on it, I told the browser: “Please wait until you think this is near the viewport before fetching it.”&lt;/p&gt;

&lt;p&gt;So the sequence looked like this, based on WebPageTest and performance panel traces:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;HTML downloaded&lt;/li&gt;
  &lt;li&gt;CSS downloaded and applied&lt;/li&gt;
  &lt;li&gt;Hero image discovered, but flagged as lazy&lt;/li&gt;
  &lt;li&gt;Intersection logic kicked in late on some devices&lt;/li&gt;
  &lt;li&gt;Actual image request started &lt;strong&gt;hundreds of milliseconds&lt;/strong&gt; later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That extra gap was exactly the regression in my LCP. I had shifted the LCP asset from 
“first in line” to “whenever you get around to it”.&lt;/p&gt;

&lt;h2&gt;The three specific mistakes I made&lt;/h2&gt;

&lt;p&gt;This was not one bug. It was a cluster.&lt;/p&gt;

&lt;h3&gt;1. Lazy loading above-the-fold images&lt;/h3&gt;

&lt;p&gt;This is the obvious one. You read one article from 2019 that says “add lazy to all images” and you turn off your brain.&lt;/p&gt;

&lt;p&gt;My hero image was &lt;strong&gt;always&lt;/strong&gt; inside the initial viewport on mobile. That image should behave like a critical asset, not a nice-to-have below-the-fold picture of a cat.&lt;/p&gt;

&lt;p&gt;Once I removed &lt;code&gt;loading="lazy"&lt;/code&gt; from that hero, my lab tests showed LCP dropping from ~3.2s back to ~2.5s. Real users took a bit longer to reflect it, but the trend matched.&lt;/p&gt;

&lt;h3&gt;2. No explicit sizes, so layout shifted&lt;/h3&gt;

&lt;p&gt;On top of lazy loading, I also skipped proper &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;. Good combination, right.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;img src="/images/post-hero.jpg" 
     alt="Post hero" 
     loading="lazy" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser had no idea how much space to reserve. So the layout jumped once the hero image arrived. That made CLS worse, and it also gave the browser less confidence about when layout was stable enough to call LCP.&lt;/p&gt;

&lt;p&gt;Once I added sizing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;img src="/images/post-hero.jpg" 
     alt="Post hero" 
     width="1200" 
     height="630" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;LCP stabilized. CLS went down from around &lt;strong&gt;0.13&lt;/strong&gt; to around &lt;strong&gt;0.02&lt;/strong&gt; on those pages. That matters for Core Web Vitals overall, not just for the LCP number.&lt;/p&gt;

&lt;h3&gt;3. Treating all images like they are equal&lt;/h3&gt;

&lt;p&gt;My first pass did not distinguish between hero, inline illustration, code screenshot, or footer logo. Same attribute on everything.&lt;/p&gt;

&lt;p&gt;Realistically I had three types of images:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Critical above-the-fold&lt;/strong&gt; (hero, key visual next to title)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Near-fold but not LCP&lt;/strong&gt; (first inline image, small icons)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Deep content&lt;/strong&gt; (later inline screenshots, bottom of article)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three need different treatment. My markup did not reflect that at all.&lt;/p&gt;

&lt;h2&gt;The fixed strategy: simple rules that actually work&lt;/h2&gt;

&lt;p&gt;I am not interested in clever heuristics here. I want rules I can follow at 1 a.m. without thinking too hard.&lt;/p&gt;

&lt;h3&gt;Rule 1: Never lazy load the LCP candidate&lt;/h3&gt;

&lt;p&gt;On my pages, the likely LCP is almost always the hero image. So rule one is simple.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;img src="/images/post-hero.jpg" 
     alt="Post hero" 
     width="1200" 
     height="630" 
     fetchpriority="high" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No lazy attribute at all. I also set &lt;code&gt;fetchpriority="high"&lt;/code&gt; to hint that this thing should start downloading early. That bumped the image fetch up in the waterfall by about 200 to 300 ms on throttled mobile tests.&lt;/p&gt;

&lt;p&gt;Result for real users:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mobile LCP on blog posts moved from ~&lt;strong&gt;3.4s&lt;/strong&gt; to ~&lt;strong&gt;2.6s&lt;/strong&gt; in CrUX within a release cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Rule 2: Lazy load anything that starts fully below the fold&lt;/h3&gt;

&lt;p&gt;If an image is clearly below the fold on mobile, I do not feel bad about lazy loading it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;img src="/images/deep-screenshot.png" 
     alt="Deep in the article" 
     loading="lazy" 
     width="800" 
     height="450" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That includes later inline screenshots and section thumbnails further down.&lt;/p&gt;

&lt;p&gt;On my pages, this dropped total image bytes on initial load by 40 to 60 percent, depending on the article. More importantly, it did not touch LCP at all, because these images never become the largest element in the initial viewport.&lt;/p&gt;

&lt;h3&gt;Rule 3: Be careful with near-fold images&lt;/h3&gt;

&lt;p&gt;The tricky ones are images near the fold. On a tall screen they are above the fold, on a smaller device they are not.&lt;/p&gt;

&lt;p&gt;I experimented with laziness on these and saw some nasty LCP spikes for certain viewports. In the end I went conservative.&lt;/p&gt;

&lt;p&gt;If an image appears:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;in the first section after the hero, and&lt;/li&gt;
  &lt;li&gt;wide enough that it can realistically become LCP on smaller screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I treat it like a critical image and &lt;strong&gt;do not&lt;/strong&gt; lazy load it. In HTML that looks like a normal image tag with proper sizing and &lt;code&gt;fetchpriority="auto"&lt;/code&gt; (or just omitted).&lt;/p&gt;

&lt;p&gt;That choice stabilized my LCP distribution. Worst-case LCP outliers on mobile dropped from over &lt;strong&gt;6s&lt;/strong&gt; down to around &lt;strong&gt;3.8s&lt;/strong&gt; for slower connections.&lt;/p&gt;

&lt;h2&gt;How I validated the fixes&lt;/h2&gt;

&lt;p&gt;I do not trust local Lighthouse scores since this mess. They are fine for rough direction, but my regressions did not show there until it was already in production.&lt;/p&gt;

&lt;p&gt;Here is what I actually used.&lt;/p&gt;

&lt;h3&gt;1. WebPageTest: see the waterfall&lt;/h3&gt;

&lt;p&gt;I set up tests for a couple of representative pages. Throttled mobile, 4G, real browser. I wanted to see:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;When the HTML finished&lt;/li&gt;
  &lt;li&gt;When CSS finished&lt;/li&gt;
  &lt;li&gt;When the hero image request started&lt;/li&gt;
  &lt;li&gt;When the hero image finished&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before the fix, the hero request sat awkwardly late in the timeline. Sometimes it started more than a second after the initial HTML. After I removed lazy loading and added &lt;code&gt;fetchpriority="high"&lt;/code&gt;, that request started almost immediately after CSS.&lt;/p&gt;

&lt;h3&gt;2. Chrome DevTools performance panel: confirm LCP element&lt;/h3&gt;

&lt;p&gt;I recorded a couple of traces on mobile emulation and inspected the “Largest Contentful Paint” event.&lt;/p&gt;

&lt;p&gt;During the broken version, LCP was usually the hero image but with a timestamp around 3.1s to 3.4s. After the fix, the same element had timestamps around 1.9s to 2.3s under similar conditions.&lt;/p&gt;

&lt;p&gt;The key was seeing the correlation between the start of the image request and the LCP timestamp. You want those close together, not separated by half a second of nothing.&lt;/p&gt;

&lt;h3&gt;3. CrUX and Search Console: real user confirmation&lt;/h3&gt;

&lt;p&gt;The real test was Chrome UX Report and Search Console’s Core Web Vitals report. Those lag behind by a few days, so I had to wait.&lt;/p&gt;

&lt;p&gt;The pattern looked like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Week 0: shipped redesign with aggressive lazy loading&lt;/li&gt;
  &lt;li&gt;Week 1: “Good” LCP share on mobile dropped from ~88% to ~60%&lt;/li&gt;
  &lt;li&gt;Week 2: shipped fix (no lazy on hero, sizes, fetchpriority)&lt;/li&gt;
  &lt;li&gt;Week 3: “Good” LCP share on mobile climbed back to ~86%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the part that matters for SEO and for users actually feeling the speed.&lt;/p&gt;

&lt;h2&gt;The mental model I use now&lt;/h2&gt;

&lt;p&gt;I treat image loading less like a generic optimization and more like traffic control.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Fast lane&lt;/strong&gt;: hero and early large visuals. No lazy. Sometimes &lt;code&gt;fetchpriority="high"&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Middle lane&lt;/strong&gt;: near-fold images that could become LCP on some devices. Usually not lazy, conservative approach.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Slow lane&lt;/strong&gt;: clearly below-the-fold images. &lt;code&gt;loading="lazy"&lt;/code&gt;, full width and height, no guilt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That model kept my LCP honest while still saving bandwidth further down the page.&lt;/p&gt;

&lt;h2&gt;If you want a quick checklist&lt;/h2&gt;

&lt;p&gt;If you are skimming this for the parts that matter, here is what actually moved my metrics.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Remove &lt;code&gt;loading="lazy"&lt;/code&gt; from your hero or any likely LCP element.&lt;/li&gt;
  &lt;li&gt;Add explicit &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; on every image.&lt;/li&gt;
  &lt;li&gt;Use &lt;code&gt;fetchpriority="high"&lt;/code&gt; on your main hero image.&lt;/li&gt;
  &lt;li&gt;Lazy load only images that are safely below the fold on most devices.&lt;/li&gt;
  &lt;li&gt;Validate with WebPageTest and real CrUX data, not just local Lighthouse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not fancy. It is boring markup. But boring markup got me from a broken &lt;strong&gt;3.5s LCP&lt;/strong&gt; back into the comfortable &lt;strong&gt;2.4s-ish&lt;/strong&gt; zone on mobile.&lt;/p&gt;

&lt;p&gt;If your Core Web Vitals suddenly look worse right after you “optimize” images, check your lazy loading first. I learned that lesson the hard way, so you maybe do not have to.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>html</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Docker Mac Studio Setup That Finally Stopped Breaking</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:14:09 +0000</pubDate>
      <link>https://dev.to/richardlemon/the-docker-mac-studio-setup-that-finally-stopped-breaking-4cli</link>
      <guid>https://dev.to/richardlemon/the-docker-mac-studio-setup-that-finally-stopped-breaking-4cli</guid>
      <description>&lt;h2&gt;Docker on Mac Studio used to be a recurring nightmare&lt;/h2&gt;

&lt;p&gt;I love my Mac Studio. I hated Docker on it for a long time.&lt;/p&gt;

&lt;p&gt;Random build failures. Containers chewing 30 GB of RAM. Volume performance going from “fine” to “did my machine freeze?” overnight. Every few weeks something broke after an update, a new project, or just because I dared to run another stack.&lt;/p&gt;

&lt;p&gt;This is the setup that finally stopped breaking on my Mac Studio. Not perfect. Just boring. Which is exactly what you want from Docker.&lt;/p&gt;

&lt;p&gt;I will walk through the actual errors I hit, the dead ends, and the final config that has been stable for months.&lt;/p&gt;

&lt;h2&gt;My hardware and baseline&lt;/h2&gt;

&lt;p&gt;For context, this is the machine:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mac Studio M2 Max&lt;/li&gt;
  &lt;li&gt;64 GB RAM&lt;/li&gt;
  &lt;li&gt;2 TB SSD&lt;/li&gt;
  &lt;li&gt;macOS Sonoma (been on 14.x, now 15 beta, same story)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker flavor:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Docker Desktop for Mac (Apple Silicon build)&lt;/li&gt;
  &lt;li&gt;Colima installed, but now only for experiments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run the usual front-end circus: Node, pnpm, Vite, Next.js, Laravel, Postgres, Redis, a couple of legacy PHP projects, and one ugly Java thing that nobody wants to touch.&lt;/p&gt;

&lt;h2&gt;The errors that kept hitting me&lt;/h2&gt;

&lt;p&gt;These are the ones that cost me the most time.&lt;/p&gt;

&lt;h3&gt;1. "no space left on device" with 800 GB free&lt;/h3&gt;

&lt;p&gt;My favorite kind of error. Complete lie. I had half a terabyte free. Docker did not care.&lt;/p&gt;

&lt;p&gt;It usually happened when building multiple images in a row, or when I ran a local registry plus a few heavier services.&lt;/p&gt;

&lt;p&gt;Typical log line:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;failed to copy: write /var/lib/docker/...: no space left on device
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;2. Containers randomly dying under light load&lt;/h3&gt;

&lt;p&gt;This one hurt because it smelled like hardware, but it was not.&lt;/p&gt;

&lt;p&gt;Symptoms:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Next.js dev server inside Docker just exited with code 137&lt;/li&gt;
  &lt;li&gt;Postgres container stopping during a migration with no helpful message&lt;/li&gt;
  &lt;li&gt;Browser reconnecting to Vite every few minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exit code 137 is usually “killed by the kernel”. On Linux that often means OOM. On Mac with Docker it often means “the little Linux VM ran out of something”.&lt;/p&gt;

&lt;h3&gt;3. Shared volume performance tanking randomly&lt;/h3&gt;

&lt;p&gt;Sometimes &lt;code&gt;node_modules&lt;/code&gt; installs took 10 seconds. Sometimes 3 minutes. Same project. Same commands. Same coffee.&lt;/p&gt;

&lt;p&gt;It turned out to be a mix of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How I mounted volumes&lt;/li&gt;
  &lt;li&gt;Mutagen / VirtioFS / cached flags&lt;/li&gt;
  &lt;li&gt;Watchers going crazy inside containers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;4. "standard_init_linux.go:211: exec user process caused: exec format error"&lt;/h3&gt;

&lt;p&gt;This one hit me early with the move to Apple Silicon.&lt;/p&gt;

&lt;p&gt;Usually when pulling older images, or building images from base images that were still &lt;code&gt;amd64&lt;/code&gt; only.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;standard_init_linux.go:211: exec user process caused: exec format error
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you see that on a Mac Studio, it is almost always an architecture mismatch.&lt;/p&gt;

&lt;h2&gt;The approach that finally worked&lt;/h2&gt;

&lt;p&gt;Fixing this was not a single trick. It was a set of decisions that reduced surprise.&lt;/p&gt;

&lt;p&gt;Rough order of attack:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Lock Docker Desktop to a sane config&lt;/li&gt;
  &lt;li&gt;Clean up disk usage and control growth&lt;/li&gt;
  &lt;li&gt;Normalize architecture across images&lt;/li&gt;
  &lt;li&gt;Fix volumes for front-end workflows&lt;/li&gt;
  &lt;li&gt;Make Docker Compose files boring and predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;1. Docker Desktop: my stable Mac Studio config&lt;/h2&gt;

&lt;p&gt;First thing I changed: I stopped treating Docker Desktop like a magic box and started treating it like a tiny server I actually manage.&lt;/p&gt;

&lt;h3&gt;Resources tab&lt;/h3&gt;

&lt;p&gt;My current settings:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;CPUs&lt;/strong&gt;: 8 (out of 12)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: 16 GB (out of 64)&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Swap&lt;/strong&gt;: 4 GB&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Virtual disk size&lt;/strong&gt;: 256 GB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The big change that killed most “no space left on device” issues was increasing the virtual disk size and then actually watching it.&lt;/p&gt;

&lt;p&gt;64 GB was not enough for my use. 128 GB worked for a while. 256 GB has been the sweet spot.&lt;/p&gt;

&lt;p&gt;I also stopped giving Docker “everything”. When I let it use 30+ GB RAM, it happily did that and then macOS became sluggish. With 16 GB it has constraints but still runs multiple stacks fine.&lt;/p&gt;

&lt;h3&gt;General tab&lt;/h3&gt;

&lt;p&gt;Settings that matter for stability:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Use Virtualization framework&lt;/strong&gt;: On&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Use Rosetta for x86/amd64 emulation&lt;/strong&gt;: On&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Use gRPC FUSE for file sharing&lt;/strong&gt;: Off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had more problems with gRPC FUSE than benefits. Disabling that and sticking to the default, plus volume mount tweaks, was more predictable for me.&lt;/p&gt;

&lt;h2&gt;2. Cleaning Docker's mess properly&lt;/h2&gt;

&lt;p&gt;I used to run &lt;code&gt;docker system prune&lt;/code&gt; like a ritual. It helped, but it was not enough on Mac Studio because of the virtual disk.&lt;/p&gt;

&lt;h3&gt;The actual commands I use now&lt;/h3&gt;

&lt;p&gt;Once a week or when I hit weird errors, I run:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker system df
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This tells me where the bloat is: images, containers, local volumes, build cache.&lt;/p&gt;

&lt;p&gt;Then I am a bit more explicit:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# stop everything
docker compose down -v

# remove dangling images, containers, networks
docker system prune -f

# remove unused volumes (careful: this wipes local DBs etc.)
docker volume prune -f

# clean build cache
docker builder prune -af
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The important piece is &lt;code&gt;docker builder prune -af&lt;/code&gt;. BuildKit happily hoards cache layers. On my machine it reclaimed tens of gigabytes the first time.&lt;/p&gt;

&lt;p&gt;If disk usage gets close to the virtual disk size, I shut everything down and then do a full reset of the disk from Docker Desktop:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Settings → Troubleshoot → Clean / Purge data&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I treat that like reformatting a dev server. Annoying, but clean.&lt;/p&gt;

&lt;h2&gt;3. Architecture: no more "exec format error"&lt;/h2&gt;

&lt;p&gt;Apple Silicon is fast. Rosetta is good. But mixing architectures in Docker is a recipe for subtle pain.&lt;/p&gt;

&lt;p&gt;My rules now:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Default everything to &lt;code&gt;linux/arm64&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;Only opt into &lt;code&gt;linux/amd64&lt;/code&gt; when the stack forces it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Setting the default platform&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;~/.docker/config.json&lt;/code&gt; I added:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  "platform": "linux/arm64"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That made &lt;code&gt;docker pull&lt;/code&gt; and &lt;code&gt;docker build&lt;/code&gt; behave more predictably on this machine.&lt;/p&gt;

&lt;h3&gt;Explicit platform in images that need amd64&lt;/h3&gt;

&lt;p&gt;For the one Java stack that just refuses to run properly on arm64, I pin it inside &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;services:
  legacy-app:
    image: somecorp/legacy-app:2.3
    platform: linux/amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Same for old Postgres versions when I have to match production:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;services:
  db:
    image: postgres:11
    platform: linux/amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once I did that, the &lt;code&gt;exec format error&lt;/code&gt; just disappeared.&lt;/p&gt;

&lt;h2&gt;4. Volumes and front-end performance on Mac Studio&lt;/h2&gt;

&lt;p&gt;This was the biggest quality of life upgrade.&lt;/p&gt;

&lt;p&gt;Most front-end dev pain on Docker for Mac is volume related. File watching, node_modules, and hot reloading over the Docker VM boundary are a bad combo if you get the mounts wrong.&lt;/p&gt;

&lt;h3&gt;I stopped mounting node_modules from the host&lt;/h3&gt;

&lt;p&gt;This alone killed a lot of flakiness and weird performance spikes.&lt;/p&gt;

&lt;p&gt;Old style:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;services:
  web:
    build: .
    volumes:
      - .:/app
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This means your &lt;code&gt;node_modules&lt;/code&gt; sits on the host and gets mapped into the container. Every file access for the toolchain crosses the VM boundary.&lt;/p&gt;

&lt;p&gt;New style I use now for Node projects:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;services:
  web:
    build: .
    working_dir: /app
    volumes:
      - .:/app
      - /app/node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The second line (&lt;code&gt;/app/node_modules&lt;/code&gt;) creates an &lt;strong&gt;anonymous volume&lt;/strong&gt; inside Docker for that path. So the source code is synced, but node_modules lives inside the VM where it is fast.&lt;/p&gt;

&lt;p&gt;Install script in the Dockerfile stays simple:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RUN corepack enable \
  &amp;amp;&amp;amp; pnpm install --frozen-lockfile
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This alone made Vite and Next dev servers much less erratic.&lt;/p&gt;

&lt;h3&gt;Using delegated / cached where it actually helps&lt;/h3&gt;

&lt;p&gt;I do not go crazy with the mount options, but I use them in a few places.&lt;/p&gt;

&lt;p&gt;Example for a Laravel app with a Node front-end:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;services:
  app:
    volumes:
      - .:/var/www:cached
      - /var/www/node_modules

  vite:
    volumes:
      - .:/var/www:delegated
      - /var/www/node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;cached&lt;/code&gt; tells Docker that the container can see slightly stale data. For PHP code that is fine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;delegated&lt;/code&gt; favors writes from the container. For Vite writing to &lt;code&gt;public&lt;/code&gt; and temp files, that helped stabilize things.&lt;/p&gt;

&lt;p&gt;I am not religious about the exact combo. The main point is: do not mount everything blindly and then wonder why file IO sucks.&lt;/p&gt;

&lt;h3&gt;Reduce watchers inside the container&lt;/h3&gt;

&lt;p&gt;I also changed how I run watch scripts. Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;"scripts": {
  "dev": "vite --host 0.0.0.0"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I often run with a polling interval or limited watch scope:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;"scripts": {
  "dev": "vite --host 0.0.0.0 --watchOptions.usePolling --watchOptions.interval=500"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Not as instant as inotify, but more predictable on Docker for Mac.&lt;/p&gt;

&lt;h2&gt;5. Boring docker-compose files&lt;/h2&gt;

&lt;p&gt;My early Compose files tried to be clever. Lots of overrides. Fancy networks. Conditional build args. That was fun until something broke and it took an hour to untangle.&lt;/p&gt;

&lt;p&gt;My main Mac Studio rule now: keep the local dev Compose minimal and explicit.&lt;/p&gt;

&lt;h3&gt;One real example&lt;/h3&gt;

&lt;p&gt;This is the trimmed version of the Compose I use for a Next.js + Postgres app:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;version: "3.9"

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_ENV: development
    platform: linux/arm64
    ports:
      - "3000:3000"
    env_file:
      - .env.local
    environment:
      DATABASE_URL: postgresql://postgres:postgres@db:5432/app
    volumes:
      - .:/app
      - /app/node_modules
    command: pnpm dev

  db:
    image: postgres:15-alpine
    platform: linux/arm64
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: app
    ports:
      - "5433:5432"
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Both services pin &lt;code&gt;linux/arm64&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;Node modules isolated inside Docker&lt;/li&gt;
  &lt;li&gt;Postgres data in a named volume, not a bind mount&lt;/li&gt;
  &lt;li&gt;Database port mapped to 5433 on host, so it does not clash with a local Postgres&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This file has not changed in months. That is the target.&lt;/p&gt;

&lt;h2&gt;6. Fixing the random container deaths&lt;/h2&gt;

&lt;p&gt;Once the resources and volumes were sane, the last weird issue was containers dying with exit code 137 under what looked like light load.&lt;/p&gt;

&lt;p&gt;What actually fixed it on my Mac Studio:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Lowering Docker's RAM from 24+ GB to 16 GB, then restarting everything&lt;/li&gt;
  &lt;li&gt;Giving the VM some swap (4 GB), instead of disabling it altogether&lt;/li&gt;
  &lt;li&gt;Checking for run-away processes with &lt;code&gt;docker stats&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one case, a broken Next build script spawned child processes in a loop. &lt;code&gt;docker stats&lt;/code&gt; exposed that in seconds. Before that, I blamed Apple Silicon, Docker, the weather, whatever.&lt;/p&gt;

&lt;p&gt;I also added basic healthchecks for critical services, so they restart cleanly instead of silently dying and leaving me guessing.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;What I stopped doing&lt;/h2&gt;

&lt;p&gt;Stability came just as much from what I removed as from what I added.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No more overlapping: Docker Desktop, Colima, and OrbStack all installed and half-configured&lt;/li&gt;
  &lt;li&gt;No more running everything with &lt;code&gt;latest&lt;/code&gt; tags&lt;/li&gt;
  &lt;li&gt;No more clever shell aliases that hide real Docker commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My rule now: this Mac Studio has one default Docker runtime (Docker Desktop), one main Compose file per project, and pinned image versions for anything I care about.&lt;/p&gt;

&lt;h2&gt;The boring, stable Docker Mac Studio setup&lt;/h2&gt;

&lt;p&gt;To recap the parts that actually mattered for stability:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Right-size Docker Desktop resources: 8 cores, 16 GB RAM, 256 GB virtual disk&lt;/li&gt;
  &lt;li&gt;Clean aggressively with &lt;code&gt;docker system df&lt;/code&gt; and &lt;code&gt;docker builder prune -af&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;Default to &lt;code&gt;linux/arm64&lt;/code&gt;, explicitly pin &lt;code&gt;linux/amd64&lt;/code&gt; when needed&lt;/li&gt;
  &lt;li&gt;Do not mount &lt;code&gt;node_modules&lt;/code&gt; from host; use anonymous volumes&lt;/li&gt;
  &lt;li&gt;Use simple, explicit Compose files with named volumes for databases&lt;/li&gt;
  &lt;li&gt;One Docker stack per project, not a shared mega Compose for everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is novel. That is the point. Docker on Mac Studio stopped breaking once I treated it like a small server with constraints, not as a magical black hole for containers.&lt;/p&gt;

&lt;p&gt;If your Mac Studio keeps throwing "no space left on device" or killing containers with exit 137, start with the disk size, the architecture, and your volume mounts. Those three were responsible for almost all of my pain.&lt;/p&gt;

</description>
      <category>docker</category>
    </item>
    <item>
      <title>What a 90ms Font Swap Taught Me About Performance Budgets</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:13:10 +0000</pubDate>
      <link>https://dev.to/richardlemon/what-a-90ms-font-swap-taught-me-about-performance-budgets-2cj</link>
      <guid>https://dev.to/richardlemon/what-a-90ms-font-swap-taught-me-about-performance-budgets-2cj</guid>
      <description>&lt;h2&gt;It started with a tiny flicker&lt;/h2&gt;

&lt;p&gt;I shipped a redesign, hit refresh, and caught a tiny flicker in the header.&lt;/p&gt;

&lt;p&gt;Body text popped in system font first. Then the web font slid in behind it. The whole thing was over in maybe 90 milliseconds. But once I saw it, I could not unsee it.&lt;/p&gt;

&lt;p&gt;That micro flash sent me down a rabbit hole that ended with a very different performance budget than the one I started with.&lt;/p&gt;

&lt;h2&gt;The crime scene: a 90ms font swap&lt;/h2&gt;

&lt;p&gt;The setup was pretty standard.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Self-hosted variable font, WOFF2 only.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;font-display: swap&lt;/code&gt; so we do not block rendering.&lt;/li&gt;
  &lt;li&gt;Preload hint for the main text face.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lighthouse loved it. Metrics were fine. No red flags.&lt;/p&gt;

&lt;p&gt;Then I loaded the site on my office monitor. 144 Hz, fairly fast machine, clean cache. The render felt slightly "shifty". Not full layout shift. More like the UI taking a tiny breath right after load.&lt;/p&gt;

&lt;p&gt;I cracked open &lt;code&gt;Performance&lt;/code&gt; in Chrome. Recorded a trace. Measured between first paint of system font and the swap to the web font. Around 90ms on my machine. 120-140ms on an older laptop. On 3G throttling, it stretched near 200ms.&lt;/p&gt;

&lt;p&gt;So now I had a number to obsess over. Dangerous.&lt;/p&gt;

&lt;h2&gt;Measuring the itch&lt;/h2&gt;

&lt;p&gt;I wired up a crude metric, because if I am going to chase a ghost, I at least want a graph.&lt;/p&gt;

&lt;p&gt;The idea:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Record the timestamp when we start rendering content.&lt;/li&gt;
  &lt;li&gt;Listen for &lt;code&gt;fontloading&lt;/code&gt; and &lt;code&gt;fontactive&lt;/code&gt; from a small FontFaceObserver wrapper.&lt;/li&gt;
  &lt;li&gt;Compute &lt;code&gt;fontSwapDelay = fontActiveTime - firstContentPaintTime&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not perfect. Good enough.&lt;/p&gt;

&lt;p&gt;On a real device set, I saw:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fast desktop: 60-100ms.&lt;/li&gt;
  &lt;li&gt;Average laptop: 110-180ms.&lt;/li&gt;
  &lt;li&gt;Mid-range Android on 4G: 220-320ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could feel it up to around 200ms on my own devices. Past that, it stopped being "barely noticeable" and started being "the page twitches after I start reading".&lt;/p&gt;

&lt;p&gt;So my brain did what developer brains love to do. It turned into a personal OKR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Get that swap under 100ms for 95% of visits."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No stakeholder asked for this. No user complained. This was pure, uncut perfectionism, dressed up as performance work.&lt;/p&gt;

&lt;h2&gt;Optimising the wrong hill&lt;/h2&gt;

&lt;p&gt;I tried everything that sounded even remotely sensible.&lt;/p&gt;

&lt;h3&gt;1. Smaller font payload&lt;/h3&gt;

&lt;p&gt;I subset the font:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;First full Latin + symbols.&lt;/li&gt;
  &lt;li&gt;Then Latin-1 only.&lt;/li&gt;
  &lt;li&gt;Then a split: core text subset for initial load, extended subset lazy-loaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gained maybe 20-30ms on shaky networks. At the cost of a more complex build step, extra font files, and some lovely edge cases with missing glyphs on rare characters.&lt;/p&gt;

&lt;p&gt;Now I had &lt;code&gt;body-core.woff2&lt;/code&gt;, &lt;code&gt;body-extended.woff2&lt;/code&gt;, and a lingering sense that I was doing too much for too little.&lt;/p&gt;

&lt;h3&gt;2. Font loading strategy acrobatics&lt;/h3&gt;

&lt;p&gt;I flipped through strategies like I was speed-running a blog post roundup.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;font-display: optional&lt;/code&gt; to skip the web font if it was too slow.&lt;/li&gt;
  &lt;li&gt;Manual class toggling once fonts loaded.&lt;/li&gt;
  &lt;li&gt;Different strategies for first visit vs. repeat visit using localStorage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fewer noticeable swaps on slow connections.&lt;/li&gt;
  &lt;li&gt;More variance in visual appearance between visits.&lt;/li&gt;
  &lt;li&gt;No dramatic impact on that 90-150ms band I was obsessed with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had traded a tiny flicker for a different set of inconsistencies. Not better. Just different.&lt;/p&gt;

&lt;h3&gt;3. System font stack experiments&lt;/h3&gt;

&lt;p&gt;The root issue was not just timing. It was the difference between the fallback font and the web font.&lt;/p&gt;

&lt;p&gt;If the metrics match closely, the swap feels less like a hiccup and more like a refinement. So I hunted for a system font stack that roughly matched my chosen typeface.&lt;/p&gt;

&lt;p&gt;I played with letter-spacing, line-height, and font-size to get the fallback as close as possible.&lt;/p&gt;

&lt;p&gt;Did it help? A bit.&lt;/p&gt;

&lt;p&gt;The swap became less noticeable on fast loads. On slower ones, you still saw the "before" and "after" state long enough for your brain to register that something just changed.&lt;/p&gt;

&lt;h3&gt;4. No web fonts at all&lt;/h3&gt;

&lt;p&gt;At some point I rage-switched to pure system fonts just to sanity check myself.&lt;/p&gt;

&lt;p&gt;Zero swaps. Zero extra font bytes. Clean, fast, boring. I lasted 24 hours before I put the web font back.&lt;/p&gt;

&lt;p&gt;This is where taste and performance fight. On a personal site, I care about typography. On a high-volume product surface, I might make a different decision.&lt;/p&gt;

&lt;h2&gt;The aha moment: a mismatched budget&lt;/h2&gt;

&lt;p&gt;The turning point was not technical.&lt;/p&gt;

&lt;p&gt;It was a call with a friend who runs a SaaS product. We were screen sharing. He liked the redesign. I told him I was still tuning the font loading. Mentioned the 90ms swap.&lt;/p&gt;

&lt;p&gt;He blinked. Then asked a very normal question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Out of curiosity, how many people are bouncing because of that?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did not have an answer. Obviously.&lt;/p&gt;

&lt;p&gt;Then he followed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What is the slowest thing a real user actually complains about right now?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That one I could answer: a not-so-great filter interaction on a content listing that could take ~1.5 seconds on certain queries.&lt;/p&gt;

&lt;p&gt;So I was polishing a 90-200ms visual artifact, while a 1500ms interaction delay was annoying paying users.&lt;/p&gt;

&lt;p&gt;Attention completely out of alignment with impact.&lt;/p&gt;

&lt;h2&gt;What a performance budget actually is&lt;/h2&gt;

&lt;p&gt;People talk about performance budgets like they are about kilobytes or milliseconds.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;A performance budget is a prioritisation story. It says which slowness you are allowed to care about first.&lt;/p&gt;

&lt;p&gt;My real budget on this project should have looked something like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Budget 1:&lt;/strong&gt; Interactions over 1s that block someone from doing the thing they came here to do. Unacceptable. Fix quickly.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Budget 2:&lt;/strong&gt; Major layout shifts or jank that break trust. Only allowed if there is a very good reason.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Budget 3:&lt;/strong&gt; Subtle visual quirks under 200ms that only designers and frontend devs notice. Nice to fix. Never the top of the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My actual behaviour completely ignored that structure. I treated a small, personal annoyance as a top-line performance issue, just because it was visible to me on every reload.&lt;/p&gt;

&lt;p&gt;That is how you blow a performance budget. Not just by shipping bloat. Also by burning attention on the wrong problems.&lt;/p&gt;

&lt;h2&gt;How I re-framed the font swap&lt;/h2&gt;

&lt;p&gt;Once I admitted I was optimising the wrong hill, I changed how I thought about the font swap entirely.&lt;/p&gt;

&lt;h3&gt;1. I put a hard cap on how much time I would spend&lt;/h3&gt;

&lt;p&gt;I gave myself one more afternoon. That was it. If I could not get a clearly better result in a few focused hours, I would ship the least annoying version and move on.&lt;/p&gt;

&lt;p&gt;This sounds trivial, but setting a clock is a real performance tool. It keeps polish from turning into procrastination.&lt;/p&gt;

&lt;h3&gt;2. I defined "good enough" in concrete numbers&lt;/h3&gt;

&lt;p&gt;Instead of chasing "perfect", I wrote down targets:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On good connections: font swap under 120ms for first paint.&lt;/li&gt;
  &lt;li&gt;On poor connections: allow up to 300ms, but no layout shift large enough to move a line of body text vertically.&lt;/li&gt;
  &lt;li&gt;Use &lt;code&gt;font-display: swap&lt;/code&gt; so content is always visible quickly, even if the font is slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turned the font from a source of guilt into just another constraint.&lt;/p&gt;

&lt;h3&gt;3. I aligned it with actual user flows&lt;/h3&gt;

&lt;p&gt;On this site, the home page and a few key content pages matter most.&lt;/p&gt;

&lt;p&gt;So the rule became simple:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Critical templates get the full font treatment with careful fallback tuning.&lt;/li&gt;
  &lt;li&gt;Less important sections lean more on system fonts or accept a lazier loading strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance budget per template. Not per file extension.&lt;/p&gt;

&lt;h2&gt;The version I shipped&lt;/h2&gt;

&lt;p&gt;After that last focused afternoon, I ended up with this setup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Single variable font file for body and headings, self-hosted, aggressively cached.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;&amp;lt;link rel="preload" as="font" type="font/woff2" crossorigin&amp;gt;&lt;/code&gt; for the primary text face on critical routes.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;font-display: swap&lt;/code&gt; for everything.&lt;/li&gt;
  &lt;li&gt;Fallback stack tuned to roughly match metrics for body text, slightly less tuned for headings.&lt;/li&gt;
  &lt;li&gt;No crazy FontFace API gymnastics on first load. Light JS only to avoid FOUT on route transitions in the SPA shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measured swap:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fast desktop: ~70-90ms.&lt;/li&gt;
  &lt;li&gt;Average laptop: ~120-150ms.&lt;/li&gt;
  &lt;li&gt;Mid Android on 4G: ~220-260ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flicker is technically still there. You can spot it if you record a video and scrub frame by frame. On real usage, I do not notice it anymore. Mostly because I stopped hunting for it.&lt;/p&gt;

&lt;h2&gt;What that 90ms actually taught me&lt;/h2&gt;

&lt;p&gt;The technical side of this was not new. Fonts are slow. Metrics matter. Fallbacks help. Everyone knows that story.&lt;/p&gt;

&lt;p&gt;The useful lesson was about attention.&lt;/p&gt;

&lt;h3&gt;1. Your nervous system is not a product analytics tool&lt;/h3&gt;

&lt;p&gt;Developers build an almost pathological sensitivity to micro-jank on their own work. We stare at the same transitions on 144 Hz monitors all day.&lt;/p&gt;

&lt;p&gt;That sensitivity is useful. It catches issues early. It also lies to you about priority.&lt;/p&gt;

&lt;p&gt;A 90ms hitch that you see 50 times a day will feel more important than a 1.5 second delay that you hit once a week in a deep settings panel. Your body is wrong. The metrics are right.&lt;/p&gt;

&lt;h3&gt;2. Performance budgets should be written down, not felt&lt;/h3&gt;

&lt;p&gt;If you do not write the budget down, your priorities drift toward whatever annoys you this week.&lt;/p&gt;

&lt;p&gt;For my projects now, I keep a simple document:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Target FCP / LCP brackets with acceptable ranges.&lt;/li&gt;
  &lt;li&gt;Max acceptable delay for core interactions.&lt;/li&gt;
  &lt;li&gt;Rules for web fonts, images, and third-party scripts.&lt;/li&gt;
  &lt;li&gt;A short list of "things we do not care about yet".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last category is key. It gives you explicit permission to ignore your own pet peeves until the important stuff is solid.&lt;/p&gt;

&lt;h3&gt;3. Avoid unitless "fast" and "slow"&lt;/h3&gt;

&lt;p&gt;"Feels slow" is how you start a conversation. It is not where you should end.&lt;/p&gt;

&lt;p&gt;I now try to translate every annoyance into at least one number:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"The list appears after I count to two" becomes "TTI for this interaction is ~1.9s".&lt;/li&gt;
  &lt;li&gt;"The header jumps a bit" becomes "CLS spike of 0.2 caused by the logo".&lt;/li&gt;
  &lt;li&gt;"The font twitches" becomes "swap delay of ~150ms on this device".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once it has a number, it can compete fairly with other work. Before that, it is just noise.&lt;/p&gt;

&lt;h2&gt;How I handle this now on new projects&lt;/h2&gt;

&lt;p&gt;I still care about typography. I still see micro-flickers other people miss. That has not changed.&lt;/p&gt;

&lt;p&gt;What changed is the order I tackle things.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Step 1:&lt;/strong&gt; Get core flows under a clear budget. First meaningful content under a sensible target. Critical interactions under a second whenever possible.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Step 2:&lt;/strong&gt; Fix the violent stuff. Layout shifts that move content under the cursor. Jank on scroll or drag.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Step 3:&lt;/strong&gt; Only then, spend time trimming the small visual rough edges like subtle font swaps, micro delays on icon loads, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old me would happily burn half a day chasing a 50ms improvement on a detail that barely affects anyone. The new me will still do that sometimes, but only after the big rocks are taken care of, and only within a fixed time box.&lt;/p&gt;

&lt;h2&gt;Small number, big lever&lt;/h2&gt;

&lt;p&gt;That 90ms font swap did not matter individually. Most users never noticed it. It did not move any metrics.&lt;/p&gt;

&lt;p&gt;But catching myself obsessing over it was useful. It was a mirror for how easily my attention drifts toward the fun problems instead of the important ones.&lt;/p&gt;

&lt;p&gt;If you care about performance, you probably share that tendency. You notice the tiny things first because you are trained to see them.&lt;/p&gt;

&lt;p&gt;The trick is not to stop noticing. The trick is to notice, measure, then decide consciously where that detail sits in your actual performance budget.&lt;/p&gt;

&lt;p&gt;Sometimes it will earn a spot near the top. Often it will not.&lt;/p&gt;

&lt;p&gt;Either way, make the call with numbers, not just with your nervous system.&lt;/p&gt;

&lt;p&gt;That is the real lesson I took from a flicker that lasted less than a blink.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>SnippetsLab vs a Plain Folder of Gists: 6 Months Side by Side</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:09:43 +0000</pubDate>
      <link>https://dev.to/richardlemon/snippetslab-vs-a-plain-folder-of-gists-6-months-side-by-side-3kl4</link>
      <guid>https://dev.to/richardlemon/snippetslab-vs-a-plain-folder-of-gists-6-months-side-by-side-3kl4</guid>
      <description>&lt;h2&gt;Why I Ran Two Snippet Systems At Once&lt;/h2&gt;

&lt;p&gt;I spent six months running two competing systems for code snippets.&lt;/p&gt;

&lt;p&gt;On one side: &lt;strong&gt;SnippetsLab&lt;/strong&gt; on macOS. Polished, native, search box in the menu bar, syntax highlighting for basically everything.&lt;/p&gt;

&lt;p&gt;On the other side: a &lt;strong&gt;plain folder of gists&lt;/strong&gt;. Literally a directory of &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.txt&lt;/code&gt; files, backed by Git, pushed to GitHub gists from the terminal.&lt;/p&gt;

&lt;p&gt;I did not do this as a thought experiment. I did it because my snippet situation was a mess.&lt;/p&gt;

&lt;p&gt;VS Code snippets. Old Evernote notes. Some random Notion pages. Half-remembered StackOverflow bookmarks. You know the drill.&lt;/p&gt;

&lt;p&gt;So I forced myself to pick two contenders and actually live in them. Full time. No “just this once I’ll paste it into Notes”.&lt;/p&gt;

&lt;p&gt;Here is what survived contact with reality.&lt;/p&gt;

&lt;h2&gt;The Two Setups, In Practice&lt;/h2&gt;

&lt;p&gt;This was the exact setup I ran.&lt;/p&gt;

&lt;h3&gt;SnippetsLab setup&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;SnippetsLab on macOS, from the App Store.&lt;/li&gt;
  &lt;li&gt;Library stored in iCloud Drive so both my Macs could see it.&lt;/li&gt;
  &lt;li&gt;Structure: a handful of top level folders: &lt;em&gt;css&lt;/em&gt;, &lt;em&gt;js&lt;/em&gt;, &lt;em&gt;react&lt;/em&gt;, &lt;em&gt;devops&lt;/em&gt;, &lt;em&gt;notes&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;Each snippet tagged with 2–3 tags: framework, topic, and &lt;code&gt;today&lt;/code&gt; if I created it that day.&lt;/li&gt;
  &lt;li&gt;Global hotkey to bring up the search window from anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Plain gists folder setup&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Folder: &lt;code&gt;~/code/snippets&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Each snippet is a separate file. &lt;code&gt;slugged-title.language&lt;/code&gt; or &lt;code&gt;slugged-title.md&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Backed by Git, synced to a private GitHub repo.&lt;/li&gt;
  &lt;li&gt;One script to convert a file into a GitHub gist if I want it public.&lt;/li&gt;
  &lt;li&gt;Search handled by &lt;code&gt;rg&lt;/code&gt; (ripgrep) and Spotlight. Sometimes VS Code “Open Folder”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No fancy UI. No hierarchy beyond folders. No tags. Just files and Git.&lt;/p&gt;

&lt;h2&gt;Speed Of Capture: Who Got Used More&lt;/h2&gt;

&lt;p&gt;What matters most for snippet tools is not features. It is &lt;strong&gt;time to capture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Can I throw something in there in 10 seconds, in the middle of a messy debugging session, and trust it will be findable later.&lt;/p&gt;

&lt;h3&gt;SnippetsLab capture workflow&lt;/h3&gt;

&lt;p&gt;My SnippetsLab capture flow looked like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Select code in VS Code or Chrome DevTools.&lt;/li&gt;
  &lt;li&gt;Hit the global hotkey to bring up the SnippetsLab window.&lt;/li&gt;
  &lt;li&gt;Click &lt;em&gt;New Snippet&lt;/em&gt;, paste, select language, add title, add tags, pick folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a good day that is 15–20 seconds. On a bad day I start fiddling with tags and spend a minute “organising”.&lt;/p&gt;

&lt;p&gt;It feels nice. Feels like I am curating a little personal StackOverflow. That feeling is a trap. It encourages perfectionism instead of capture.&lt;/p&gt;

&lt;h3&gt;Gists folder capture workflow&lt;/h3&gt;

&lt;p&gt;The plain-folder capture flow was much more brutal.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Copy the code.&lt;/li&gt;
  &lt;li&gt;Hit a custom Alfred workflow or shell alias that creates a timestamped file in &lt;code&gt;~/code/snippets/inbox&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Paste, maybe add one-line title at the top. Save. Done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Average time: under 10 seconds, every time. No friction. No options.&lt;/p&gt;

&lt;p&gt;After a week this became automatic. I stopped thinking about it.&lt;/p&gt;

&lt;p&gt;If I did nothing else, the inbox filled with raw snippets. Once or twice a week I cleaned it up: rename the file, move to a folder, commit, push.&lt;/p&gt;

&lt;p&gt;Because of that, the folder system ended up with &lt;strong&gt;more&lt;/strong&gt; snippets than SnippetsLab, even though SnippetsLab is objectively a nicer app.&lt;/p&gt;

&lt;h2&gt;Search: Where The Fancy UI Actually Helped&lt;/h2&gt;

&lt;p&gt;Capture speed is one side of the story. Retrieval matters more.&lt;/p&gt;

&lt;p&gt;The obvious fear with raw files is: “I will never find anything again.”&lt;/p&gt;

&lt;h3&gt;SnippetsLab search&lt;/h3&gt;

&lt;p&gt;SnippetsLab has a global search box with nice fuzzy matching. Type “react debou” and it suggests “react debounce hook”, plus any snippets tagged with &lt;code&gt;react&lt;/code&gt; or &lt;code&gt;debounce&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Syntax highlighting is clean. Multi-language snippets look fine. I liked how comments and code were visually separated. For skimming longer snippets that helps a lot.&lt;/p&gt;

&lt;p&gt;For exactly-remembered snippets SnippetsLab is fast. I type 3–4 characters and hit enter.&lt;/p&gt;

&lt;p&gt;Where it fell down for me is &lt;strong&gt;vague memory search&lt;/strong&gt;. When I barely remember what I called something and mostly remember a fragment of an error message or a particular CSS property I used.&lt;/p&gt;

&lt;p&gt;SnippetsLab search matches titles, content, and tags. It works. But it is slower and less precise than my terminal muscle memory.&lt;/p&gt;

&lt;h3&gt;Folder + ripgrep search&lt;/h3&gt;

&lt;p&gt;My folder search flow looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd ~/code/snippets
rg "setTimeout"&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or in Alfred: type &lt;code&gt;snip &amp;lt;term&amp;gt;&lt;/code&gt;, which runs &lt;code&gt;rg&lt;/code&gt; under the hood and opens matches in VS Code.&lt;/p&gt;

&lt;p&gt;For vague-memory search this is excellent. I search by random words that appear in the body of the snippet, not the title.&lt;/p&gt;

&lt;p&gt;“shadow-md hover:scale-105” will find me the tailwind card example I half remember. “ERR_INVALID_ARG_TYPE” will find the exact Node API bug I hit in March.&lt;/p&gt;

&lt;p&gt;And because it is just files, Spotlight also works as a soft backup. If I forget my own commands I can still type into macOS search and land somewhere useful.&lt;/p&gt;

&lt;p&gt;So this one surprised me. I expected SnippetsLab to win search by a mile. In practice, the &lt;strong&gt;raw power of grep + keyboard habits&lt;/strong&gt; beat the polished UI more often than not.&lt;/p&gt;

&lt;h2&gt;Structure, Tags, And The Illusion Of Organisation&lt;/h2&gt;

&lt;p&gt;I like structure. I coach baseball. I optimise sleep. I like systems.&lt;/p&gt;

&lt;p&gt;SnippetsLab is catnip for people like me. Folders, tags, colors, icons. You can create a beautiful taxonomy of your past work.&lt;/p&gt;

&lt;p&gt;Here is the problem. Maintaining that taxonomy became its own job.&lt;/p&gt;

&lt;p&gt;I would paste a 15-line snippet, then spend more time deciding “Is this &lt;em&gt;React&lt;/em&gt; or &lt;em&gt;Frontend&lt;/em&gt; or &lt;em&gt;Performance&lt;/em&gt;?” than I had spent writing the code in the first place.&lt;/p&gt;

&lt;p&gt;The folder of gists did not give me the option. It was mostly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;css&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;js&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;infra&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;random&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost everything fit into one of those. If something did not, it went to &lt;code&gt;random&lt;/code&gt;, and future-me could deal with it or ignore it.&lt;/p&gt;

&lt;p&gt;Over six months, SnippetsLab looked more organised, but the &lt;strong&gt;effective&lt;/strong&gt; organisation was not better. I still searched by keywords most of the time.&lt;/p&gt;

&lt;p&gt;Tags felt great on day one and completely optional by month three.&lt;/p&gt;

&lt;h2&gt;Sync, Backup, And Lock-In&lt;/h2&gt;

&lt;p&gt;This is where my bias really shows.&lt;/p&gt;

&lt;p&gt;I trust &lt;strong&gt;Git + plain text&lt;/strong&gt; more than any app database. I have seen enough proprietary formats die.&lt;/p&gt;

&lt;h3&gt;SnippetsLab sync&lt;/h3&gt;

&lt;p&gt;SnippetsLab stores its library in a single database file. You can sync that through iCloud or Dropbox.&lt;/p&gt;

&lt;p&gt;iCloud sync was mostly fine, but I had two annoying incidents.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Once, after a macOS update, SnippetsLab came up with an empty library for about 30 seconds while iCloud rehydrated the file. I nearly had a heart attack.&lt;/li&gt;
  &lt;li&gt;Another time, both Macs were open, and I edited the same snippet on each machine within a short window. One of the edits disappeared. No visible conflict handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither issue lost me critical data, but my trust took a hit.&lt;/p&gt;

&lt;h3&gt;Git-backed folder sync&lt;/h3&gt;

&lt;p&gt;The git-backed folder did not care about macOS updates. Or about which editor I used. Or about my future self.&lt;/p&gt;

&lt;p&gt;It is just files. I commit regularly. Push to GitHub. Occasionally pull on the other machine.&lt;/p&gt;

&lt;p&gt;Worst case, I get a merge conflict in a single snippet file. I fix it like any other code conflict.&lt;/p&gt;

&lt;p&gt;Long term, this also keeps my options open. If I decide in two years that I want to move everything into some web-based tool or a different snippet manager, I can feed it a directory of &lt;code&gt;.md&lt;/code&gt; files and call it a day.&lt;/p&gt;

&lt;p&gt;SnippetsLab has export, which is nice. But needing an export step at all is already a form of lock-in for me.&lt;/p&gt;

&lt;h2&gt;Daily Usage Patterns: What I Actually Reached For&lt;/h2&gt;

&lt;p&gt;Numbers time. Over six months, on my main Mac, I tracked my usage roughly like this.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Snippets created in SnippetsLab: around 90.&lt;/li&gt;
  &lt;li&gt;Snippets created in the folder: around 220.&lt;/li&gt;
  &lt;li&gt;Retrievals from SnippetsLab (window opened with actual copy): roughly 140.&lt;/li&gt;
  &lt;li&gt;Retrievals from the folder (terminal or VS Code search resulting in a paste): around 260.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Early on, the split was closer to 50/50. By month four it was not. I just stopped putting new things into SnippetsLab unless I needed its specific strengths.&lt;/p&gt;

&lt;p&gt;Those strengths were mostly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Multi-part snippets&lt;/strong&gt; where I wanted a description, code, and maybe a shell command together.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Visual references&lt;/strong&gt;, like when I pasted a small SVG or wanted to remember a diagram.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Presentations&lt;/strong&gt;. When I was pairing or sharing screen and wanted a nice UI to browse examples, SnippetsLab looked professional. The folder did not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everything else, the folder won just by being boring and fast.&lt;/p&gt;

&lt;h2&gt;When SnippetsLab Is Clearly Better&lt;/h2&gt;

&lt;p&gt;Despite all that, I do not think SnippetsLab is bad. Far from it. It just solves a slightly different problem well.&lt;/p&gt;

&lt;p&gt;Here is where I think SnippetsLab genuinely shines.&lt;/p&gt;

&lt;h3&gt;Teams and teaching&lt;/h3&gt;

&lt;p&gt;If you are the “developer who teaches other developers” on your team, SnippetsLab is useful.&lt;/p&gt;

&lt;p&gt;You can group snippets into collections, add clean titles and descriptions, and then screen share that while walking through an onboarding session.&lt;/p&gt;

&lt;p&gt;A raw folder in VS Code looks sloppy in that context. It is fine for me. It is not as friendly for someone new to the codebase or to the stack.&lt;/p&gt;

&lt;h3&gt;Heavier use of tags and metadata&lt;/h3&gt;

&lt;p&gt;If you love tags, and you actually use them, SnippetsLab gives you a UI that rewards that behaviour.&lt;/p&gt;

&lt;p&gt;You can think in tag intersections: “show me all &lt;em&gt;react&lt;/em&gt; snippets that touch &lt;em&gt;forms&lt;/em&gt; and &lt;em&gt;validation&lt;/em&gt;.”&lt;/p&gt;

&lt;p&gt;With plain files you can fake that with naming conventions and grep, but it is clunkier. I personally do not think in tags often enough for this to matter, but some people do.&lt;/p&gt;

&lt;h3&gt;Living entirely on macOS&lt;/h3&gt;

&lt;p&gt;If you are 100% on macOS, never touch Linux, and do not care about future portability, then the native feel of SnippetsLab is nice.&lt;/p&gt;

&lt;p&gt;The menu bar icon. The hotkeys. The minimal friction to paste into any Cocoa text field. All of that is smoother than my terminal-heavy setup for most casual users.&lt;/p&gt;

&lt;h2&gt;Why I Ultimately Stayed With The Boring Folder&lt;/h2&gt;

&lt;p&gt;After six months, I did something decisive. I turned off SnippetsLab’s auto launch.&lt;/p&gt;

&lt;p&gt;It is still installed. I still open it once in a while when I want to show someone something cleanly. But my default stack is now:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Folder of snippets under Git.&lt;/li&gt;
  &lt;li&gt;Couple of small shell scripts and Alfred workflows for capture and search.&lt;/li&gt;
  &lt;li&gt;VS Code or Neovim as the viewer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason is not that SnippetsLab is bad. It is that &lt;strong&gt;tools that pretend to be simple but add invisible friction&lt;/strong&gt; annoy me more than tools that are obviously manual and dumb.&lt;/p&gt;

&lt;p&gt;Every time SnippetsLab triggered my “organise this properly” instinct, I paid a cognitive tax. The folder never asked for that. It just accepted the mess.&lt;/p&gt;

&lt;p&gt;And I have learned that for anything I expect to live longer than a year, simplicity and portability beat polish.&lt;/p&gt;

&lt;h2&gt;How I Would Decide If I Started Fresh&lt;/h2&gt;

&lt;p&gt;If you are reading this because your snippets are scattered and you want to fix it, here is how I would choose, knowing what I know now.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If you want a &lt;strong&gt;personal snippet knowledge base&lt;/strong&gt; that feels like an app, with tags, folders, and a nice UI, use SnippetsLab.&lt;/li&gt;
  &lt;li&gt;If you want a &lt;strong&gt;low-friction, long-term, no-drama archive&lt;/strong&gt; that you can migrate easily in 5 years, use a Git-backed folder of text files.&lt;/li&gt;
  &lt;li&gt;If you work across Linux, Windows, and macOS, do not even bother with native-only tools for this. Use the folder.&lt;/li&gt;
  &lt;li&gt;If you teach often or onboard juniors and want to show snippets on calls, SnippetsLab helps you look more prepared.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honestly, you cannot really get this wrong, as long as you actually use whatever you pick for a month.&lt;/p&gt;

&lt;p&gt;That is the main lesson from my six months of running both in parallel. The real value did not come from which tool I used. It came from the simple discipline of &lt;strong&gt;capturing&lt;/strong&gt; everything I did not want to think through twice.&lt;/p&gt;

&lt;p&gt;The plain folder just made that discipline slightly easier to keep.&lt;/p&gt;

</description>
      <category>code</category>
    </item>
    <item>
      <title>Why I Still Pay For Tower When Git CLI Is Free</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:08:44 +0000</pubDate>
      <link>https://dev.to/richardlemon/why-i-still-pay-for-tower-when-git-cli-is-free-3b0e</link>
      <guid>https://dev.to/richardlemon/why-i-still-pay-for-tower-when-git-cli-is-free-3b0e</guid>
      <description>&lt;h2&gt;Git CLI is great. I still pay for Tower.&lt;/h2&gt;

&lt;p&gt;I write a lot of Git commands by hand. I like the terminal. I like muscle memory. I also pay for Tower every year and I do not feel stupid about it.&lt;/p&gt;

&lt;p&gt;This is not a pitch for Tower. Use whatever Git client you want. Or none at all. This is just my honest breakdown of when I reach for a visual Git tool instead of the CLI, and why that makes sense for how I work.&lt;/p&gt;

&lt;p&gt;Because on paper, the CLI does it all. It is free, fast, scriptable, and lives in the same place as the code. So why do I still double-click a Git app?&lt;/p&gt;

&lt;h2&gt;Where my Git muscle memory actually stops&lt;/h2&gt;

&lt;p&gt;I am comfortable in Git. I have the usual aliases. &lt;code&gt;gst&lt;/code&gt; for status. &lt;code&gt;gco&lt;/code&gt; for checkout. &lt;code&gt;gb&lt;/code&gt; for branch. The basics live in my fingers.&lt;/p&gt;

&lt;p&gt;Here is the rough list of things I still instinctively do in the terminal:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Creating branches: &lt;code&gt;gco -b feature/x&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;Small, obvious commits: &lt;code&gt;git commit -am "Fix padding"&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;Quick rebases: &lt;code&gt;git pull --rebase origin main&lt;/code&gt;
&lt;/li&gt;
  &lt;li&gt;Interactive staging with &lt;code&gt;git add -p&lt;/code&gt; when I am already in flow&lt;/li&gt;
  &lt;li&gt;Simple logs: &lt;code&gt;git log --oneline --graph --decorate -20&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I know exactly what I want and I do it often enough, the CLI wins. It is faster, and my brain goes straight from intention to command.&lt;/p&gt;

&lt;p&gt;The problem is not the commands I know by heart. The problem starts when the operation is slightly weird, has a few steps, or involves history surgery that I do only once every few weeks.&lt;/p&gt;

&lt;h2&gt;The honest list of things I kept Googling&lt;/h2&gt;

&lt;p&gt;If you have used Git for a while, you know this feeling. You want to fix something in history or clean up a messy branch, and suddenly you are on page three of Stack Overflow threads from 2014.&lt;/p&gt;

&lt;p&gt;Before I started using Tower seriously, I noticed a clear pattern. These are some of the things that always broke my flow in the terminal:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Reordering or dropping a bunch of commits from a feature branch before opening a PR&lt;/li&gt;
  &lt;li&gt;Splitting one big, lazy commit into two meaningful ones&lt;/li&gt;
  &lt;li&gt;Staging very specific hunks across multiple files while keeping others for later&lt;/li&gt;
  &lt;li&gt;Undoing a commit that was already pushed, but safely, without nuking someone else’s work&lt;/li&gt;
  &lt;li&gt;Tracking which branch came from where in a long-lived feature branch jungle&lt;/li&gt;
  &lt;li&gt;Figuring out what exactly changed in a rebase after I finished it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Could I do all of this in the CLI? Of course. I did for years. With notes. And aliases. And occasional regret.&lt;/p&gt;

&lt;p&gt;The problem is not capability. It is cognitive load. If every complex Git action costs me three tabs, a few blog posts, and a test repo to practice on, my actual work slows down.&lt;/p&gt;

&lt;h2&gt;Visual command versus muscle memory&lt;/h2&gt;

&lt;p&gt;The way I think about this is simple. The CLI is muscle memory. Tower is visual command.&lt;/p&gt;

&lt;p&gt;Muscle memory is cheap and fast as long as the pattern is fixed. &lt;code&gt;git commit&lt;/code&gt; will always be &lt;code&gt;git commit&lt;/code&gt;. You do it ten times a day, it becomes automatic. Perfect.&lt;/p&gt;

&lt;p&gt;Visual command is different. Instead of memorizing syntax, you visually inspect the thing you want to change, then you manipulate it directly. This costs more in UI overhead, but less in mental syntax juggling.&lt;/p&gt;

&lt;p&gt;Git, in particular, is a tool where the data structure matters. Branches, parents, merges, detached heads, upstreams. It is an actual graph, not just a list of actions. A graph wants a visual.&lt;/p&gt;

&lt;p&gt;Tower gives me that graph, plus verbs attached to it. Click a commit, see what it touches, and decide what to do with it. That is the core of the value for me.&lt;/p&gt;

&lt;h2&gt;Where Tower actually earns its subscription&lt;/h2&gt;

&lt;p&gt;Here are the concrete workflows where I happily leave the terminal and open Tower. This is where the subscription pays for itself in my head.&lt;/p&gt;

&lt;h3&gt;1. Cleaning up a branch before a PR&lt;/h3&gt;

&lt;p&gt;On side projects, I am sloppy. I commit half-baked stuff, I spike features, I throw in TODO comments. Before opening a real PR, I like to clean the history so it tells a sane story.&lt;/p&gt;

&lt;p&gt;In Tower, I do this visually:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I open the branch and see the commit list as a tree&lt;/li&gt;
  &lt;li&gt;I scan the messages for noise like “temp test” or “oops”&lt;/li&gt;
  &lt;li&gt;I select a range of commits and use interactive rebase from the menu&lt;/li&gt;
  &lt;li&gt;I reorder or squash with drag and drop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could use &lt;code&gt;git rebase -i HEAD~7&lt;/code&gt;. I know the syntax. I have done it many times. I still prefer dragging commits around and seeing the result as a continuous tree.&lt;/p&gt;

&lt;p&gt;The visual flow also lowers the “oh no, I ruined everything” anxiety. I can see what will happen before I hit continue. That matters more than I want to admit.&lt;/p&gt;

&lt;h3&gt;2. Staging exactly what I mean&lt;/h3&gt;

&lt;p&gt;Git staging is underrated. I think more people should use it deliberately. A good commit tells a focused story. The problem is that partial changes across many files are hard to juggle in your head.&lt;/p&gt;

&lt;p&gt;In the CLI, &lt;code&gt;git add -p&lt;/code&gt; is powerful, but the UX is cramped. Tiny diff windows, single-character commands, hard to jump around between files.&lt;/p&gt;

&lt;p&gt;In Tower, the staging panel is where I live when I am shaping a commit:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I see all changed files, grouped by directory&lt;/li&gt;
  &lt;li&gt;I click through diffs with a big readable view&lt;/li&gt;
  &lt;li&gt;I stage individual hunks or even individual lines with the mouse&lt;/li&gt;
  &lt;li&gt;If a file belongs to two different logical changes, I split it cleanly between two commits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is that my commit history actually reflects how the feature evolved. Not just random save points.&lt;/p&gt;

&lt;p&gt;Could I force myself to do this in the terminal? Yes. Would I actually do it consistently on a Friday afternoon? Probably not. The visual tool makes the good habit easier.&lt;/p&gt;

&lt;h3&gt;3. Understanding weird repository history&lt;/h3&gt;

&lt;p&gt;Every repo has that one branch that lived too long. Or that one merge from a few months ago that introduced something cursed.&lt;/p&gt;

&lt;p&gt;When I need to understand how we got here, I do not want to read walls of text logs. I want a map.&lt;/p&gt;

&lt;p&gt;In Tower, I can:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Scroll through the full graph of branches and merges&lt;/li&gt;
  &lt;li&gt;See where a feature branched off main and when it came back&lt;/li&gt;
  &lt;li&gt;Filter by branch, author, or search for “payment” in commit messages&lt;/li&gt;
  &lt;li&gt;Click a merge commit and see exactly what it merged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of forensics is possible in the CLI with the right combination of &lt;code&gt;git log&lt;/code&gt; flags. I have a few presets. They are still not as immediate as looking at a graph.&lt;/p&gt;

&lt;p&gt;The payoff is simple. I reach understanding faster. That reduces the odds that I fix the wrong thing or patch over a deeper issue.&lt;/p&gt;

&lt;h3&gt;4. Recovering from mistakes without panic&lt;/h3&gt;

&lt;p&gt;Everyone messes up Git sooner or later. You reset the wrong thing. You hard force push. You commit to main by accident.&lt;/p&gt;

&lt;p&gt;I am not immune. When I screw up, I care less about &lt;em&gt;how&lt;/em&gt; to undo it and more about &lt;em&gt;not making it worse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Tower helps with that by making history recovery visible:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The reflog is in my face. I can literally see “where” I was before.&lt;/li&gt;
  &lt;li&gt;I can right-click an old state and reset the branch to it with options.&lt;/li&gt;
  &lt;li&gt;I can create a new branch from a previous commit before I touch anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, I could use &lt;code&gt;git reflog&lt;/code&gt; and manually copy hashes. I have done that a lot. In a slightly stressful moment, I prefer clicking the known good state in a list and moving on.&lt;/p&gt;

&lt;h2&gt;Why I do not use Tower for everything&lt;/h2&gt;

&lt;p&gt;This is important. I do not think a Git GUI replaces the CLI. I think it sits next to it.&lt;/p&gt;

&lt;p&gt;Here are situations where I almost always stay in the terminal:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Small projects or quick fixes where I know I will only do one or two commits&lt;/li&gt;
  &lt;li&gt;Working over SSH on a remote machine or Codespace&lt;/li&gt;
  &lt;li&gt;Automating things in scripts or git hooks&lt;/li&gt;
  &lt;li&gt;Anytime I am already deep in a Tmux / Vim workflow and opening a GUI would break flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes Tower is open all day. Sometimes it stays closed for a week and everything happens in the CLI. That is fine. I treat it like a power tool, not a mandatory layer.&lt;/p&gt;

&lt;h2&gt;Why not just use the Git panel in the editor?&lt;/h2&gt;

&lt;p&gt;Most editors ship with some kind of Git integration. VS Code. WebStorm. You name it. So why not just use that instead of paying for a dedicated client?&lt;/p&gt;

&lt;p&gt;I tried that route for a while. My personal issues with editor Git panels:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;They are great for simple stage / commit / push loops, but weak for deep history work.&lt;/li&gt;
  &lt;li&gt;The graph views are often cramped inside a sidebar with too little space.&lt;/li&gt;
  &lt;li&gt;More complex operations still fall back to the terminal anyway.&lt;/li&gt;
  &lt;li&gt;I like my editor focused on code, not on being a full Git dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tower feels like a separate, intentional context. When I open it, I am in "version control mode". I am not tempted to tweak code while I am trying to fix history. That separation is healthy for me.&lt;/p&gt;

&lt;h2&gt;The real cost: time, not money&lt;/h2&gt;

&lt;p&gt;Tower costs money. The CLI does not. That is the usual objection.&lt;/p&gt;

&lt;p&gt;Here is how I think about it. I probably lose more money per month accidentally doomscrolling Twitter for ten minutes a day than I pay for Tower. The actual cost that hurts is time and mental friction.&lt;/p&gt;

&lt;p&gt;If Tower saves me from two or three Git rabbit holes per month, it pays for itself. By “saves” I mean:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fewer mistakes when rewriting history&lt;/li&gt;
  &lt;li&gt;Faster understanding of complicated merge situations&lt;/li&gt;
  &lt;li&gt;Less context switching between browser, docs, and a test repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not religious about tools. If Tower vanished tomorrow, I would survive in pure CLI again. I just know my own patterns well enough now to admit that a good visual Git client makes me slightly more dangerous with less stress.&lt;/p&gt;

&lt;h2&gt;When a visual Git tool actually makes sense&lt;/h2&gt;

&lt;p&gt;I think a lot of "CLI vs GUI" debates are silly. The better framing is "what are you trying to minimize".&lt;/p&gt;

&lt;p&gt;If you want to minimize dependencies and external tools, stay in the terminal and learn the deep Git stuff. That is a good path. You will not regret knowing Git well.&lt;/p&gt;

&lt;p&gt;If you want to minimize context switching, errors under pressure, and repetitive Googling for arcane flags that you only use twice a year, then a visual tool starts to make sense.&lt;/p&gt;

&lt;p&gt;For me, Tower hits a sweet spot:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CLI for fast, daily operations and automation&lt;/li&gt;
  &lt;li&gt;Tower for history surgery, commit shaping, and visual understanding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool is not the point. The point is getting confident enough with your version control that it almost disappears. I care about the code and the product more than how I told Git to shuffle bytes around.&lt;/p&gt;

&lt;p&gt;So I pay for Tower. Not because the CLI is lacking, but because I would rather spend my brain cycles on the weird CSS edge case or that performance bug in the frontend instead of re-learning a rebase incantation I forgot three months ago.&lt;/p&gt;

&lt;p&gt;Your threshold might be different. That is fine. But if you have been pretending the Git CLI gives you everything you need while secretly dreading every non-trivial history change, a visual client might be the cheaper thing to upgrade.&lt;/p&gt;

</description>
      <category>git</category>
    </item>
    <item>
      <title>I Would Rather Publish 600 Useful Words</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:07:37 +0000</pubDate>
      <link>https://dev.to/richardlemon/i-would-rather-publish-600-useful-words-4f1f</link>
      <guid>https://dev.to/richardlemon/i-would-rather-publish-600-useful-words-4f1f</guid>
      <description>&lt;h2&gt;The problem with chasing 1,000+ words&lt;/h2&gt;

&lt;p&gt;I used to have a simple rule for publishing: if a post was under 1,000 words, it was not done. Longer meant more authoritative, more “in-depth,” more likely to rank. That was the story I told myself.&lt;/p&gt;

&lt;p&gt;When I read back through those posts now, I can see the point where the useful part ends and the vanity metric begins. It is usually somewhere around the 500-word mark. After that, the signal drops and the padding starts.&lt;/p&gt;

&lt;p&gt;The extra words were not extra depth. They were throat-clearing, bonus anecdotes, and slightly different ways of saying the same thing. The reader was paying with attention for my decision to optimize for a number instead of their time.&lt;/p&gt;

&lt;h2&gt;How the “SEO sweet spot” bent the writing&lt;/h2&gt;

&lt;p&gt;The “SEO sweet spot” is a neat story: Google likes 1,200–1,500 word posts, so if you want traffic, you should write 1,200–1,500 word posts. I believed it, so I engineered my structure to hit that range.&lt;/p&gt;

&lt;p&gt;On paper, it worked. Traffic ticked up. The problem showed up one layer deeper. Engagement time per word went down. People were skimming, not reading.&lt;/p&gt;

&lt;p&gt;Heatmaps made the pattern obvious. There was always a drop-off cliff in the same place: right after the article had actually answered the question. Everything after that was written for an algorithm, not a human being.&lt;/p&gt;

&lt;p&gt;That is the trap with fixed word counts. You start from the number and build out, instead of starting from the idea and cutting back. You may get more visitors, but you train them to treat your work like something to mine, not something to read.&lt;/p&gt;

&lt;h2&gt;What changed: write the idea, then cut until it hurts&lt;/h2&gt;

&lt;p&gt;I eventually flipped the rule. Instead of “hit 1,000+ words,” it became: write the idea, then cut until it hurts.&lt;/p&gt;

&lt;p&gt;The editing test is simple: if I can remove a sentence without breaking the logic chain, it goes. If the paragraph is only there to make the post feel more substantial, it goes. If an anecdote repeats a point I have already made, it goes, even if it is a good story.&lt;/p&gt;

&lt;p&gt;What survives that process is usually around 500–700 words. Not because I am aiming for short, but because the fluff cannot defend itself. A 600-word post that survives this kind of editing is denser than a 1,300-word post that survives padding.&lt;/p&gt;

&lt;p&gt;My best-performing post this year was about 580 words. It got more saves, more shares, and more return visits than any 1,500-word explainer I have written.&lt;/p&gt;

&lt;h2&gt;Word counts create fake “done” signals&lt;/h2&gt;

&lt;p&gt;There is a very particular satisfaction in watching a counter tick past 1,000. It feels like a level-up animation. You hit the target, so your brain labels the task complete.&lt;/p&gt;

&lt;p&gt;The problem is that the counter is lying. I started noticing that many of my “finished” long posts were essentially extended introductions. I had spent hundreds of words setting context nobody asked for, then finally squeezed the actual argument into the last third.&lt;/p&gt;

&lt;p&gt;Fixed word counts are good at one thing: creating the illusion of completion. They tell you that you have done enough, whether or not the reader has what they came for. The real question is simpler: can someone act on this, or think differently because of it? If the answer is yes at 450 words, then the post is finished at 450 words. If the answer is no at 1,800 words, then the post is not finished, it is just long.&lt;/p&gt;

&lt;h2&gt;The padding tax and reader trust&lt;/h2&gt;

&lt;p&gt;Padding is not free. There is a tax, and it is paid in reader trust.&lt;/p&gt;

&lt;p&gt;When someone opens your article about a simple idea and sees a solid wall of text, they make a quick judgment: this is going to be work. If they push through and discover that the core idea could have fit on a single screen, they remember that too.&lt;/p&gt;

&lt;p&gt;I was training my audience to expect bloat, which meant they stopped opening my emails. After switching to shorter, tighter pieces, my open rates climbed and unsubscribes dropped. The lesson was brutal: respect for a reader’s time compounds faster than keyword density.&lt;/p&gt;

&lt;h2&gt;Shorter posts expose fuzzy thinking&lt;/h2&gt;

&lt;p&gt;The dirty secret of padded writing is that it hides fuzzy thinking. If you are not sure what you believe yet, it is easy to wander around the topic for 1,200 words and hope the point emerges somewhere in the middle.&lt;/p&gt;

&lt;p&gt;A 600-word limit removes that hiding place. You have to know your thesis, your supporting evidence, and your landing point before you start shaping sentences. The meandering draft still happens, but it stays in your notes, not in front of the reader.&lt;/p&gt;

&lt;p&gt;The trade-off is simple: writing gets harder and reading gets easier. You spend more effort deciding what actually matters, and the path through the idea gets shorter and clearer.&lt;/p&gt;

&lt;p&gt;If a paragraph cannot justify its existence, it does not get a spot. That pressure improves the thinking as much as the prose.&lt;/p&gt;

&lt;h2&gt;Measuring “insight per scroll” instead of length&lt;/h2&gt;

&lt;p&gt;I now think about posts in terms of “insight per scroll.” How much value does a reader get before they have to flick their finger again?&lt;/p&gt;

&lt;p&gt;A useful 600-word piece delivers its core value quickly. A padded 1,300-word piece makes the reader work for the same reward. One respects that people have other things to do. The other assumes they are happy to sit through a monologue because you formatted it nicely.&lt;/p&gt;

&lt;p&gt;My favorite compliment on an article is not “this is comprehensive.” It is “read this, it is quick.” That sentence means the sender trusts me not to waste their colleague’s time. It means the recipient is more likely to actually read the thing instead of parking it in a “later” folder that never gets opened.&lt;/p&gt;

&lt;p&gt;I would rather be the writer people are glad they clicked than the one they keep meaning to get around to.&lt;/p&gt;

&lt;h2&gt;The new rule&lt;/h2&gt;

&lt;p&gt;The rule now is simple: I would rather publish 600 useful words than pad them to 1,300. I still draft long. I still wander in private. But what ships is the version where every paragraph has a job.&lt;/p&gt;

&lt;p&gt;If the idea is done at 380 words, it goes out at 380. If it genuinely needs 2,000, it gets 2,000, but it has to fight for every line. The metric is no longer word count. The metric is whether someone can read the piece once and feel their time was well spent.&lt;/p&gt;

&lt;p&gt;The internet does not need more long posts. It needs more finished ideas.&lt;/p&gt;

</description>
      <category>seo</category>
    </item>
    <item>
      <title>The Two-Minute Rule I Stole From GTD And Actually Kept</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:06:54 +0000</pubDate>
      <link>https://dev.to/richardlemon/the-two-minute-rule-i-stole-from-gtd-and-actually-kept-43c8</link>
      <guid>https://dev.to/richardlemon/the-two-minute-rule-i-stole-from-gtd-and-actually-kept-43c8</guid>
      <description>&lt;h2&gt;The only survivor from GTD&lt;/h2&gt;

&lt;p&gt;I tried to go full GTD once. The whole thing. Capturing, contexts, weekly reviews, the giant master list of everything I could ever care about.&lt;/p&gt;

&lt;p&gt;I lasted about three weeks.&lt;/p&gt;

&lt;p&gt;What survived was one tiny rule. The two-minute rule. Everything else went in the bin.&lt;/p&gt;

&lt;p&gt;The funny part is that this one piece does more for my actual day than any fancy app, PARA system, or color-coded calendar I ever tried to force on my life.&lt;/p&gt;

&lt;h2&gt;The rule, spelled out like I actually use it&lt;/h2&gt;

&lt;p&gt;The textbook GTD version is:&lt;/p&gt;

&lt;p&gt;If something takes less than two minutes, do it now.&lt;/p&gt;

&lt;p&gt;My version is slightly different:&lt;/p&gt;

&lt;p&gt;If something takes less than two minutes, and it prevents future friction for me or someone I care about, I do it now.&lt;/p&gt;

&lt;p&gt;That extra clause matters. It keeps me from using the rule as an excuse to ping-pong through shallow work all day.&lt;/p&gt;

&lt;h2&gt;Why the rest of GTD bounced off me&lt;/h2&gt;

&lt;p&gt;I do not hate GTD. I just do not live like the person it was written for.&lt;/p&gt;

&lt;p&gt;I have a weird stack of roles. I ship web experiences for clients. I tinker with weird side projects. I coach baseball. I track training and recovery because my body is basically a lab experiment at this point.&lt;/p&gt;

&lt;p&gt;Context lists like &lt;em&gt;&lt;a class="mentioned-user" href="https://dev.to/office"&gt;@office&lt;/a&gt;&lt;/em&gt; and &lt;em&gt;@computer&lt;/em&gt; make zero sense when the office is my laptop, my kitchen table, or the dugout, depending on the day.&lt;/p&gt;

&lt;p&gt;Also, I found the overhead brutal. Capturing everything. Processing everything. Maintaining contexts. Weekly reviews that felt like accounting.&lt;/p&gt;

&lt;p&gt;I would spend a whole Saturday morning "getting current" in some app instead of actually doing anything that moved life forward. My brain eventually tagged the whole GTD stack as overhead and I stopped touching it.&lt;/p&gt;

&lt;p&gt;The two-minute rule was different. It did not require a new app, a new identity, or a Sunday ceremony. It just sat quietly in my head and made decisions easier.&lt;/p&gt;

&lt;h2&gt;Where the rule lives in my day&lt;/h2&gt;

&lt;p&gt;I do not have a timer going. I am not counting seconds. Two minutes is a feeling, not a measurement.&lt;/p&gt;

&lt;p&gt;Here are the moments where the rule actually runs my life.&lt;/p&gt;

&lt;h3&gt;1. The inbox scan I actually tolerate&lt;/h3&gt;

&lt;p&gt;I do not live in email, but I do pass through it a few times a day.&lt;/p&gt;

&lt;p&gt;When I open it, I flick through and mentally tag each thing.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Under two minutes and important&lt;/strong&gt;: reply, archive, move on.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Under two minutes and pointless&lt;/strong&gt;: unsubscribe, filter, or delete.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Over two minutes&lt;/strong&gt;: it goes on a separate list with a verb and a tiny next step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Verb and tiny next step" matters here. Not "Redesign client's hero section". Instead I write "Draft alt hero layout idea A".&lt;/p&gt;

&lt;p&gt;The result is boring but powerful. My inbox is not a task manager. It is just messages. The actual work lives somewhere else, described clearly.&lt;/p&gt;

&lt;p&gt;The two-minute rule is the thing that forces that separation. It stops me from turning every email session into accidental deep work. I respond to the quick ones, log the real work, and leave.&lt;/p&gt;

&lt;h3&gt;2. Code friction clean-up&lt;/h3&gt;

&lt;p&gt;Most of my work sits inside a code editor. That is where I notice little annoyances.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A misnamed component.&lt;/li&gt;
  &lt;li&gt;A CSS variable that is obviously wrong.&lt;/li&gt;
  &lt;li&gt;A console.log I forgot three commits ago.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Old me would think: "I will refactor this properly later" and then never do it. Because "later" would require a whole mental setup and a dedicated block of time.&lt;/p&gt;

&lt;p&gt;Two-minute rule me does this instead:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If renaming that component and updating imports takes under two minutes, I just do it.&lt;/li&gt;
  &lt;li&gt;If deleting the logs and running tests takes under two minutes, I just do it.&lt;/li&gt;
  &lt;li&gt;If it clearly needs more time, I drop a quick comment or a tiny TODO and move on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not about "always keep your codebase pristine". I do not live in some clean-architecture fantasy.&lt;/p&gt;

&lt;p&gt;It is about removing small future landmines immediately, while momentum is still there. Because those landmines show up later when I am trying to actually ship something.&lt;/p&gt;

&lt;h3&gt;3. Life admin that used to rot in the corner&lt;/h3&gt;

&lt;p&gt;Everyone has a life-admin pile. Mine used to live as a stack of envelopes near the door and a few innocent looking tabs in Safari.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Reply to the club about training slots.&lt;/li&gt;
  &lt;li&gt;Send a photo for some registration thing.&lt;/li&gt;
  &lt;li&gt;Sign a document that has already been sitting there for a week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-minute rule turned into a simple habit here.&lt;/p&gt;

&lt;p&gt;Every time I notice one of these, I ask myself a blunt question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Am I really so busy I cannot spare ninety seconds for this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ninety percent of the time, the honest answer is no. So I do it. Right there. Right then. No ceremony. No task manager. Just done.&lt;/p&gt;

&lt;p&gt;This matters more than it looks. Those tiny items carry a weird cognitive load. It is not the task that drains you. It is the constant background reminder that you are the type of person who does not return simple forms.&lt;/p&gt;

&lt;h2&gt;Where I deliberately ignore the rule&lt;/h2&gt;

&lt;p&gt;The two-minute rule can wreck your focus if you apply it like a zealot. It can turn your day into a Twitch stream of micro-tasks.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. I once tried to keep a two-minute-clean desk at all times. Any stray paper, cable, or sticky note triggered the rule.&lt;/p&gt;

&lt;p&gt;I got a tidy desk and a shattered attention span.&lt;/p&gt;

&lt;p&gt;So now I have a few clear carve-outs.&lt;/p&gt;

&lt;h3&gt;1. Deep work blocks are a no-fly zone&lt;/h3&gt;

&lt;p&gt;When I sit down for an actual deep work block, I draw a hard line. No two-minute tasks unless something is literally blocking my current work.&lt;/p&gt;

&lt;p&gt;If I am in the middle of building a complex animation or debugging some weird layout glitch, email does not matter. Slack does not matter. The laundry can unionize for all I care.&lt;/p&gt;

&lt;p&gt;During those blocks, the two-minute rule is scoped only to the current task.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rename the variable so the next line makes sense? Yes, do it.&lt;/li&gt;
  &lt;li&gt;Extract a tiny helper while I am here? Sure, if it is quick.&lt;/li&gt;
  &lt;li&gt;Refactor the whole module? No. That is a different session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the rule from expanding sideways into a productivity trap. I want it vertical. Only down into the current thing I am doing.&lt;/p&gt;

&lt;h3&gt;2. Notifications do not win just because they are short&lt;/h3&gt;

&lt;p&gt;Most notifications are technically under two minutes to respond to. That does not mean they deserve the front row of my brain.&lt;/p&gt;

&lt;p&gt;I keep notifications aggressively trimmed. No pop-ups from email. No badges screaming in the dock. Messaging apps are pull, not push.&lt;/p&gt;

&lt;p&gt;The two-minute rule only applies when I have chosen to look at a channel. Not when the channel waves at me from the corner like a needy Tamagotchi.&lt;/p&gt;

&lt;h3&gt;3. "Under two minutes" is about doing, not deciding&lt;/h3&gt;

&lt;p&gt;This one took me a while.&lt;/p&gt;

&lt;p&gt;I used to burn ten minutes deciding whether something was a two-minute task. Which sort of defeats the purpose.&lt;/p&gt;

&lt;p&gt;Now I use a simple heuristic.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If I can see the whole task in my head, with no branches, it probably fits.&lt;/li&gt;
  &lt;li&gt;If I catch myself thinking "and then maybe I could also" it does not fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That tiny distinction keeps me honest. The two-minute rule is for doing, not for planning more work while pretending I am being efficient.&lt;/p&gt;

&lt;h2&gt;Why this one rule stuck when the rest did not&lt;/h2&gt;

&lt;p&gt;Most productivity systems ask you to reorganize your whole life. New app. New tags. New review rituals. A new identity as a "system person".&lt;/p&gt;

&lt;p&gt;The two-minute rule did not ask for any of that. It behaved more like a patch for my existing OS.&lt;/p&gt;

&lt;p&gt;Here is why I think it survived.&lt;/p&gt;

&lt;h3&gt;1. It plays nice with chaos&lt;/h3&gt;

&lt;p&gt;My days are lumpy. Some blocks are pure maker work. Some are coaching. Some are life admin thrown in the middle because the world refuses to respect my calendar.&lt;/p&gt;

&lt;p&gt;The two-minute rule does not care about that mix. It just waits patiently for a decision point, then offers a simple fork.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Yes&lt;/strong&gt;, it is small and prevents future friction, do it now.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;No&lt;/strong&gt;, it is not small, capture it somewhere you trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No new UI. No fancy method. Just a mental if-statement I run on the fly.&lt;/p&gt;

&lt;h3&gt;2. It shrinks the "open loop" tax&lt;/h3&gt;

&lt;p&gt;GTD talks a lot about open loops. Stuff your brain keeps tracking because you have not parked it somewhere reliable.&lt;/p&gt;

&lt;p&gt;For me, the worst open loops were not the big projects. Those are obvious. They get space on the calendar. They get their own notes. They get talked about.&lt;/p&gt;

&lt;p&gt;The worst ones were stupidly small.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Reply to that parent about Saturday practice."&lt;/li&gt;
  &lt;li&gt;"Book physio before that minor ache becomes a major problem."&lt;/li&gt;
  &lt;li&gt;"Pay that annoying 23 euro invoice."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-minute rule kills those loops before they get a chance to become psychological clutter. It is like mental garbage collection that runs constantly in the background.&lt;/p&gt;

&lt;h3&gt;3. It respects momentum&lt;/h3&gt;

&lt;p&gt;Big systems want you to step out of your flow, capture, categorize, and then re-enter.&lt;/p&gt;

&lt;p&gt;That works in theory. In practice it feels like stopping a good bike ride because you saw a poster about better cycling posture.&lt;/p&gt;

&lt;p&gt;The two-minute rule respects momentum. If you are already looking at the thing, already thinking about the thing, and doing the thing would take less time than describing it, just do it.&lt;/p&gt;

&lt;p&gt;No context switch. No ceremony. Back to real work.&lt;/p&gt;

&lt;h2&gt;How I would use it if I was starting from zero&lt;/h2&gt;

&lt;p&gt;If I somehow lost all my systems tomorrow and had to rebuild from scratch, I would start with this rule and a basic capture list. Nothing else.&lt;/p&gt;

&lt;p&gt;The setup would look like this.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One place to capture anything that is clearly more than two minutes. Could be a notebook, a plain text file, or a simple app. I would not overthink it.&lt;/li&gt;
  &lt;li&gt;A conscious decision that email, messages, and admin only get attention at specific times, not all day.&lt;/li&gt;
  &lt;li&gt;The two-minute rule running as a filter whenever I touch those channels or notice a small physical task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No tags. No contexts. No complex review cycles yet. Just one question, asked repeatedly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I end this in under two minutes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If yes, I do it now. If no, I write a tiny, concrete next step on the list and move on.&lt;/p&gt;

&lt;h2&gt;Some concrete examples from last week&lt;/h2&gt;

&lt;p&gt;To make this less abstract, here is a tiny grab bag from a random week.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Client asked for a small copy tweak on a landing page. Opening the CMS, changing three words, re-reading, and hitting publish took about ninety seconds. I did it during my email pass instead of creating a ticket.&lt;/li&gt;
  &lt;li&gt;My catcher asked if we could move Saturday's bullpen by thirty minutes. I checked the calendar, replied, and immediately updated the shared schedule. Under two minutes. No "remember to update it later" note lurking in my head.&lt;/li&gt;
  &lt;li&gt;Noticed a misleading label on a toggled state in a React component. The fix was one line of JSX and a quick test in the browser. I patched it on the spot instead of leaving a TODO.&lt;/li&gt;
  &lt;li&gt;Got a letter that needed a signature and a photo of an ID. I used the phone, scanned it, sent the email reply immediately. Envelope went straight into recycling instead of living on the table for a week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these tasks are interesting. That is the point. They are exactly the kind of low-drama stuff that accumulates and quietly drains your attention if you do not swat them early.&lt;/p&gt;

&lt;h2&gt;Keeping the rule small on purpose&lt;/h2&gt;

&lt;p&gt;People love turning simple ideas into capital-S Systems. I try very hard not to do that here.&lt;/p&gt;

&lt;p&gt;I never track how many two-minute tasks I complete. There is no leaderboard. No automation. No fancy shortcut that pops up a timer every time I touch a keyboard.&lt;/p&gt;

&lt;p&gt;The power is in the simplicity.&lt;/p&gt;

&lt;p&gt;See a small task. Ask if it really is small. If yes, and if it removes future friction, kill it now. Then go back to the real work that actually matters.&lt;/p&gt;

&lt;p&gt;The rest of GTD did not survive contact with my life. This one tiny rule did. That is good enough for me.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>learning</category>
      <category>productivity</category>
      <category>watercooler</category>
    </item>
    <item>
      <title>When smooth animations make a fast site feel slow</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:06:06 +0000</pubDate>
      <link>https://dev.to/richardlemon/when-smooth-animations-make-a-fast-site-feel-slow-4ojo</link>
      <guid>https://dev.to/richardlemon/when-smooth-animations-make-a-fast-site-feel-slow-4ojo</guid>
      <description>&lt;h2&gt;When smooth feels broken&lt;/h2&gt;

&lt;p&gt;A 600&amp;nbsp;ms ease-out transition looks great in a prototype. In a real workflow, it can feel like the site is ignoring you.&lt;/p&gt;

&lt;p&gt;I built a hover-reveal navigation panel for a client that eased in over 500&amp;nbsp;ms and eased out over 400&amp;nbsp;ms. In a user-testing session, three out of five people clicked the same menu item twice because the delay made them think the first click had not registered.&lt;/p&gt;

&lt;p&gt;The animation was technically flawless. It just inserted a perceptual gap between intent and action. I cut it to 150&amp;nbsp;ms with a simple ease-out, and the double-click problem vanished. Smoothness had been masquerading as broken behavior.&lt;/p&gt;

&lt;h2&gt;Staggered entrances and the repeat-visit tax&lt;/h2&gt;

&lt;p&gt;Staggered entrance animations are a classic first-impression trick. They are also a quiet tax on anyone who has to use the interface more than once.&lt;/p&gt;

&lt;p&gt;I implemented a cascading fade-in for dashboard cards, each tile delayed by 80&amp;nbsp;ms to create a wave effect on load. During the first visit, it felt premium. By the third visit, a user described the site as “heavy,” even though Lighthouse scores were green.&lt;/p&gt;

&lt;p&gt;The animation was creating a felt performance cost that no metric captured. I replaced the stagger with a single 100&amp;nbsp;ms fade for all cards, and feedback shifted to “snappy.” Delight on the first impression cannot justify friction on the tenth.&lt;/p&gt;

&lt;h2&gt;Parallax and the one input you should not fight&lt;/h2&gt;

&lt;p&gt;Scroll is already a compromised input. Trackpads, mice, touchpads, touchscreens all feel different, and users often blame themselves when things feel off.&lt;/p&gt;

&lt;p&gt;I added a subtle parallax layer to a product landing page, with background images moving at 0.5x scroll speed. It looked cinematic in the mockup, but on a laptop with a trackpad, the scroll felt muddy. Users were not blaming their hardware; they were blaming the site.&lt;/p&gt;

&lt;p&gt;I removed the parallax entirely and replaced it with a static hero image. The animation had made the core interaction feel broken, and “broken” reads as “slow.” If your effect interferes with scroll, you are taxing the one gesture people already struggle to control.&lt;/p&gt;

&lt;h2&gt;Skeleton screens that teach users to expect slowness&lt;/h2&gt;

&lt;p&gt;Loading skeletons are supposed to make waiting feel better. If you are not careful, they train users to expect that waiting is the default.&lt;/p&gt;

&lt;p&gt;I implemented a pulsing skeleton screen for a data-heavy page, with a 2&amp;nbsp;second crossfade to real content. The actual API response was averaging 400&amp;nbsp;ms, but because the skeleton had to play out, users perceived the load time as 2&amp;nbsp;seconds. They started assuming the site was slow before the content ever arrived.&lt;/p&gt;

&lt;p&gt;I removed the skeleton and let the raw content pop in at 400&amp;nbsp;ms with a 50&amp;nbsp;ms opacity fade. Perceived speed is not actual speed, and animation is often the lens that distorts the difference.&lt;/p&gt;

&lt;h2&gt;Micro-interactions and aggregate drag&lt;/h2&gt;

&lt;p&gt;Most performance discussions focus on frame rates and timings in isolation. Users do not experience them that way. They feel the sum.&lt;/p&gt;

&lt;p&gt;I once had a page with five separate animated elements: a button that scaled on hover, a card that lifted with a shadow, a progress bar that eased, an icon that rotated, and a toast that slid in. Individually, each was 200&amp;nbsp;ms and 60&amp;nbsp;fps. Collectively, they created a page where nothing felt instant.&lt;/p&gt;

&lt;p&gt;I stripped three of them entirely and kept the button scale and the toast slide. The page suddenly felt light. Users do not benchmark individual animations; they feel the aggregate drag.&lt;/p&gt;

&lt;h2&gt;When the best animation is no animation&lt;/h2&gt;

&lt;p&gt;I spent two hours tweaking the bezier curve of a page-transition slide, convinced the right easing would make it feel fast. During testing, users preferred the version where I removed the transition and let the next page render immediately.&lt;/p&gt;

&lt;p&gt;I had been optimizing the wrong variable. If you are tuning an animation to feel less slow, the real solution is usually to kill the animation and let the browser do what it is already optimized for: instant state change.&lt;/p&gt;

&lt;h2&gt;The trap of “feels premium”&lt;/h2&gt;

&lt;p&gt;In design critiques, long, fluid animations get labeled as “premium” because they look expensive. But premium in a user's hands is not the same as premium in a design review.&lt;/p&gt;

&lt;p&gt;In a checkout flow, a 400&amp;nbsp;ms cart-slide animation was described in testing as “the site taking its time,” whereas a 0&amp;nbsp;ms instant reveal of the cart panel was never mentioned at all. Unnoticed is the goal for utility animations. If the user is aware of your animation, it is probably already too long.&lt;/p&gt;

&lt;h2&gt;A simple rule for trimming animation&lt;/h2&gt;

&lt;p&gt;When a page feels slow even though the metrics say it is fast, look at your animations before you look at your servers.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;If an animation sits between intent and feedback, shorten it until it is barely there or remove it.&lt;/li&gt;
  &lt;li&gt;If an effect runs on every visit to a core screen, design for the third visit, not the first.&lt;/li&gt;
  &lt;li&gt;If a transition is compensating for jank, fix the jank instead of hiding it.&lt;/li&gt;
  &lt;li&gt;If you are tuning easing curves to make something feel faster, try a version with no animation at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the animations that survived user testing were the ones I removed. The ones I kept were shorter, simpler, and tied to feedback, not theater.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Asking. Start Delegating: How I Actually Use AI On My Site</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 06:55:02 +0000</pubDate>
      <link>https://dev.to/richardlemon/stop-asking-start-delegating-how-i-actually-use-ai-on-my-site-4bmd</link>
      <guid>https://dev.to/richardlemon/stop-asking-start-delegating-how-i-actually-use-ai-on-my-site-4bmd</guid>
      <description>&lt;h2&gt;AI is not a smarter Google&lt;/h2&gt;

&lt;p&gt;I am convinced most people are using AI in the worst possible way.&lt;/p&gt;

&lt;p&gt;They treat it like a slightly magical search bar. Type question. Get answer. Copy. Paste. Forget.&lt;/p&gt;

&lt;p&gt;I think that mindset is holding a lot of people back. Developers. Designers. Knowledge workers. Even my baseball kids who ask ChatGPT for homework help.&lt;/p&gt;

&lt;p&gt;AI is not a better Q&amp;amp;A machine. It is a delegation machine.&lt;/p&gt;

&lt;p&gt;You do not "ask" AI. You give it a job.&lt;/p&gt;

&lt;p&gt;This post is me making that shift concrete. I just shipped six AI gallery pages on my site, built entirely around that idea. Not as a gimmick. As infrastructure for how I work, learn, and build.&lt;/p&gt;

&lt;h2&gt;Why I stopped asking AI questions&lt;/h2&gt;

&lt;p&gt;The turning point was basically frustration.&lt;/p&gt;

&lt;p&gt;My workflow looked like this for months:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Open ChatGPT&lt;/li&gt;
  &lt;li&gt;Ask something like "How do I X in Astro / Svelte / Next"&lt;/li&gt;
  &lt;li&gt;Skim the answer&lt;/li&gt;
  &lt;li&gt;Try the snippet&lt;/li&gt;
  &lt;li&gt;Debug for 30 minutes anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers were fine. Sometimes even useful. But nothing stuck.&lt;/p&gt;

&lt;p&gt;I would ask the same class of questions over and over. Same concepts. Same patterns. Same gotchas. No real accumulation of knowledge. Just one-off transactions.&lt;/p&gt;

&lt;p&gt;Then I noticed something: the few times I actually got huge value from AI, I was not asking. I was delegating.&lt;/p&gt;

&lt;p&gt;"Rebuild this layout using CSS grid, but keep these class names."&lt;/p&gt;

&lt;p&gt;"Refactor this component, keep the same API, and annotate the performance tradeoffs in comments."&lt;/p&gt;

&lt;p&gt;"Act like my annoying senior engineer and poke holes in this data model."&lt;/p&gt;

&lt;p&gt;That felt different. Less like search. More like a teammate who does legwork while I keep steering.&lt;/p&gt;

&lt;h2&gt;Delegation &amp;gt; questions&lt;/h2&gt;

&lt;p&gt;So I made a decision: treat AI like a junior colleague with unlimited patience and questionable taste.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I do not ask "How do I do X".&lt;/li&gt;
  &lt;li&gt;I say "You are responsible for X. Here is context. Here are constraints. Here is the definition of done."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shift sounds subtle. It is not.&lt;/p&gt;

&lt;p&gt;When you ask a question, the model guesses what you want.&lt;/p&gt;

&lt;p&gt;When you delegate a job, you tell it what you want and where it fits inside a bigger system.&lt;/p&gt;

&lt;p&gt;I started writing prompts like I write tickets for myself:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Problem statement&lt;/li&gt;
  &lt;li&gt;Existing assets&lt;/li&gt;
  &lt;li&gt;Constraints&lt;/li&gt;
  &lt;li&gt;Output format&lt;/li&gt;
  &lt;li&gt;What I will do next with the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly the responses got more predictable. More reusable. Less hallucinated.&lt;/p&gt;

&lt;p&gt;And then I hit the second problem.&lt;/p&gt;

&lt;p&gt;All those good prompts and workflows were stuck inside random chats. Lost in the scroll.&lt;/p&gt;

&lt;h2&gt;The missing layer: an AI gallery, not a graveyard of chats&lt;/h2&gt;

&lt;p&gt;I wanted a place where my best AI workflows could live.&lt;/p&gt;

&lt;p&gt;Not as a marketing page. Not as "prompt engineering" porn. As actual, battle tested tools that I come back to.&lt;/p&gt;

&lt;p&gt;I tried:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Notion pages with prompts&lt;/li&gt;
  &lt;li&gt;Code snippets in a repo&lt;/li&gt;
  &lt;li&gt;Obsidian notes with copy-paste templates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that sucked in practice.&lt;/p&gt;

&lt;p&gt;When I am in build mode I do not want to context switch into a vault, find the right note, copy, switch window, paste, then adapt the prompt again.&lt;/p&gt;

&lt;p&gt;So I did what I usually do: I built something small, opinionated, and slightly selfish.&lt;/p&gt;

&lt;p&gt;Six AI gallery pages on my own site.&lt;/p&gt;

&lt;p&gt;Each page is a collection of specific delegation flows around one theme. Not generic prompts. Not screenshots. Actual workflows with inputs, constraints, and real outputs from my projects.&lt;/p&gt;

&lt;h2&gt;Announcing the six AI gallery pages&lt;/h2&gt;

&lt;p&gt;Here is what I shipped.&lt;/p&gt;

&lt;p&gt;These live on my site as proper pages, not hidden tools. They are meant for a broad audience. If you can type, you can use these ideas. If you build things for a living, you will probably adapt them.&lt;/p&gt;

&lt;h3&gt;1. AI for Web &amp;amp; UI building&lt;/h3&gt;

&lt;p&gt;This page is basically my front-end assistant in public.&lt;/p&gt;

&lt;p&gt;I use AI here as a layout engineer and code janitor:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Given this Figma export, generate semantic HTML and a Tailwind scaffold."&lt;/li&gt;
  &lt;li&gt;"Refactor this spaghetti CSS into logical layers and BEM-like naming without changing the final rendering."&lt;/li&gt;
  &lt;li&gt;"Suggest minimal variants of this component for mobile, but keep the design system tokens intact."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I include real before and afters from my codebase. Not perfect. Not theoretical.&lt;/p&gt;

&lt;p&gt;The point is simple. Stop asking "How do I center this" and start delegating "You own the responsive layout, here are constraints, I will review".&lt;/p&gt;

&lt;h3&gt;2. AI for Content &amp;amp; Writing&lt;/h3&gt;

&lt;p&gt;I think asking AI "write a blog post about X" is lazy and usually gives trash.&lt;/p&gt;

&lt;p&gt;So this page shows how I actually use it to support writing without losing my voice.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Take this messy brain dump and map out the structure without rewriting my sentences."&lt;/li&gt;
  &lt;li&gt;"Extract all the specific claims from this draft and list which ones need sources."&lt;/li&gt;
  &lt;li&gt;"Rewrite only the clunky sentences, keep the informal tone, do not touch any technical terms."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I treat AI as an editor, not a ghostwriter. A fast, opinionless editor that helps me see structure and friction.&lt;/p&gt;

&lt;p&gt;The gallery shows side by side drafts and the interventions I kept.&lt;/p&gt;

&lt;h3&gt;3. AI for Learning &amp;amp; Deep Work&lt;/h3&gt;

&lt;p&gt;This one is close to my biohacker side.&lt;/p&gt;

&lt;p&gt;I use AI as a study partner that never gets bored of my stupid questions but still forces me to think.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Act like a coach, not a lecturer. Ask me questions until I can explain React Server Components from first principles."&lt;/li&gt;
  &lt;li&gt;"Given this course outline, build a 4-week learning plan that fits into 90 minutes a day and schedules spaced repetition."&lt;/li&gt;
  &lt;li&gt;"Generate ten problems that test my understanding of event sourcing, sorted by difficulty, and hide the answers until I ask."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people ask AI to explain things. I think that is the wrong direction.&lt;/p&gt;

&lt;p&gt;I ask it to interrogate my understanding instead. That is delegation as well. I delegate the role of relentless tutor.&lt;/p&gt;

&lt;h3&gt;4. AI for Personal Systems &amp;amp; Life Admin&lt;/h3&gt;

&lt;p&gt;This page is for the boring parts of life that still eat hours.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Turn this raw weekly log into a short reflection with 3 wins, 3 misses, and 2 experiments for next week."&lt;/li&gt;
  &lt;li&gt;"Given my sleep, training, and work logs, suggest schedule tweaks that protect deep work blocks."&lt;/li&gt;
  &lt;li&gt;"Summarise this mess of emails and notes into one decision doc with clear options and tradeoffs."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here I basically treat AI like an operations assistant.&lt;/p&gt;

&lt;p&gt;It does not make decisions for me. It compresses reality so I can decide faster.&lt;/p&gt;

&lt;h3&gt;5. AI for Coaching &amp;amp; Communication&lt;/h3&gt;

&lt;p&gt;I coach baseball. I also mentor junior devs.&lt;/p&gt;

&lt;p&gt;Both groups need clear feedback, simple language, and the right level of challenge.&lt;/p&gt;

&lt;p&gt;On this page I show how I use AI to prepare better conversations:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Take these scattered notes on a player and turn them into one clear message with one focus area this week."&lt;/li&gt;
  &lt;li&gt;"Rewrite this feedback for a 13-year-old who loves hitting but hates fielding. Do not sugarcoat, stay honest."&lt;/li&gt;
  &lt;li&gt;"Given this junior's pull request and my inline comments, propose a short Loom script outline that teaches the underlying pattern."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not outsource the human part. The empathy and judgment stay mine.&lt;/p&gt;

&lt;p&gt;AI helps me say the thing more clearly and consistently.&lt;/p&gt;

&lt;h3&gt;6. AI for Experiments &amp;amp; Side Projects&lt;/h3&gt;

&lt;p&gt;The last gallery is basically my playground.&lt;/p&gt;

&lt;p&gt;Anything that feels like a small bet goes here:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;"Brainstorm 10 weird ways to visualise this dataset in the browser. Avoid typical chart types."&lt;/li&gt;
  &lt;li&gt;"Given this rough idea for a micro SaaS, create three fake landing page variants that test different value props."&lt;/li&gt;
  &lt;li&gt;"Help me design an experiment to see if people actually care about feature X. I want to run it this week with no backend changes."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here the model is more of a chaos generator that I keep on a leash.&lt;/p&gt;

&lt;p&gt;I delegate the job of producing options and experiment scaffolds. I keep the job of choosing and executing.&lt;/p&gt;

&lt;h2&gt;A broad audience, a specific mindset&lt;/h2&gt;

&lt;p&gt;I built these pages for a broad audience.&lt;/p&gt;

&lt;p&gt;You do not need to be a senior engineer. You do not need to understand transformers. You do not need a perfect "stack".&lt;/p&gt;

&lt;p&gt;You only need to accept one idea. AI is a delegation tool, not a vending machine for answers.&lt;/p&gt;

&lt;p&gt;If you are a developer, that means thinking in systems, not snippets.&lt;/p&gt;

&lt;p&gt;If you are a designer, that means delegating the boring layout variants and keeping the taste decisions.&lt;/p&gt;

&lt;p&gt;If you are a teacher or coach, that means using AI to structure your thinking, not to replace your expertise.&lt;/p&gt;

&lt;p&gt;The gallery pages are opinionated examples of that mindset in action. They are not "best practice". They are my practice.&lt;/p&gt;

&lt;h2&gt;How I actually built these pages&lt;/h2&gt;

&lt;p&gt;Because I know some of you care more about the plumbing.&lt;/p&gt;

&lt;p&gt;The site runs on a modern static stack with a light API layer. Nothing exotic. I do not think overcomplicating this kind of content is worth it.&lt;/p&gt;

&lt;p&gt;The important part is the structure I chose for the galleries:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Each gallery is a standalone page with a clear theme.&lt;/li&gt;
  &lt;li&gt;Every item has: context, the exact prompt or instruction, constraints, sample input, and real output.&lt;/li&gt;
  &lt;li&gt;I track which flows I actually reuse with a simple client-side counter. If something never gets reused, it gets cut or reworked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the pages evolve with my work. They are not frozen collections.&lt;/p&gt;

&lt;p&gt;When I notice I am repeating a manual pattern, I turn it into a delegatable flow and add it to the relevant gallery.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;I kept rewriting similar "refactor this component" prompts in random chats. That friction was my signal. I stopped, abstracted the pattern, gave it a name, wrote it up in the Web &amp;amp; UI gallery, and now I have a reusable workflow.&lt;/p&gt;

&lt;p&gt;The process is almost boring:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Spot repetition.&lt;/li&gt;
  &lt;li&gt;Extract the job.&lt;/li&gt;
  &lt;li&gt;Document the delegation pattern.&lt;/li&gt;
  &lt;li&gt;Publish.&lt;/li&gt;
  &lt;li&gt;Iterate when it breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But boring is good. Boring means it is part of my operating system now.&lt;/p&gt;

&lt;h2&gt;If you take one thing from this&lt;/h2&gt;

&lt;p&gt;Stop asking AI for fish. Start hiring it as a clumsy intern who never sleeps.&lt;/p&gt;

&lt;p&gt;Give it jobs, not trivia questions.&lt;/p&gt;

&lt;p&gt;Describe what success looks like. Define constraints. Explain what you will do with the output.&lt;/p&gt;

&lt;p&gt;Then build your own small gallery of workflows that actually earn their place in your day.&lt;/p&gt;

&lt;p&gt;Mine just happens to be public now: six AI gallery pages that show how I use this stuff across code, content, learning, life, coaching, and experiments.&lt;/p&gt;

&lt;p&gt;If that helps you stop treating AI like a search bar and start treating it like a teammate, then the shift is already happening.&lt;/p&gt;

&lt;p&gt;The rest is just reps.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why I Track HRV Every Morning (And How It Actually Changes My Day)</title>
      <dc:creator>Richard Lemon</dc:creator>
      <pubDate>Mon, 25 May 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/richardlemon/why-i-track-hrv-every-morning-and-how-it-actually-changes-my-day-3egk</link>
      <guid>https://dev.to/richardlemon/why-i-track-hrv-every-morning-and-how-it-actually-changes-my-day-3egk</guid>
      <description>&lt;h2&gt;HRV is only useful if it changes your day&lt;/h2&gt;

&lt;p&gt;I have tracked HRV every morning for a few years now. Not because I like graphs. Because it is the only number that consistently talks back when I am lying to myself.&lt;/p&gt;

&lt;p&gt;I build web stuff, coach baseball, and play the biohacker hobby game. That combination makes it very easy to run on willpower and caffeine until something breaks. HRV is my early warning system.&lt;/p&gt;

&lt;p&gt;But here is the key point. I do not care about the exact number. I care about what I do differently because of it. No behavior change, no point.&lt;/p&gt;

&lt;h2&gt;The setup: cheap, boring, and consistent&lt;/h2&gt;

&lt;p&gt;I do not treat HRV like a gadget fashion show. I want boring and repeatable.&lt;/p&gt;

&lt;p&gt;My morning routine:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Wake up, bathroom, water.&lt;/li&gt;
  &lt;li&gt;Sit on the same chair.&lt;/li&gt;
  &lt;li&gt;Same 60–90 second HRV reading with a chest strap and phone app.&lt;/li&gt;
  &lt;li&gt;Eyes open, normal breathing, no breathwork tricks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I care about three things only:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;My rolling baseline.&lt;/strong&gt; Roughly the 7–30 day trend.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Today vs that baseline.&lt;/strong&gt; How far up or down.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Direction.&lt;/strong&gt; Is it drifting up, flat, or sagging over a few days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app gives me a recovery score. That score is just math on top of the HRV number, but it is easier to work with a simple traffic light in my head.&lt;/p&gt;

&lt;h2&gt;The traffic light: green, yellow, red&lt;/h2&gt;

&lt;p&gt;I run my life on a very dumb, very useful mental model.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt;: Nervous system looks ready. HRV at or above baseline. Resting heart rate normal.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Yellow&lt;/strong&gt;: Slightly suppressed HRV or resting heart rate a bit elevated, or both. Body is stressed but not falling apart.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Red&lt;/strong&gt;: Noticeably down HRV, sometimes paired with a big bump in resting heart rate, plus I feel like cardboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it. No decimal worship. I do not care if my RMSSD is 68 or 72. I care if the light is green, yellow, or red and where that sits relative to my baseline.&lt;/p&gt;

&lt;p&gt;The important part is what I decided in advance for each color. If you make rules on the spot you will negotiate yourself into dumb decisions.&lt;/p&gt;

&lt;h2&gt;Green days: permission to go heavy&lt;/h2&gt;

&lt;p&gt;On a green day, I treat HRV clearance as permission. Not as pressure.&lt;/p&gt;

&lt;p&gt;I have three decision buckets that change when I see green: training, work, and recovery inputs.&lt;/p&gt;

&lt;h3&gt;Green training rules&lt;/h3&gt;

&lt;p&gt;Green usually means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Hard session goes ahead&lt;/strong&gt;: heavy lifts, sprint work, or a tough baseball practice.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Volume is allowed to climb&lt;/strong&gt;: I may add a set or two, or stretch the conditioning a bit.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Experiment window is open&lt;/strong&gt;: if I want to test a new drill, a different sprint pattern, or a new strength block, I try it on a green day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example from last month: I had a planned heavy lower body day with some sprint repeats. HRV was clearly green, slightly above baseline after two nights of solid sleep. Instead of just running the default, I pushed the sprint count by 20 percent and added one heavier top set on squats.&lt;/p&gt;

&lt;p&gt;The session felt good, no grindy reps, and HRV stayed stable the next morning. That told me my body could handle that new load. So I locked that progression in as the new normal for the block.&lt;/p&gt;

&lt;h3&gt;Green work rules&lt;/h3&gt;

&lt;p&gt;Green day equals high-quality cognitive work window.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;I schedule my hardest coding or design work as early as possible.&lt;/li&gt;
  &lt;li&gt;I avoid early calls. I move meetings to the afternoon if I can.&lt;/li&gt;
  &lt;li&gt;If there is a tricky refactor or a gnarly WebGL experiment, this is the day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HRV does not directly tell me about focus, but there is a pattern. When recovery is solid, my willingness to sit with hard problems is much higher.&lt;/p&gt;

&lt;h3&gt;Green recovery rules&lt;/h3&gt;

&lt;p&gt;When I see green I do &lt;em&gt;not&lt;/em&gt; celebrate with junk behavior. I do the opposite.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Keep caffeine normal, do not double it just because I feel good.&lt;/li&gt;
  &lt;li&gt;Stick to my usual bedtime rather than “rewarding” myself with a late night.&lt;/li&gt;
  &lt;li&gt;Give myself a small recovery boost, like a longer walk in the sun.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Green is where you build new capacity. You can only do that if you do not instantly spend all the extra energy on random nonsense.&lt;/p&gt;

&lt;h2&gt;Yellow days: ego control territory&lt;/h2&gt;

&lt;p&gt;Most days are not green or red. They are slightly off. That is where the traffic light actually earns its keep.&lt;/p&gt;

&lt;p&gt;Yellow is where my ego wants to ignore the data. This is also where I have made the most dumb training mistakes in the past.&lt;/p&gt;

&lt;h3&gt;Yellow training rules&lt;/h3&gt;

&lt;p&gt;On a yellow day I do not cancel training. I change the framing.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Intensity stays, volume drops&lt;/strong&gt;: I still lift heavy or move fast, but I cut sets or total reps.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Or volume stays, intensity drops&lt;/strong&gt;: I keep the number of sets, but I stay well away from failure and keep heart rate lower.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;No new experiments&lt;/strong&gt;: I do not test 1RMs, new sprint distances, or brutal circuits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete example. My HRV is slightly below baseline after a late-night game or a long coding session that turned into a 1 AM situation. I had planned 5 x 5 heavy bench and a bunch of accessory work.&lt;/p&gt;

&lt;p&gt;On a yellow day, I might keep the 5 x 5 but drop the load by 5–10 percent, skip one accessory exercise, and cut conditioning in half. I still show up. The workout still happens. I do not dig a deeper hole.&lt;/p&gt;

&lt;h3&gt;Yellow work rules&lt;/h3&gt;

&lt;p&gt;Yellow days are for progress without heroics.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ship small pieces. Fix bugs, clean up code, handle admin.&lt;/li&gt;
  &lt;li&gt;Push big decisions to a green day if possible.&lt;/li&gt;
  &lt;li&gt;Be very suspicious of “one more hour” thinking in the evening.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I force a late-night coding binge on a yellow day, HRV usually hits red the next morning. I have seen that pattern enough times to consider it basically deterministic for me.&lt;/p&gt;

&lt;h3&gt;Yellow recovery rules&lt;/h3&gt;

&lt;p&gt;On yellow I start turning dials.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Earlier cutoff for screens and work.&lt;/li&gt;
  &lt;li&gt;A bit more carbs in the evening to support sleep.&lt;/li&gt;
  &lt;li&gt;Move non-urgent life tasks to another day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is simple. Yellow means “you are drifting.” I want to see if I can pull myself back toward green tomorrow without skipping life.&lt;/p&gt;

&lt;h2&gt;Red days: forced humility&lt;/h2&gt;

&lt;p&gt;Red is where HRV saves me from myself. This is the only color where I am willing to scrap the plan completely.&lt;/p&gt;

&lt;p&gt;Red usually comes from one of a few predictable causes for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Travel and broken sleep.&lt;/li&gt;
  &lt;li&gt;Back-to-back high intensity days because I ignored a yellow signal.&lt;/li&gt;
  &lt;li&gt;Getting sick, or fighting something off.&lt;/li&gt;
  &lt;li&gt;Two days of very late games or events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Red training rules&lt;/h3&gt;

&lt;p&gt;On a red day I have a simple default.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No high-intensity training.&lt;/li&gt;
  &lt;li&gt;LISS cardio only: easy walk, light bike, mobility.&lt;/li&gt;
  &lt;li&gt;If I feel awful, full rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I already stacked a few red or deep yellow days in a row, I usually remove any training pressure completely. I let the system reset. Every time I ignored that, HRV punished me with a longer slump and worse sleep.&lt;/p&gt;

&lt;p&gt;One specific pattern: if HRV tanks and resting heart rate is up, and &lt;em&gt;perceived effort&lt;/em&gt; spiked on what should have been an easy session the day before, I do not negotiate. I shut it down for that day. That combo almost always means something is off globally, not just “I am a bit tired.”&lt;/p&gt;

&lt;h3&gt;Red work rules&lt;/h3&gt;

&lt;p&gt;On red days I lower the ambition floor.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Cancel non-essential meetings if I can.&lt;/li&gt;
  &lt;li&gt;Move deep work to another day. Only do it if I somehow feel unusually sharp despite the score.&lt;/li&gt;
  &lt;li&gt;Knock out simple, mechanical tasks and stop pretending I will architect a whole new feature set in that state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had to get over the guilt of this. But every time I pretend I am fine on a red day, I pay for it later with sloppy code and more time spent fixing mistakes.&lt;/p&gt;

&lt;h3&gt;Red recovery rules&lt;/h3&gt;

&lt;p&gt;Red is where I go heavier on interventions.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Hard stop on screens at least an hour before bed.&lt;/li&gt;
  &lt;li&gt;Light dinner, no late heavy meals.&lt;/li&gt;
  &lt;li&gt;Walks instead of workouts, sunlight early in the day.&lt;/li&gt;
  &lt;li&gt;If possible, a short nap, but capped so I do not wreck the night.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also use red days as an audit. Why am I here? Was it a one-off travel day or have I been stacking small stupid decisions for a week?&lt;/p&gt;

&lt;h2&gt;Where HRV actually changed my behavior&lt;/h2&gt;

&lt;p&gt;HRV itself did not change much in my life on day one. The traffic light and pre-committed rules did.&lt;/p&gt;

&lt;p&gt;Here are a few specific shifts that stuck.&lt;/p&gt;

&lt;h3&gt;I stopped pretending sleep does not matter&lt;/h3&gt;

&lt;p&gt;Before HRV, I treated sleep like a flexible variable. If I wanted to code late or watch a game, I did it. I would “catch up” later.&lt;/p&gt;

&lt;p&gt;HRV showed me something simple. Two short nights do more damage than one really short night. The second late night pushes the nervous system into a state where the next day is always worse than it feels in the moment.&lt;/p&gt;

&lt;p&gt;Green-to-red swings after stacked short nights convinced me to protect at least five nights a week. I still have late nights, just fewer in a row.&lt;/p&gt;

&lt;h3&gt;I stopped proving toughness to no one&lt;/h3&gt;

&lt;p&gt;Most of the athletes I coach, and most developers I know, have the same problem. We mistake stubbornness for discipline.&lt;/p&gt;

&lt;p&gt;HRV gave me a clean excuse to stop that. If the score is red and the trend has been sliding for three days, I do not get bonus points for “pushing through.” I get slower progress and more nagging injuries.&lt;/p&gt;

&lt;p&gt;It took a while, but now if the traffic light is red and I still feel the itch to max out something, I treat that as a bug in my behavior, not a sign of commitment.&lt;/p&gt;

&lt;h3&gt;I started planning weeks, not days&lt;/h3&gt;

&lt;p&gt;Tracking HRV showed me that what I do on Monday is still echoing around on Thursday.&lt;/p&gt;

&lt;p&gt;So now I design weeks around likely traffic lights:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Monday and Thursday are likely green: main heavy training and hardest coding.&lt;/li&gt;
  &lt;li&gt;Tuesday and Friday are more flexible: if I see yellow, those become lighter work or technique days.&lt;/li&gt;
  &lt;li&gt;Weekend is the buffer: travel, games, or family stuff that may push HRV around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not perfect. Life rarely respects my plans. But thinking in terms of green / yellow / red probability has made my training blocks far more stable.&lt;/p&gt;

&lt;h2&gt;HRV is not a boss, it is a negotiating partner&lt;/h2&gt;

&lt;p&gt;I do not outsource decisions to HRV. I use it as one more voice in the room.&lt;/p&gt;

&lt;p&gt;Sometimes I override it. For example, if it is a once-a-year event or competition, I am going to show up and do the thing, even on a red day. I just go in with my eyes open. I expect a recovery tax afterwards and I plan around it.&lt;/p&gt;

&lt;p&gt;Most days though, the traffic light is enough to keep my ego in check and my training productive.&lt;/p&gt;

&lt;h2&gt;If you want to steal this system&lt;/h2&gt;

&lt;p&gt;If you want to copy anything here, copy the decision loop, not the tech.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pick &lt;strong&gt;one&lt;/strong&gt; HRV method. Keep it boring and consistent.&lt;/li&gt;
  &lt;li&gt;Track for a few weeks without changing anything just to see your baseline.&lt;/li&gt;
  &lt;li&gt;Define your own clear green, yellow, red thresholds in that app or in a notebook.&lt;/li&gt;
  &lt;li&gt;Write down rules for each color &lt;em&gt;before&lt;/em&gt; you see the next score.&lt;/li&gt;
  &lt;li&gt;Commit to following those rules for a month and see what happens to performance and mood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think most people obsess over gadgets and miss the point. HRV is not about who has the highest number. It is about having a simple, brutally honest mirror that you agree to listen to every morning.&lt;/p&gt;

&lt;p&gt;That is why I still track it. Not because I care about today’s exact milliseconds. Because it changes what I do at 9 AM.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>mentalhealth</category>
      <category>productivity</category>
      <category>watercooler</category>
    </item>
  </channel>
</rss>
