Upgrade to Pro — share decks privately, control downloads, hide ads and more …

No-build Utopia: Modern User Experiences with R...

No-build Utopia: Modern User Experiences with Rails and Web Standards

Avatar for Ryan Townsend

Ryan Townsend

April 17, 2026

More Decks by Ryan Townsend

Other Decks in Programming

Transcript

  1. N o - build U t op i a :

    MODERN USER EXPERIENCES wi t h R ail s & W eb S t and a rd s Ryan Townsend Wrocloverb 2026
  2. w a s r ight * * This asterisk is

    going to do some lifting
  3. +15% o n D e sk t op , +10%

    M obi l e Source: bsky.app/profile/tkadlec.bsky.social/post/3mhxwzlsqhc2b
  4. Ecommerce Amazon (1995), Booking.com (1996), OpenTable (1999), Shopify (2006), Airbnb

    (2008) Webmail Hotmail (1996), Gmail (2004) Live Chat / Helpdesk LiveChat (2002), MSN Web Messenger (2004), Facebook Messenger (2011), Intercom (2011), Zendesk Chat (2014) Interactive Maps MapQuest (1996), Google Maps (2005) Analytics & Dashboards Google Analytics (2005), Mixpanel (2009) Social Media LinkedIn (2002), Facebook (2004), Twitter (2006), Pinterest (2010), Instagram Web (2010) Media Streaming Vimeo (2004), YouTube (2005), Netflix (2007), Spotify (2008) Real-time Collaboration Google Docs (2006), Canva (2012), Slack (2013), Airtable (2013), Notion (2016), Figma (2016), Miro (2017) Real-time A/V Twitch (2011), Discord (2015), Zoom (2016), Riverside (2019)
  5. Source: youtu.be/J8WH72hOnF4 T h is w a s 2013* *

    it was sunset due to performance issues
  6. • Frameworks • Libraries • Bundlers • Transpilers • Dev

    Servers • Hot Reloading W e ' r e FORCED TO MAINTAIN
  7. Ryan checks out Rails 0.9 from SVN We YOLO our

    static resources into /public 2005
  8. Rails 1.1 introduced RJS page.insert_html / .replace_html page.show / .hide

    / .toggle / .remove page.visual_effect (script.aculo.us) page.call 2006
  9. 2009 2011 Early adopters of Sprockets Rails 3.1 intros Asset

    Pipeline: concatenate -> minify -> fingerprint CoffeeScript & Sass (later known as SCSS)
  10. Rails 5.1 introduced Webpacker for React, Vue, Angular etc. Resume-driven

    development ramps up: Microservices, NoSQL, GraphQL, SPAs 2017
  11. 2021 Rails 7.0 integrated Import Maps Hotwire & Turbo replaced

    Turbolinks Propshaft: a minimal Sprockets replacement For the 99.999%, this is enough:
  12. • View Transitions • Speculation Rules • Compression Dictionaries •

    Invokers • Declarative Partial Updates }5 APIs to convince you to reach for The Web Platform before NPM
  13. WHEN? Level 1: ✅ Safari 18+ ✅ Chrome 111+ ✅

    Firefox 144+ Level 2: ✅ Safari 18.2+ ✅ Chrome 126+ ❌ Firefox Note: this is a progressive enhancement, fallback just means no animation.
  14. WHAT? A declarative API for prefetching HTML or even fully

    prerendering pages before user navigation
  15. { "prerender": [ { "where": { "or": [{ "href_matches": "/products/*",

    "relative_to": "document" }] }, "eagerness": "moderate" } ] }
  16. 2 1

  17. Eagerness Level Desktop Mobile conservative Pointer down Touch down moderate

    Hover for 200ms Complex viewport heuristics eager Hover for 10ms In viewport for 50ms immediate ASAP in idle time ASAP in idle time Source: docs.google.com/document/d/1YPbtUPfZIDElzBZNx8IQMzRFvy8oauLG_i1XIr6jgTs
  18. Action Behaviour Resource Use Impact prerender Loads full page Resource

    hungry Fastest experience, if accurate prerender-until-script Currently in Chrome trial Loads full page, until a <script> tag is encountered Preparser still runs Fairly heavy on resources Usually instant first paint, delayed interactivity prefetch Coming soon to Safari Loads only HTML No preparser Least resource hungry Instant TTFB but traditional progressive download experience
  19. WHEN? Prefetch: ⌛ Safari ✅ Chrome 109+ ❌ Firefox Prerender:

    ❌ Safari ✅ Chrome 109+ ❌ Firefox Note: this is a progressive enhancement, also you could fallback to manual prefetch Resource Hints
  20. WHY? Mitigate the cache busting impact of your release velocity

    by ‘upgrading’ resources. Achieve the best of both worlds: the cache granularity of individual small files with the compression ratios of concatenated bundles.
  21. HOW? Some HTTP headers and a little elbow grease to

    apply compression* * Okay, this one is currently a little more complex, but the impact is worth it
  22. header footer header footer + = = header footer +

    header footer JSON fetch HTML diff Shell Dictionary MPAs (Multi-Page Applications) SPAs (Single Page Applications) New Page New Page
  23. Browser Server Content-Encoding: dcz application.v2.js Compresses response with dictionary Decompresses

    with dictionary application.v1.js No dictionary (yet) Use-As-Dictionary: match="/application.*.js" Content-Encoding: br Stores as a dictionary application.v1.js Serves with regular compression Stores file as dictionary application.v2.js Available-Dictionary: :a1b2c3d4e5f6: Accept-Encoding: dcz, zstd, br, gzip Loads dictionary Transmits dictionary hash with request (new release to production)
  24. WHEN? ⌛ Safari ✅ Chrome 130+ ⌛ Firefox Note: this

    is a progressive enhancement, fallback to ZStandard, Brotli, GZIP etc.
  25. <button type="button" commandfor="edit-dialog" command="show-modal"> Edit </button> <dialog id="edit-dialog" closedby="any"> <button

    type="button" commandfor="edit-dialog" command="close"> Close </button> <%= form_for(product) do |f| %> ... <% end %> </dialog>
  26. <button type="button" commandfor="item-quantity" command="--step-up"> Increment </button> <input id="item-quantity" type="number" step="1"

    min="1" /> <button type="button" commandfor="item-quantity" command="--step-down"> Decrement </button>
  27. <form action="/articles/1/like"> <button type="submit" interestfor="who-liked"> Like this article </button> </form>

    <ol id="who-liked" popover="hint"> <li>Tim Kadlec</li> <li>Sergey Chernyshev</li> <li>Alex Krivit</li> <li>Mike Kozicki</li> </ol>
  28. WHEN? Invoker Commands: ✅ Safari 26.2+ ✅ Chrome 135+ ✅

    Firefox 144+ Interest Invokers: ❌ Safari ✅ Chrome 142+ ❌ Firefox Note: there are polyfills for Invoker Commands and Interest Invokers
  29. WHY? No complex JavaScript necessary to patch the DOM. No

    isomorphic rendering necessary. Take slow rendering out of the critical TTFB path and get your <head> delivered ASAP. I'm mostly excited where this can go in future...
  30. HOW? Mostly HTML <template> tags and a couple that look

    like old school PHP. A little JavaScript to stream content into the document.
  31. const response = await fetch(...) document.startViewTransition(async () => { await

    response.body .pipeThrough(new TextDecoderStream()) .pipeTo(document.body.streamAppendHTMLUnsafe()); });
  32. Browser Server Navigation (link / form) Accept: text/html+fragment, text/html, */*

    Content-Type: text/html+fragment With fragment Apply change to DOM Content-Type: text/html Without fragment Hard-navigate to new document or redirect if status = 3XX
  33. if (document.body.streamAppendHTMLUnsafe) { navigation.addEventListener("navigate", async (event) => { // if

    we can't intercept, ignore if (!event.canIntercept) return; // if the element (or an ancestor) has opted-out, return early if (event.sourceElement.closest("[data-hard-navigation]")) return; // an alternative opt-in approach: // if (!event.sourceElement.closest("[data-soft-navigation]")) return; event.intercept({ async handler() { // make the request const response = await fetch(event.destination.url, { method: event.formData ? "POST" : "GET", headers: new Headers({ // favour fragments, if our server supports them "Accept": "text/html+fragment, text/html, */*" }), body: event.formData, // don't follow redirects, we'll handle manually redirect: "manual" }); // handle redirects if (response.status >= 300 && response.status < 400) { window.location.href = response.headers.get("Location"); return; // handle our fragment response } else if (response.headers.get("Content-Type")?.includes("text/html+fragment")) { document.startViewTransition(async () => { await response.body .pipeThrough(new TextDecoderStream()) .pipeTo(document.body.streamAppendHTMLUnsafe()); }); // fallback to HTML } else { const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); document.startViewTransition(() => { document.documentElement.replaceWith(doc.documentElement); }); } } }); }); } A whopping 614 bytes with Brotli compression
  34. WHEN? ❌ Safari 🧪 Chrome Canary ❌ Firefox Note: this

    is VERY early days. One to follow for the next 6-12 months. Enable experiment with Chrome Canary flags: --enable-blink-features=HTMLProcessingInstruction,DocumentPatching,NewHTMLSettingFunctions
  35. View Transitions Fluid app-like UI transitions Speculation Rules Instant hard

    navigations Compression Dictionaries Reduced network overhead Invokers Closing of the Uncanny Valley between render and pages becoming interactive Declarative Partial Updates A potential future of server-side rendered (SSR'd) soft navigations
  36. – J ohn M a y n ard K e

    yn e s “The market can remain irrational longer than you can remain solvent”