[{"data":1,"prerenderedAt":371},["ShallowReactive",2],{"$fi_SA3l8D9e4tuLzFLMXUroEj8Vi4FUp0B2g2TbBke08":3,"post-building-pwas-for-real-people-not-perfect-networks":74},{"site":4,"features":8,"stripe":10,"bridalfile":12,"navigation":13,"branding":51,"footer":54},{"id":5,"name":6,"domain":7},"7578a1b2-a8ec-462f-8980-d46ebefc76a2","Pwa Developer","pwadeveloper.uk",{"products":9,"bookings":9,"event_capacity":9},false,{"enabled":9,"test_mode":11},true,{"enabled":9},{"footer":14,"header":34},{"id":15,"name":16,"items":17},"4c83a83d-7d89-442d-8e4c-717e61d71c19","Footer Navigation",[18,22,26,30],{"id":19,"url":20,"label":21},"b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e","/about","About",{"id":23,"url":24,"label":25},"c9d0e1f2-a3b4-4c5d-6e7f-8a9b0c1d2e3f","/blog","Blog",{"id":27,"url":28,"label":29},"d0e1f2a3-b4c5-4d6e-7f8a-9b0c1d2e3f4a","/contact","Contact",{"id":31,"url":32,"label":33},"e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b","/websites","Websites",{"id":35,"name":36,"items":37},"090e7007-6dd0-4416-8ce2-2680c446d2a9","Main Navigation",[38,40,42,44,49],{"id":39,"url":20,"label":21},"c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",{"id":41,"url":32,"label":33},"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",{"id":43,"url":24,"label":25},"d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a",{"id":45,"url":46,"label":47,"target":48},"e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b","https://www.youtube.com/@anothermagentodev9161","YouTube","_blank",{"id":50,"url":28,"label":29},"f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c",{"logo":52,"logoDark":52,"favicon":52,"tagline":53},null,"Web Developer & Headless CMS — Hull, East Yorkshire",{"summary":55,"copyright":52,"address":56,"contact":60,"socialLinks":61,"openingHours":73},"Senior web developer in Hull, East Yorkshire. I built HD CMS — a headless platform powering 15+ live business websites. Bespoke sites from £20/month.",{"line1":52,"line2":52,"city":57,"state":58,"postalCode":52,"country":59},"Hull","East Yorkshire","United Kingdom",{"phone":52,"email":52},[62,65,69],{"url":46,"icon":63,"platform":64},"fa-brands fa-youtube","youtube",{"url":66,"icon":67,"platform":68},"https://github.com/adamcjackson1982","fa-brands fa-github","github",{"url":70,"icon":71,"platform":72},"https://www.linkedin.com/in/adamcjackson/","fa-brands fa-linkedin","linkedin",[],{"id":75,"slug":76,"title":77,"excerpt":78,"content":79,"featured_image":369,"published":11,"published_at":370},"ebc85ae0-e6be-4889-9d9d-d4c22ade0839","building-pwas-for-real-people-not-perfect-networks","Building PWAs for Real People, Not Perfect Networks","The assumption of perfect connectivity is a luxury most users don't have. Here's how to build for the reality of flaky networks and interrupted sessions.",{"type":80,"content":81},"doc",[82,88,92,96,103,107,152,156,161,165,173,181,189,197,202,206,248,253,257,261,293,297,302,306,348,353,357,361,365],{"type":83,"content":84},"paragraph",[85],{"text":86,"type":87},"Most web applications are built assuming reliable Wi-Fi. The developer has fast broadband. The staging environment is on a local network. Testing happens in ideal conditions. Then the app ships to users who are nowhere near ideal conditions.","text",{"type":83,"content":89},[90],{"text":91,"type":87},"The users I build for work in warehouses with concrete walls that kill signal. They're on construction sites where the only internet is a phone hotspot that drops constantly. They're in rural areas where 3G is still common and 4G is a luxury.",{"type":83,"content":93},[94],{"text":95,"type":87},"Building for these users changed how I think about every technical decision.",{"type":97,"attrs":98,"content":100},"heading",{"level":99},2,[101],{"text":102,"type":87},"Why \"Perfect Wi-Fi\" Assumptions Fail",{"type":83,"content":104},[105],{"text":106,"type":87},"The failure modes are predictable:",{"type":108,"content":109},"bulletList",[110,122,132,142],{"type":111,"content":112},"listItem",[113],{"type":83,"content":114},[115,120],{"text":116,"type":87,"marks":117},"Loading states that never resolve.",[118],{"type":119},"bold",{"text":121,"type":87}," A spinner that assumes the request will eventually complete. It won't. The user stares at it for 30 seconds, then closes the app.",{"type":111,"content":123},[124],{"type":83,"content":125},[126,130],{"text":127,"type":87,"marks":128},"Lost form data.",[129],{"type":119},{"text":131,"type":87}," A user fills out a detailed form. Hits submit. Network drops. The request fails. The form resets. Everything is gone.",{"type":111,"content":133},[134],{"type":83,"content":135},[136,140],{"text":137,"type":87,"marks":138},"Stale data presented as current.",[139],{"type":119},{"text":141,"type":87}," Cached data displays but there's no indication it might be outdated. The user makes decisions based on information that changed hours ago.",{"type":111,"content":143},[144],{"type":83,"content":145},[146,150],{"text":147,"type":87,"marks":148},"Silent failures.",[149],{"type":119},{"text":151,"type":87}," An action appears to succeed because the UI updated optimistically. But the sync failed. The user doesn't know until much later when the data is wrong.",{"type":83,"content":153},[154],{"text":155,"type":87},"These aren't edge cases. For users in challenging network environments, they're the default experience.",{"type":97,"attrs":157,"content":158},{"level":99},[159],{"text":160,"type":87},"UX for Failure States",{"type":83,"content":162},[163],{"text":164,"type":87},"Every network operation can fail. The UX needs to handle that gracefully.",{"type":83,"content":166},[167,171],{"text":168,"type":87,"marks":169},"Timeouts with recovery.",[170],{"type":119},{"text":172,"type":87}," Don't wait forever. Set reasonable timeouts. When they hit, offer options: retry, work offline, or abandon. Never leave the user trapped in a loading state.",{"type":83,"content":174},[175,179],{"text":176,"type":87,"marks":177},"Persistent input.",[178],{"type":119},{"text":180,"type":87}," Any user input should survive network failures, app restarts, and browser crashes. Store form data locally as the user types. Restore it when they return. Never lose their work.",{"type":83,"content":182},[183,187],{"text":184,"type":87,"marks":185},"Clear sync status.",[186],{"type":119},{"text":188,"type":87}," Users should always know if their data is synced, pending, or failed. A subtle icon is enough — checkmark for synced, clock for pending, warning for failed. Don't hide this information.",{"type":83,"content":190},[191,195],{"text":192,"type":87,"marks":193},"Actionable errors.",[194],{"type":119},{"text":196,"type":87}," \"Network error\" tells the user nothing useful. \"Changes saved locally. Will sync when connected.\" tells them exactly what happened and what to expect.",{"type":97,"attrs":198,"content":199},{"level":99},[200],{"text":201,"type":87},"Mobile-First Realities",{"type":83,"content":203},[204],{"text":205,"type":87},"Mobile isn't just \"smaller screens.\" It's fundamentally different constraints:",{"type":108,"content":207},[208,218,228,238],{"type":111,"content":209},[210],{"type":83,"content":211},[212,216],{"text":213,"type":87,"marks":214},"Interrupted sessions.",[215],{"type":119},{"text":217,"type":87}," Users get phone calls. They switch apps. They walk into elevators. Sessions are constantly interrupted. State must persist across these interruptions.",{"type":111,"content":219},[220],{"type":83,"content":221},[222,226],{"text":223,"type":87,"marks":224},"Variable bandwidth.",[225],{"type":119},{"text":227,"type":87}," A user might start on Wi-Fi, walk outside, drop to 4G, then hit a dead zone. The app needs to handle all of these within a single session.",{"type":111,"content":229},[230],{"type":83,"content":231},[232,236],{"text":233,"type":87,"marks":234},"Battery concerns.",[235],{"type":119},{"text":237,"type":87}," Aggressive polling and constant network activity drain batteries. Users notice. Batch operations where possible. Use push instead of poll where available.",{"type":111,"content":239},[240],{"type":83,"content":241},[242,246],{"text":243,"type":87,"marks":244},"Touch targets.",[245],{"type":119},{"text":247,"type":87}," Error recovery UI needs to work with fingers, not mice. Retry buttons need to be large enough to tap accurately. Error messages need to be readable on small screens.",{"type":97,"attrs":249,"content":250},{"level":99},[251],{"text":252,"type":87},"Performance Budgets",{"type":83,"content":254},[255],{"text":256,"type":87},"A performance budget isn't a guideline. It's a constraint that shapes every decision.",{"type":83,"content":258},[259],{"text":260,"type":87},"For the users I build for, the budget is tight:",{"type":108,"content":262},[263,273,283],{"type":111,"content":264},[265],{"type":83,"content":266},[267,271],{"text":268,"type":87,"marks":269},"Initial load under 3 seconds on 3G.",[270],{"type":119},{"text":272,"type":87}," That means aggressive code splitting, minimal JavaScript, and smart preloading. Every kilobyte matters.",{"type":111,"content":274},[275],{"type":83,"content":276},[277,281],{"text":278,"type":87,"marks":279},"Time to interactive under 5 seconds.",[280],{"type":119},{"text":282,"type":87}," The app shell needs to render fast and become usable fast. Users won't wait for a full data fetch before they can do anything.",{"type":111,"content":284},[285],{"type":83,"content":286},[287,291],{"text":288,"type":87,"marks":289},"Subsequent navigations under 1 second.",[290],{"type":119},{"text":292,"type":87}," Once the app is loaded, navigation should feel instant. Service worker caching, prefetching, and local data make this possible.",{"type":83,"content":294},[295],{"text":296,"type":87},"These aren't arbitrary numbers. They're based on when real users give up and try something else.",{"type":97,"attrs":298,"content":299},{"level":99},[300],{"text":301,"type":87},"Designing for Interruption",{"type":83,"content":303},[304],{"text":305,"type":87},"The assumption that users complete tasks in a single session is wrong. They get interrupted. They come back hours later. They switch devices.",{"type":108,"content":307},[308,318,328,338],{"type":111,"content":309},[310],{"type":83,"content":311},[312,316],{"text":313,"type":87,"marks":314},"Save state constantly.",[315],{"type":119},{"text":317,"type":87}," Don't wait for explicit save actions. Auto-save everything. Make it impossible to lose progress.",{"type":111,"content":319},[320],{"type":83,"content":321},[322,326],{"text":323,"type":87,"marks":324},"Support resume.",[325],{"type":119},{"text":327,"type":87}," When a user returns, put them back where they were. Don't make them start over or navigate back to their task.",{"type":111,"content":329},[330],{"type":83,"content":331},[332,336],{"text":333,"type":87,"marks":334},"Handle stale sessions.",[335],{"type":119},{"text":337,"type":87}," A session started yesterday might resume today. Data might have changed. Handle the reconciliation gracefully.",{"type":111,"content":339},[340],{"type":83,"content":341},[342,346],{"text":343,"type":87,"marks":344},"Work across devices.",[345],{"type":119},{"text":347,"type":87}," Users might start on their phone and finish on their laptop. Cloud sync makes this possible. Local-only storage makes it impossible.",{"type":97,"attrs":349,"content":350},{"level":99},[351],{"text":352,"type":87},"Real Feedback Loops",{"type":83,"content":354},[355],{"text":356,"type":87},"The best insights come from watching real users in real conditions. Not usability labs with perfect Wi-Fi. Actual field use.",{"type":83,"content":358},[359],{"text":360,"type":87},"I've sat with users in warehouses, watching them struggle with apps that worked perfectly in my office. The problems are obvious when you see them. The user taps a button. Nothing happens for 10 seconds. They tap again. Now there are duplicate requests. The app state is confused.",{"type":83,"content":362},[363],{"text":364,"type":87},"Those observations drive better technical decisions than any amount of theoretical architecture discussion.",{"type":83,"content":366},[367],{"text":368,"type":87},"Build for the users you actually have, in the conditions they actually face. Everything else is optimising for a fantasy.","https://sktegczfmabucrnpgrvp.supabase.co/storage/v1/object/public/media/7578a1b2-a8ec-462f-8980-d46ebefc76a2/2025/12/1767022902275-blog-hero-real-people.svg","2025-12-29T14:00:00+00:00",1774075395602]