[{"data":1,"prerenderedAt":270},["ShallowReactive",2],{"post-responsive-images-without-layout-shift":3,"blog-posts":14},{"_path":4,"title":5,"description":6,"date":7,"tags":8,"readingTime":12,"body":13},"\u002Fblog\u002Fresponsive-images-without-layout-shift\u002F","Responsive Images Without Layout Shift","Width, height, srcset, and lazy loading patterns that keep pages stable while images load.","2026-01-22",[9,10,11],"html","performance","frontend",5,"# Responsive Images Without Layout Shift\n\nA page that jumps when images arrive feels broken even if the pixels eventually look fine. Most layout shift from images is preventable with a few habits.\n\n## Reserve space up front\n\nAlways set `width` and `height` on `\u003Cimg>` (or use CSS `aspect-ratio`) so the browser can allocate layout before the file downloads. The rendered size can still be fluid; the ratio is what stops the jump.\n\n```html\n\u003Cimg\n  src=\"\u002Fimages\u002Fhero.jpg\"\n  srcset=\"\u002Fimages\u002Fhero-640.jpg 640w, \u002Fimages\u002Fhero-1280.jpg 1280w\"\n  sizes=\"(max-width: 768px) 100vw, 720px\"\n  width=\"1280\"\n  height=\"720\"\n  alt=\"Hero shot of the project\"\n  loading=\"lazy\"\n\u002F>\n```\n\n```css\n.prose img {\n  max-width: 100%;\n  height: auto;\n  aspect-ratio: attr(width) \u002F attr(height); \u002F* where supported, or set explicitly *\u002F\n}\n```\n\nIn practice I set intrinsic `width`\u002F`height` in HTML to the source dimensions, then let CSS scale with `max-width: 100%; height: auto`. Modern browsers use those attributes to compute aspect ratio and reserve space—exactly what CLS needs.\n\nFor background images in CSS, reserve space on the container instead:\n\n```css\n.hero {\n  aspect-ratio: 16 \u002F 9;\n  background-image: url('\u002Fimages\u002Fhero.jpg');\n  background-size: cover;\n}\n```\n\nCSS-only backgrounds cannot use `srcset` the same way, so for LCP-critical heroes I prefer a real `\u003Cimg>` (or `\u003Cpicture>`) and position it as the visual plane. You get dimensions, responsive sources, and alt text in one place.\n\nSVGs and icons should reserve space too—fixed width\u002Fheight or a wrapping element with a defined box—so fonts and icons do not shove text after load.\n\n## Match srcset to real breakpoints\n\nGenerate a small set of widths you actually serve—usually two or three—not a dozen sizes nobody requests. Pair `srcset` with a `sizes` attribute that reflects your CSS layout, not a generic `100vw` on every image.\n\nA `sizes` lie is a common CLS-adjacent performance bug: the browser picks a huge file because it thinks the image is viewport-wide, when CSS actually shows it at 320px in a card grid.\n\nWorkflow that stays honest:\n\n1. Look at the layout in DevTools at mobile, tablet, and desktop widths\n2. Note the displayed CSS width of the image in each band\n3. Write `sizes` to match those bands\n4. Export 2–3 source widths that cover those displays at 1x\u002F2x as needed\n\n```html\n\u003Cimg\n  src=\"\u002Fimages\u002Fcard-640.jpg\"\n  srcset=\"\n    \u002Fimages\u002Fcard-320.jpg 320w,\n    \u002Fimages\u002Fcard-640.jpg 640w,\n    \u002Fimages\u002Fcard-960.jpg 960w\n  \"\n  sizes=\"(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 320px\"\n  width=\"960\"\n  height=\"640\"\n  alt=\"Screenshot of the dashboard\"\n  loading=\"lazy\"\n\u002F>\n```\n\nUse `\u003Cpicture>` when art direction changes—not only resolution. Cropping a wide hero differently on mobile is an art-direction problem; serving the same crop at multiple widths is a resolution problem. Do not conflate them.\n\nModern formats (AVIF\u002FWebP) with a JPEG\u002FPNG fallback belong in `\u003Cpicture>` or negotiated `Accept` handling. Format gains help bytes; they do not replace width\u002Fheight for layout stability.\n\n## Lazy load below the fold\n\n`loading=\"lazy\"` on off-screen images saves bandwidth without hurting LCP on the hero. Keep the largest above-the-fold image eager so the first paint stays fast.\n\nRules of thumb:\n\n- **Hero \u002F LCP image** — `loading=\"eager\"` (or omit lazy), discoverable early in HTML, properly sized\n- **Below the fold** — `loading=\"lazy\"`\n- **Carousels** — lazy slides that are not visible; be careful with preload tricks that fight lazy\n- **`fetchpriority=\"high\"`** — consider on the single LCP image when competing with other requests\n\nLazy loading does not fix missing dimensions. A lazy image without width\u002Fheight still shifts layout when it finally paints. Fix space first, then defer the download.\n\nFor CSS `background-image` galleries, lazy behavior needs Intersection Observer or similar—native `loading` does not apply. Prefer `\u003Cimg>` in content whenever you can.\n\nOpen Graph and social preview images are a separate concern—they do not affect CLS on your page—but the in-article figures that do should follow the same width\u002Fheight discipline whether they come from markdown or a CMS. Consistency across templates matters more than one perfect hero.\n\n## Watch for the other CLS culprits around images\n\nImages are the usual suspect, but related shifts sneak in:\n\n- Ads or embeds injected above content without reserved height\n- Web fonts swapping metrics after image layout settled (less common, still real)\n- Dynamic banners (“cookie consent”, “new post”) inserted at the top after load\n- Aspect ratios that change between skeleton and final media\n\nReserve space for those embeds the same way you do for photos. If you must inject UI, prefer inserting below the viewport or in a slot that already has height.\n\nIn Chrome Performance \u002F Experience tools, look at CLS contributions and confirm the shifting node is the image (or its wrapper). Fixing a unrelated font issue will not help if the hero still lacks dimensions.\n\nOn content sites I bake dimensions into the template or markdown pipeline so authors cannot ship a bare `\u003Cimg src>` without width and height. A lint rule or a remark\u002Frehype check is enough; the point is to make the stable default automatic rather than a memory test on every PR. Once the habit is structural, layout shift from images stops being a recurring fire drill.\n\n## Wrap-up\n\nStable image layout is mostly explicit dimensions plus honest `srcset`\u002F`sizes`, with lazy loading reserved for below-the-fold media. Fix those once per template—post body, card grid, project hero—and CLS from images largely disappears. The page should feel finished while pixels are still arriving, not rearrange when they do.",[15,23,33,42,49,57,64,72,79,86,92,100,107,114,121,128,134,142,151,159,165,172,174,182,190,197,203,209,216,222,228,234,241,248,255,263],{"_path":16,"title":17,"description":18,"date":19,"tags":20,"readingTime":12},"\u002Fblog\u002Fclient-side-tools-that-earn-trust\u002F","Client-Side Tools That Earn Trust","How to design browser tools that feel private by default—local processing, clear data boundaries, and UX that never asks people to guess where their files go.","2026-07-20",[21,11,22],"privacy","tools",{"_path":24,"title":25,"description":26,"date":27,"tags":28,"readingTime":32},"\u002Fblog\u002Fusb-c-cables-that-actually-matter\u002F","USB-C Cables That Actually Matter","How to buy USB-C cables by wattage and data speed so you stop guessing which cord charges slowly or fails a file transfer.","2026-07-14",[29,30,31],"gadgets","hardware","productivity",4,{"_path":34,"title":35,"description":36,"date":37,"tags":38,"readingTime":12},"\u002Fblog\u002Fcolor-contrast-people-can-read\u002F","Color Contrast People Can Actually Read","Quick checks for text and UI colors that stay legible in light mode, dark mode, and on mediocre displays.","2026-07-07",[39,40,41],"accessibility","css","design",{"_path":43,"title":44,"description":45,"date":46,"tags":47,"readingTime":12},"\u002Fblog\u002Fportable-ssds-worth-carrying\u002F","Portable SSDs Worth Carrying","When a pocket SSD beats cloud sync for travel, backups, and large media—and how to pick one that stays fast and durable.","2026-06-26",[29,48,31],"storage",{"_path":50,"title":51,"description":52,"date":53,"tags":54,"readingTime":12},"\u002Fblog\u002Fwireless-earbuds-that-last-the-day\u002F","Wireless Earbuds That Last the Day","Simple charging and fit habits that stretch earbud battery life without babysitting the case every few hours.","2026-06-18",[29,55,56],"audio","habits",{"_path":58,"title":59,"description":60,"date":61,"tags":62,"readingTime":32},"\u002Fblog\u002Fwhen-a-second-monitor-helps\u002F","When a Second Monitor Actually Helps","A practical take on dual screens for coding, writing, and research—plus when a single good display is still the better desk.","2026-06-10",[29,31,63],"workspace",{"_path":65,"title":66,"description":67,"date":68,"tags":69,"readingTime":32},"\u002Fblog\u002Fshort-session-game-picks\u002F","Short-Session Games That Respect Your Evening","Genres and design patterns that fit a 30-minute window without the “one more hour” trap.","2026-06-04",[70,56,71],"gaming","indie",{"_path":73,"title":74,"description":75,"date":76,"tags":77,"readingTime":12},"\u002Fblog\u002Fphone-charging-habits-that-age-better\u002F","Phone Charging Habits That Age Better","Practical charging routines that keep a phone useful longer—without obsessing over the perfect battery percentage.","2026-05-23",[29,78,56],"mobile",{"_path":80,"title":81,"description":82,"date":83,"tags":84,"readingTime":12},"\u002Fblog\u002Ffinishing-side-projects-without-burning-out\u002F","Finishing Side Projects Without Burning Out","A practical playbook for shrinking scope, shipping thin vertical slices, and keeping personal projects fun long enough to actually launch.","2026-05-14",[31,85,56],"side-projects",{"_path":87,"title":88,"description":89,"date":90,"tags":91,"readingTime":12},"\u002Fblog\u002Faccessible-forms-people-finish\u002F","Accessible Forms That People Actually Finish","Practical accessibility checks for labels, errors, and focus so your forms work for more users.","2026-05-07",[39,9,11],{"_path":93,"title":94,"description":95,"date":96,"tags":97,"readingTime":12},"\u002Fblog\u002Fvue-composition-api-patterns\u002F","Practical Vue Composition API Patterns","Reusable patterns for composables, shared state, and cleaner component logic in Vue 3.","2026-04-27",[98,99,11],"vue","javascript",{"_path":101,"title":102,"description":103,"date":104,"tags":105,"readingTime":12},"\u002Fblog\u002Fdesigning-empty-states-that-teach\u002F","Designing Empty States That Teach the Product","Empty screens are not placeholders—they are onboarding. How to write, layout, and sequence first-run UI so people know what to do next.","2026-04-19",[106,11,41],"ux",{"_path":108,"title":109,"description":110,"date":111,"tags":112,"readingTime":12},"\u002Fblog\u002Fwhy-indie-games-keep-winning\u002F","Why Indie Games Keep Winning Attention","How small teams punch above their weight with focused design, personality, and smart scope.","2026-04-13",[70,71,113],"industry",{"_path":115,"title":116,"description":117,"date":118,"tags":119,"readingTime":12},"\u002Fblog\u002Fkeeping-component-props-simple\u002F","Keeping Component Props Simple","When to split a component, when to use slots, and how to avoid prop objects that grow forever.","2026-04-03",[98,11,120],"best-practices",{"_path":122,"title":123,"description":124,"date":125,"tags":126,"readingTime":12},"\u002Fblog\u002Fcalm-frontend-performance\u002F","A Calm Approach to Frontend Performance","Performance work that sticks: measure what users feel, fix the critical path first, and avoid optimization theater on personal and portfolio sites.","2026-03-26",[10,11,127],"web",{"_path":129,"title":130,"description":131,"date":132,"tags":133,"readingTime":12},"\u002Fblog\u002Fcss-layout-without-the-hacks\u002F","CSS Layout Without the Hacks","Modern Flexbox and Grid techniques that replace brittle floats, magic numbers, and overflow tricks.","2026-03-17",[40,11,41],{"_path":135,"title":136,"description":137,"date":138,"tags":139,"readingTime":12},"\u002Fblog\u002Fcloud-gaming-what-gets-in-the-way\u002F","Cloud Gaming: What Still Gets in the Way","Latency, input feel, and library lock-in—why streaming games is impressive and still uneven.","2026-03-10",[70,140,141],"cloud-gaming","tech",{"_path":143,"title":144,"description":145,"date":146,"tags":147,"readingTime":12},"\u002Fblog\u002Fshipping-static-sites-with-nuxt\u002F","Shipping Static Sites with Nuxt","How I build and deploy a Nuxt site to Cloudflare Pages with predictable routes and content.","2026-03-02",[148,149,150],"nuxt","ssg","devops",{"_path":152,"title":153,"description":154,"date":155,"tags":156,"readingTime":12},"\u002Fblog\u002Fdebugging-faster-in-the-browser\u002F","Debugging Faster in the Browser","A short toolkit of DevTools habits—breakpoints, network filters, and DOM inspection—that cut debug time.","2026-02-24",[99,157,158],"devtools","debugging",{"_path":160,"title":161,"description":162,"date":163,"tags":164,"readingTime":12},"\u002Fblog\u002Fsmall-javascript-habits-that-scale\u002F","Small JavaScript Habits That Scale","Everyday habits—naming, early returns, and clearer async flow—that keep front-end codebases maintainable.","2026-02-18",[99,120,11],{"_path":166,"title":167,"description":168,"date":169,"tags":170,"readingTime":32},"\u002Fblog\u002Fhealthier-gaming-routine\u002F","Building a Healthier Gaming Routine","Simple habits for sessions that stay fun—breaks, goals, and knowing when to stop.","2026-02-02",[70,56,171],"wellness",{"_path":4,"title":5,"description":6,"date":7,"tags":173,"readingTime":12},[9,10,11],{"_path":175,"title":176,"description":177,"date":178,"tags":179,"readingTime":12},"\u002Fblog\u002Fgit-habits-for-small-teams\u002F","Git Habits for Solo and Small Teams","Branch naming, commit messages, and PR hygiene that keep history useful without slowing you down.","2026-01-09",[180,181,31],"git","workflow",{"_path":183,"title":184,"description":185,"date":186,"tags":187,"readingTime":12},"\u002Fblog\u002Fwhat-makes-a-game-stream-worth-watching\u002F","What Makes a Game Stream Worth Watching","Commentary, pacing, and community—why some streams click even when the gameplay is messy.","2025-12-14",[70,188,189],"streaming","content",{"_path":191,"title":192,"description":193,"date":194,"tags":195,"readingTime":12},"\u002Fblog\u002Fwhen-to-reach-for-typescript-vue\u002F","When to Reach for TypeScript on a Vue App","A pragmatic take on where TypeScript pays off in Vue—and where plain JS is still fine.","2025-11-30",[196,98,11],"typescript",{"_path":198,"title":199,"description":200,"date":201,"tags":202,"readingTime":12},"\u002Fblog\u002Fenvironment-variables-for-static-sites\u002F","Environment Variables for Static Sites","How to use build-time env vars in Nuxt and other static generators without leaking secrets or breaking CI.","2025-10-05",[148,150,11],{"_path":204,"title":205,"description":206,"date":207,"tags":208,"readingTime":12},"\u002Fblog\u002Fpicking-a-side-project-stack\u002F","Picking a Side Project Stack Without Overthinking It","How to choose tools for a personal project when the goal is shipping, not building the perfect architecture.","2025-09-18",[31,11,120],{"_path":210,"title":211,"description":212,"date":213,"tags":214,"readingTime":32},"\u002Fblog\u002Fco-op-games-for-busy-friends\u002F","Co-op Games That Work When Everyone Is Busy","Async-friendly and drop-in co-op picks for friend groups that rarely share the same free evening.","2025-08-22",[70,215,56],"co-op",{"_path":217,"title":218,"description":219,"date":220,"tags":221,"readingTime":12},"\u002Fblog\u002Fsemantic-html-still-matters\u002F","Semantic HTML Still Matters","Landmarks, headings, and native elements that make pages easier to use, style, and maintain.","2025-07-06",[9,39,11],{"_path":223,"title":224,"description":225,"date":226,"tags":227,"readingTime":12},"\u002Fblog\u002Fgit-without-the-jargon\u002F","Learning Git Without the Jargon","A plain-language mental model for commits, branches, and merges when tutorials assume you already know Git.","2025-05-14",[180,181,31],{"_path":229,"title":230,"description":231,"date":232,"tags":233,"readingTime":12},"\u002Fblog\u002Fwhy-loading-states-matter\u002F","Why Loading States Matter More Than Animations","Skeletons, disabled buttons, and honest feedback beat flashy spinners when data takes time to arrive.","2025-03-28",[106,11,41],{"_path":235,"title":236,"description":237,"date":238,"tags":239,"readingTime":12},"\u002Fblog\u002Fnaming-things-so-future-you-survives\u002F","Naming Things So Future You Survives","Practical rules for naming variables, components, and files so a codebase stays readable months after the clever jokes fade.","2025-03-10",[99,120,240],"maintainability",{"_path":242,"title":243,"description":244,"date":245,"tags":246,"readingTime":12},"\u002Fblog\u002Fsave-systems-that-respect-time\u002F","Save Systems That Respect the Player’s Time","Checkpoints, autosaves, and manual slots are design choices. How thoughtful saving reduces rage-quits without deleting tension from the game.","2025-02-28",[70,247,106],"game-design",{"_path":249,"title":250,"description":251,"date":252,"tags":253,"readingTime":12},"\u002Fblog\u002Fmicrocopy-that-makes-interfaces-human\u002F","Microcopy That Makes Interfaces Feel Human","Button labels, error lines, and helper text are product design. How to write UI words that reduce hesitation without sounding like a chatbot.","2025-01-22",[106,254,41],"writing",{"_path":256,"title":257,"description":258,"date":259,"tags":260,"readingTime":12},"\u002Fblog\u002Freadmes-that-get-projects-running\u002F","READMEs That Get Projects Running","How to write a README that helps someone—including future you—install, run, and understand a project without opening a support chat.","2024-12-18",[261,262,56],"documentation","open-source",{"_path":264,"title":265,"description":266,"date":267,"tags":268,"readingTime":12},"\u002Fblog\u002Fprogressive-enhancement-still-pays-off\u002F","Progressive Enhancement Still Pays Off","Build the core experience in HTML first, then layer CSS and JavaScript so your site stays useful when networks, scripts, or devices misbehave.","2024-11-05",[9,11,269],"architecture",1784620504977]