<?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: Taylor Hunt</title>
    <description>The latest articles on DEV Community by Taylor Hunt (@tigt).</description>
    <link>https://dev.to/tigt</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F127429%2F370e6334-33ec-4c82-8937-f1a7116ddd89.jpg</url>
      <title>DEV Community: Taylor Hunt</title>
      <link>https://dev.to/tigt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tigt"/>
    <language>en</language>
    <item>
      <title>Why not React?</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Sun, 17 Sep 2023 04:22:50 +0000</pubDate>
      <link>https://dev.to/tigt/why-not-react-2f8l</link>
      <guid>https://dev.to/tigt/why-not-react-2f8l</guid>
      <description>&lt;p&gt;This was an internal analysis I wrote when challenged if Kroger.com could do the same MPA speed tricks as &lt;a href="https://dev.to/tigt/so-what-c8j"&gt;my Marko demo&lt;/a&gt;, but still using React because, well, you know.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;It’s from around 2021, so some questions got answered, some predictions got proven wrong or boring-in-retrospect, and React’s official SSR story actually released. I once planned a rewrite, but I’m burnt out and can’t be arsed. (Feel free to tell me what I got wrong in the comments.)&lt;/p&gt;

&lt;p&gt;However, I’m publishing it (with some updated hyperlinks) because this post might still be useful to someone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://twitter.com/royalicing/status/1520983370410254336" rel="noopener noreferrer"&gt;React’s streaming has interesting flaws&lt;/a&gt;, and &lt;a href="https://calendar.perfplanet.com/2022/mobile-performance-of-next-js-sites/" rel="noopener noreferrer"&gt;the official RSC implementation ain’t exactly fast&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=ZKH3DLT4BKw" rel="noopener noreferrer"&gt;Low-end devices remain stubbornly stagnant 2 years later&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addyosmani.com/blog/react-server-components-app-router/" rel="noopener noreferrer"&gt;React indeed says it’s changing in unfamiliar ways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The intro became more relevant than I could have possibly known when I wrote it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t believe me? Well, a WebPageTest is worth a thousand posts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;center&gt;&lt;small&gt;&lt;em&gt;WebPageTest results for: Simulated 3G, emulated MOTOG4, Los Angeles&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;
  &lt;thead&gt;&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;th&gt;Next.js RSCs + streaming&lt;/th&gt;
&lt;th&gt;Marko &lt;/th&gt;
&lt;th&gt;HackerNews (control)&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;

  &lt;tr&gt;
&lt;th&gt;URL
    &lt;/th&gt;
&lt;td&gt;
&lt;a href="https://next-news-rsc.vercel.sh" rel="noopener noreferrer"&gt;next-news-rsc.vercel.sh&lt;/a&gt;
    &lt;/td&gt;
&lt;td&gt;
&lt;a href="https://marko-hackernews.ryansolid.workers.dev" rel="noopener noreferrer"&gt;marko-hackernews.&lt;br&gt;ryansolid.workers.dev&lt;/a&gt;
    &lt;/td&gt;
&lt;td&gt;
&lt;a href="https://news.ycombinator.com" rel="noopener noreferrer"&gt;news.ycombinator.com&lt;/a&gt;

  &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;WPT link
    &lt;/th&gt;
&lt;td&gt;
&lt;a href="https://www.webpagetest.org/result/230717_BiDcVJ_9GN/1/details/" rel="noopener noreferrer"&gt;230717_BiDcVJ_9GN&lt;/a&gt;
    &lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.webpagetest.org/result/230717_AiDc35_A1Q/3/details/" rel="noopener noreferrer"&gt;230717_AiDc35_A1Q&lt;/a&gt;
    &lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.webpagetest.org/result/230717_AiDcNY_A0G/2/details/" rel="noopener noreferrer"&gt;230717_AiDcNY_A0G&lt;/a&gt;

  &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;JS
    &lt;/th&gt;
&lt;td&gt;94.9 kB
    &lt;/td&gt;
&lt;td&gt;0.3 kB
    &lt;/td&gt;
&lt;td&gt;1.9 kB

  &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;HTML
    &lt;/th&gt;
&lt;td&gt;9.5 kB
    &lt;/td&gt;
&lt;td&gt;3.8 kB
    &lt;/td&gt;
&lt;td&gt;5.8 kB

  &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;text/x-component&lt;/code&gt;
    &lt;/th&gt;
&lt;td&gt;111.1 kB
    &lt;/td&gt;
&lt;td&gt;0 kB
    &lt;/td&gt;
&lt;td&gt;0 kB

  &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;Waterfall chart
    &lt;/th&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgtw1qp9bc76d5aqkwna.png" width="800" height="569"&gt;
    &lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftg05u5yt7c3e1b5keknw.png" width="800" height="246"&gt;
    &lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fermt62x32ecyhtlixypf.png" width="800" height="286"&gt;

&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I’m not including timings, since unfortunately WPT seems to have stopped using real Androids, and &lt;a href="https://www.youtube.com/watch?v=4bZvq3nodf4&amp;amp;t=617s" rel="noopener noreferrer"&gt;Chrome’s CPU throttling is way too optimistic for low-end devices&lt;/a&gt;. However, note that browsers can’t parse &lt;code&gt;text/x-component&lt;/code&gt; natively&lt;sup id="fnref2"&gt;2&lt;/sup&gt;, so that parsing is blocked behind JS and the parse time is more punishing on low-end devices than usual.&lt;/p&gt;

&lt;p&gt;I’m also not sure if I should test &lt;a href="https://next-edge-demo.netlify.app/rsc" rel="noopener noreferrer"&gt;next-edge-demo.netlify.app/rsc&lt;/a&gt; instead, but &lt;a href="https://www.webpagetest.org/result/230717_BiDcTQ_B5W/" rel="noopener noreferrer"&gt;its results seemed so inconsistent&lt;/a&gt; I wasn’t sure it’s functioning correctly.&lt;/p&gt;

&lt;p&gt;Anyway, time for the original analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can we get React to be as fast as the Kroger Lite demo?
&lt;/h2&gt;

&lt;p&gt;Probably not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay smart guy, why not?
&lt;/h2&gt;

&lt;p&gt;This is not going to be a short answer; bear with me. I’m going to start with an abstract point, then back it up with concrete evidence for my pessimism.&lt;/p&gt;




&lt;p&gt;Code struggles to escape why it was created. &lt;strong&gt;You can often trace the latest version’s strengths and weaknesses all the way back to the goals of the original authors.&lt;/strong&gt; Is that because of backwards compatibility? Developer cultures? Feedback loops? Yes, and tons of other reasons. The broader effect is known as &lt;a href="https://en.wikipedia.org/wiki/Path_dependence" rel="noopener noreferrer"&gt;path dependence&lt;/a&gt;, where the best way to predict a technology’s future is to examine its past.&lt;/p&gt;

&lt;p&gt;Front-end frameworks are no exception:&lt;/p&gt;

&lt;dl&gt;
  &lt;dt&gt;
&lt;a href="https://www.offerzen.com/blog/rich-harris-on-why-he-created-svelte" rel="noopener noreferrer"&gt;Svelte was invented to embed data visualizations into other web pages.&lt;/a&gt;
  &lt;/dt&gt;
&lt;dd&gt;💪 Svelte has first-class transitions and fine-grained updates, because those things are very important for good dataviz.
  &lt;dd&gt;🤕 Svelte’s &lt;a href="https://github.com/sveltejs/svelte/issues/958" rel="noopener noreferrer"&gt;streaming nonsupport&lt;/a&gt; and &lt;a href="https://gist.github.com/ryansolid/71e2b160df4db33fcca2862355377983" rel="noopener noreferrer"&gt;iconoclastic “𝑋 components↦bundle size” curve&lt;/a&gt; make sense if you consider the code Svelte was invented for didn’t &lt;b&gt;do&lt;/b&gt; those things in the first place.

  &lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;
&lt;a href="https://en.wikipedia.org/wiki/AngularJS#Development_history" rel="noopener noreferrer"&gt;Angular was made to quickly build internal desktop web apps.&lt;/a&gt;
  &lt;/dt&gt;
&lt;dd&gt;💪 Angular fans are right about how quickly you can make a functional UI.
  &lt;dd&gt;🤕 If you think about what big-company workstations are like and the open-in-a-tab-all-day nature of intranet desktop webapps, Angular’s performance tradeoffs are entirely rational — until you try using it for mobile.

  &lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;
&lt;a href="https://www.youtube.com/watch?v=KVZ-P-ZI6W4&amp;amp;t=94s" rel="noopener noreferrer"&gt;React was created to stop Facebook’s org chart from Conway’s Law-ing all over their desktop site’s front-end.&lt;/a&gt;
  &lt;/dt&gt;
&lt;dd&gt;💪 React has absolutely proven its ability to let teams of any size, any degree of cooperation, and any skill level work together on large codebases.
  &lt;dd&gt;🤕 As for React’s original weaknesses… Well, don’t take my word for it, &lt;a href="https://www.youtube.com/watch?v=KVZ-P-ZI6W4&amp;amp;t=310s" rel="noopener noreferrer"&gt;take it from the React team&lt;/a&gt;: &lt;blockquote&gt;So people started playing around with this internally, and everyone had the same reaction. They were like, “Okay, A.) I have no idea how this is going to be performant enough, but B.) it’s &lt;em&gt;so&lt;/em&gt; fun to work with.” Right? &lt;em&gt;Everybody&lt;/em&gt; was like, “This is so cool, I don’t really &lt;em&gt;care&lt;/em&gt; if it’s too slow — somebody will make it faster.”&lt;/blockquote&gt;

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;That quote explains a lot of things, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React’s historical promises that new versions will make your app faster for you&lt;/li&gt;
&lt;li&gt;The pattern of React websites appointing specialized front-end performance teams&lt;/li&gt;
&lt;li&gt;Why big companies like React, since departmentalizing concerns so other departments don’t worry about them is how big companies &lt;em&gt;work&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m not claiming those things are wrong! I’m saying it’s a &lt;em&gt;consistent philosophy&lt;/em&gt;. For once, I’m not even being snide about React’s performance; the React team are very open about their strategy of relieving framework consumers from worrying about performance, by having the framework authors do it for them. I even think that strategy works for (most of) Meta’s websites!&lt;/p&gt;

&lt;p&gt;But so far, it &lt;a href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/" rel="noopener noreferrer"&gt;has not made websites overall any faster&lt;/a&gt;. And the things the React team have resorted to have gotten odder and more involved. Lastly, &lt;strong&gt;our site (and indeed, most sites) are not very similar to facebook.com&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;
  🔮 &lt;b&gt;Note from the future&lt;/b&gt;
  &lt;br&gt;
Because it’s the obvious question to ask: &lt;a href="https://twitter.com/RyanCarniato/status/1565209384241602560" rel="noopener noreferrer"&gt;Marko got started when eBay devs wanted to use Node.js, and the business said “Okay, but it can’t regress performance”&lt;/a&gt;.&lt;br&gt;
💪 Strength: &lt;a href="https://calendar.perfplanet.com/2014/async-fragments-rediscovering-progressive-html-rendering-with-marko/" rel="noopener noreferrer"&gt;&lt;em&gt;It didn’t.&lt;/em&gt;&lt;/a&gt; That’s not typical for JS frameworks.&lt;br&gt;
🤕 Weakness: Marko’s early focus on performance instead of outreach/integrations beyond what eBay uses/etc. also explains why most devs haven’t heard of it.&lt;br&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  But can React be that fast &lt;em&gt;anyway?&lt;/em&gt; It’s all just code; it can be worked with.
&lt;/h2&gt;

&lt;p&gt;It’s code &lt;em&gt;and&lt;/em&gt; path dependencies &lt;em&gt;and&lt;/em&gt; culture &lt;em&gt;and&lt;/em&gt; a support ecosystem, each with their own values, golden paths, and pewter paths. Let’s examine those concretely — we’ll look at &lt;strong&gt;how feasible &lt;em&gt;technically&lt;/em&gt; it would be to have React perform at the same speed as the Kroger Lite demo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Starting with what traits are desirable for MPAs:&lt;/p&gt;

&lt;dl&gt;

&lt;dt&gt;📃 Streamed HTML
&lt;/dt&gt;
&lt;dd&gt;Incrementally flush parts of the page to the HTTP stream, so pages aren’t as slow as their slowest part.

&lt;/dd&gt;
&lt;dt&gt;🥾 Fast boot
&lt;/dt&gt;
&lt;dd&gt;If JS reboots on every navigation, it should do so quickly.

&lt;/dd&gt;
&lt;dt&gt;🥀 Hydration correctness
&lt;/dt&gt;
&lt;dd&gt;Like airplane takeoff, hydration is a few moments where any of a hundred tiny things could ruin the rest of the trip.
&lt;dd&gt;In MPAs, it’s vital to reconcile DOM updates from user input during load, as that “edge case” becomes a repeat occurrence.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;🏸 Fast server runtime
&lt;/dt&gt;
&lt;dd&gt;If we’re leaning on the server, it better render efficiently.
&lt;dd&gt;Even more important for spinning up distributed datacenter instances, edge rendering, &lt;a href="https://developers.google.com/web/updates/2018/05/beyond-spa" rel="noopener noreferrer"&gt;Service Worker rendering&lt;/a&gt;, and other near-user processors.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;🍂 Tree-shakeable framework
&lt;/dt&gt;
&lt;dd&gt;SPAs assume use of all of framework’s features eventually, so they bundle them to get into JS engines’ caches early. MPAs want to remove code from the critical path if it’s not used in it, amortizing framework cost across pages.

&lt;/dd&gt;
&lt;dt&gt;🧠 Multi-page mental model
&lt;/dt&gt;
&lt;dd&gt;If a component only renders on the server, should you pretend it’s in the DOM?
&lt;dd&gt;If you can’t have API parity between server and client, provide clear and obvious boundaries.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;h3&gt;
  
  
  📃 Streamed HTML
&lt;/h3&gt;

&lt;p&gt;I consider streaming HTML to be &lt;strong&gt;the&lt;/strong&gt; most important thing for consistently-high-performance server rendering. If you didn’t read &lt;a href="https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2"&gt;the post about it&lt;/a&gt;, here’s the difference it makes:&lt;/p&gt;



&lt;a href="https://assets.codepen.io/183091/HTML+streaming+vs.+non.mp4" rel="noopener noreferrer"&gt;Download video: HTML Streaming vs. Non-streaming&lt;/a&gt;
&lt;a href="https://assets.codepen.io/183091/html-streaming.vtt" rel="noopener noreferrer"&gt;Here’s the closed captions&lt;/a&gt;, since Forem doesn’t allow &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; elements.



&lt;h4&gt;
  
  
  Important facets of incremental HTML streaming
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both explicit &lt;em&gt;and&lt;/em&gt; implicit flushes;&lt;/strong&gt; early &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; for asset loading, around API calls, at TCP window size boundaries…&lt;/li&gt;
&lt;li&gt;All flushes, but especially implicit ones, should &lt;strong&gt;avoid too much chunking&lt;/strong&gt;: overeager flushing defeats compression, inflates HTTP encoding overhead, bedevils TCP scheduling/fragmentation, and hitches Node’s event loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested component flushes&lt;/strong&gt; help avoid contorting code to expose flush details at the renderer’s top level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-order server rendering&lt;/strong&gt;, for when APIs don’t return in the same order they’re used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-order flushes&lt;/strong&gt; , &lt;a href="https://engineering.fb.com/2010/06/04/web/bigpipe-pipelining-web-pages-for-high-performance/" rel="noopener noreferrer"&gt;so inessential components don’t hold up the rest of the page (like Facebook’s old BigPipe)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlling render dependencies of nested and out-of-order flushes&lt;/strong&gt; is important to prevent displaying funky UI states. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid-stream render errors&lt;/strong&gt; should finish cleanly without wasting resources, and emit an &lt;code&gt;error&lt;/code&gt; event on the stream so &lt;a href="https://github.com/marko-js/community/pull/1" rel="noopener noreferrer"&gt;the HTTP layer can properly signal the error state&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunked component hydration&lt;/strong&gt;, so component interactivity matches component visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marko has been optimizing those important subtleties for almost a decade, and React… hasn’t.&lt;/p&gt;

&lt;p&gt;Additionally, we had a brownfield tax. Kroger.com didn’t render its React app to a stream, so that app had many stream incompatibilities of the kind described here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generally, any pattern that uses the server render pass to generate markup that needs to be added to the document &lt;em&gt;before&lt;/em&gt; the SSR-ed chunk will be fundamentally incompatible with streaming.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67" rel="noopener noreferrer"&gt;What’s New With Server-Side Rendering in React 16 § Streaming Has Some Gotchas&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🥾 Fast boot
&lt;/h3&gt;

&lt;p&gt;SPAs’ core tradeoff: the first page load can be painful in order for fast future interactions. But in MPAs, &lt;em&gt;every&lt;/em&gt; page load must be fast.&lt;/p&gt;

&lt;h4&gt;
  
  
  Costs in JS runtime boot
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Download time&lt;/li&gt;
&lt;li&gt;Parse time &amp;amp; effort&lt;/li&gt;
&lt;li&gt;Compilation: compile time, bytecode memory pressure, and JIT bailouts&lt;/li&gt;
&lt;li&gt;Execution (repeats every page, regardless of JIT caching)&lt;/li&gt;
&lt;li&gt;Initial memory churn/garbage collection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only some of those costs can be skipped on future page loads, with the difficulty increasing as you go down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloads are skipped with HTTP caching.&lt;/li&gt;
&lt;li&gt;Modern browsers are smart enough to background and cache parses, but not for all &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;s — either from script characteristics or parallelization limits.&lt;/li&gt;
&lt;li&gt;Compiler caches intentionally require multiple executions to store an entire script, and compilation bailouts can thrash for a surprisingly long time.&lt;/li&gt;
&lt;li&gt;Execution can never be &lt;em&gt;skipped&lt;/em&gt;, but warm JIT caches and stored execution contexts can slash properly-planned runtimes.&lt;/li&gt;
&lt;li&gt;Memory churn and overhead during load is impossible to avoid — intentionally by the ECMAScript standard, even.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Luckily, &lt;a href="https://v8.dev/blog/cost-of-javascript-2019" rel="noopener noreferrer"&gt;v8 moved most parses to background threads&lt;/a&gt;. Unfortunately, while that doesn’t block the main thread, parsing still has to &lt;em&gt;finish&lt;/em&gt;. This is exacerbated by Android devices’ &lt;a href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener noreferrer"&gt;big.LITTLE architecture&lt;/a&gt;, where the available LITTLE cores are much slower than the cores already occupied with core browser tasks in the main, network, and compositor threads.&lt;/p&gt;

&lt;p&gt;More on v8’s JS costs and caching, as it’s the primary target for low-spec Androids and JavaScript servers alike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://v8.dev/blog/cost-of-javascript-2019" rel="noopener noreferrer"&gt;The cost of JavaScript in 2019&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://v8.dev/blog/code-caching-for-devs" rel="noopener noreferrer"&gt;Code caching for JavaScript developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://v8.dev/blog/improved-code-caching" rel="noopener noreferrer"&gt;Improved code caching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Does React play nice with JS engines’ boot process?
&lt;/h4&gt;

&lt;p&gt;Remember React’s demos back in the day that got interactive much faster than competing frameworks? React’s lazier one-way data flow was the key, as it didn’t spend time building dependency graphs like most of its peers.&lt;/p&gt;

&lt;p&gt;Unfortunately, that’s the only nice thing I found about React and JS engines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The sheer &lt;a href="https://bundlephobia.com/scan-results?packages=react,react-dom" rel="noopener noreferrer"&gt;size of &lt;code&gt;react&lt;/code&gt; + &lt;code&gt;react-dom&lt;/code&gt;&lt;/a&gt; negatively affects every step of JS booting: parse time, going to ZRAM on cheap Androids, and eviction from compiler caches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React components are functions or classes with big &lt;code&gt;render&lt;/code&gt; methods, which defeats eager evaluation, duplicates parse workload via &lt;a href="https://v8.dev/blog/preparser" rel="noopener noreferrer"&gt;lazy compilation&lt;/a&gt;, and frustrates code caching:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;One caveat with code caching is that it only caches what’s being eagerly compiled. This is generally only the top-level code that’s run once to setup global values. Function definitions are usually lazily compiled and aren’t always cached.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://medium.com/reloading/javascript-start-up-performance-69200f43b201#5aaa" rel="noopener noreferrer"&gt;JavaScript Start-up Performance&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;React prioritizes stable in-page performance at the expense of reconciliation and memory churn at boot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React renders and VDOM results are infamously &lt;a href="https://erdem.pl/2019/08/v-8-function-optimization" rel="noopener noreferrer"&gt;megamorphic&lt;/a&gt;, so JS compilers waste cycles optimizing and bailing out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React’s synthetic event system slows event listener attachment and makes early user input sluggish each load.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Rehydration performance
&lt;/h4&gt;

&lt;p&gt;Rehydration spans all of the above considerations for JS boot, and was the primary culprit in our performance traces. You can’t ignore rehydration costs for the theoretical ideal React MPA.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance metrics collected from real websites using SSR rehydration indicate its use should be heavily discouraged. Ultimately, the reason comes down to User Experience: it’s extremely easy to end up leaving users in an “uncanny valley”.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://developers.google.com/web/updates/2019/02/rendering-on-the-web#rehydration" rel="noopener noreferrer"&gt;Rendering on the Web § A Rehydration Problem: One App for the Price of Two&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thus, faster rehydration is almost as common as incremental HTML in the React ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/25983001/strategies-for-server-side-rendering-of-asynchronously-initialized-react-js-comp" rel="noopener noreferrer"&gt;Strategies for server-side rendering of asynchronously initialized React.js components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react-server.io/docs/intro/why-react-server" rel="noopener noreferrer"&gt;Why React Server § Streaming client initialization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/LukasBombach/next-super-performance" rel="noopener noreferrer"&gt;next-super-performance&lt;/a&gt; — &lt;a href="https://medium.com/@luke_schmuke/how-we-achieved-the-best-web-performance-with-partial-hydration-20fab9c808d5" rel="noopener noreferrer"&gt;The case of partial hydration (with Next and Preact)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Ephem/react-lightyear" rel="noopener noreferrer"&gt;react-lightyear&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Along with their own caveats, each implementation runs into the same limitations from React itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Mandatory &lt;code&gt;[data-reactroot]&lt;/code&gt; wrappers hurt DOM size and reflow time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React listens for events on each render root, which increases memory usage and slows event handling since previous &lt;code&gt;===&lt;/code&gt; invariants are no longer guaranteed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Virtual DOM imposes a lot of rehydration overhead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render the entire component tree&lt;/li&gt;
&lt;li&gt;Read back the existing DOM&lt;/li&gt;
&lt;li&gt;Diff the two&lt;/li&gt;
&lt;li&gt;Render the reconciled component tree&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s a lot of work to show something nigh-identical to when you started!&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🥀 Rehydration correctness
&lt;/h3&gt;

&lt;p&gt;Cursory research turns up &lt;em&gt;a lot&lt;/em&gt; of folks struggling to hand off SSR’d HTML to React:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://farmdev.com/thoughts/107/why-server-side-rendering-in-react-is-so-hard/" rel="noopener noreferrer"&gt;Why Server Side Rendering In React Is So Hard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://joshwcomeau.com/react/the-perils-of-rehydration/" rel="noopener noreferrer"&gt;The Perils of Rehydration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.jakoblind.no/case-study-ssr-react/" rel="noopener noreferrer"&gt;Case study of SSR with React in a large e-commerce app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.logrocket.com/fixing-gatsbys-rehydration-issue/" rel="noopener noreferrer"&gt;Fixing Gatsby’s rehydration issue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gatsbyjs/gatsby/issues/17914" rel="noopener noreferrer"&gt;gatsbyjs#17914: [Discussion] Gatsby, React &amp;amp; Hydration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facebook/react/issues?q=is%3Aissue+label%3A" rel="noopener noreferrer"&gt;React bugs for “Server Rendering”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No, really, skim those links. The nature of their problems strongly suggests that React was not designed for SSR, and thus uniquely struggles with it. If you think that’s an opinion, consider the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;React handles &lt;a href="https://medium.com/the-svt-tech-blog/react-suspense-and-server-rendering-da3711b2a26a#907a" rel="noopener noreferrer"&gt;intentional differences between client and server render&lt;/a&gt; about as ungracefully as possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If we use React for regions of more-complex interactivity throughout a page, what’s the best way to handle the rest of the page?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it a pain to share state/props/etc. across multiple React “islands” on a page? Do Hooks behave oddly if you do that?&lt;/li&gt;
&lt;li&gt;Can we use Portals to get around this? (Note Portals don’t SSR.)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;React’s error handling when rehydrating is… nonexistent. By default, it rejects showing any errors to the user in favor of flashing content or tearing down entire DOM trees into blank nodes.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;blockquote&gt;

&lt;p&gt;React 16 doesn’t fix mismatched SSR-generated HTML attributes&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67" rel="noopener noreferrer"&gt;What’s New with Server-Side Rendering in React 16 § Gotchas&lt;/a&gt;&lt;/p&gt;


&lt;/blockquote&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;That’s… kind of a big deal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Loses interaction state like focus, selection, &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, edited &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;s on hydration. Losing users’ work is maddening in the best case, and this issue is magnified over slow networks/bad reception/low-spec devices. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Has issues with controlled elements &lt;a href="https://twitter.com/dan_abramov/status/1200119993712726016" rel="noopener noreferrer"&gt;when the boot process catches up and event listeners fire all at once&lt;/a&gt;. Note the above demo is using future Suspense APIs to solve a problem all React apps can fall into today.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🏸 Fast server runtime
&lt;/h3&gt;

&lt;p&gt;Server-side optimizations for React are more common than anything else in this analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://rax.js.org/" rel="noopener noreferrer"&gt;Rax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/walmartlabs/react-ssr-optimization" rel="noopener noreferrer"&gt;react-ssr-optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nearform.com/blog/speeding-up-react-ssr-announcing-esx/" rel="noopener noreferrer"&gt;ESX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zekchan/react-ssr-error-boundary" rel="noopener noreferrer"&gt;react-ssr-error-boundary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/the-svt-tech-blog/react-suspense-and-server-rendering-da3711b2a26a#69e4" rel="noopener noreferrer"&gt;React suspense and server rendering § So what’s the catch?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;…and a million others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;small&gt;(The cynical takeaway is that because developers have to pay for React’s inefficiencies on servers, they are directly incentivized to to fix them, as opposed to inefficiences on clients.)&lt;/small&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Isomorphic rendering is not a helpful abstraction for tweaking performance between server-side vs. client-side — web applications often end up CPU-bound on arbitrarily-slow user devices, but memory-bound on servers with resources split between connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A fast, efficient runtime can double as &lt;a href="https://developers.google.com/web/updates/2018/05/beyond-spa" rel="noopener noreferrer"&gt;Service Worker rendering to streams&lt;/a&gt; for offline rendering, without needing to ship heavier CSR for things that don’t need it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, almost all prior art for optimizing React server render involves caching output, which won’t help for Service Workers, EdgeWorkers, cloud functions, etc. So the suggested “trisomorphic rendering” pattern (which the demo bet on for offline) is probably a no-go with React.&lt;/p&gt;

&lt;h3&gt;
  
  
  🍂 Tree-shakeable runtime
&lt;/h3&gt;

&lt;p&gt;Omitting code saves load time, memory use, and evaluation cost — including on the server! Svelte’s “disappearing framework” philosophy would be the logical conclusion of a tree-shakeable runtime — or maybe its reduction to absurdity, for &lt;a href="https://github.com/feltcoop/why-svelte#the-bundle-size-inflection-point" rel="noopener noreferrer"&gt;Svelte’s pathological cases&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/facebook/react/issues/16173" rel="noopener noreferrer"&gt;Facebook has considered modularizing React, but didn’t conclude it was a win for how they use it.&lt;/a&gt; They also experimented with &lt;a href="https://github.com/facebook/react/issues/15257" rel="noopener noreferrer"&gt;an event system that tree-shook events you didn’t listen for&lt;/a&gt;, but abandoned it as well.&lt;/p&gt;

&lt;p&gt;In short: nope.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Multi-page mental model
&lt;/h3&gt;

&lt;p&gt;The most extreme MPA mental model probably belongs to Ruby on Rails. Rails wholly bets on server-side rendering, and even abstracts its JavaScript APIs to resemble its HTTP-invoked Controller/Model/View paradigm.&lt;/p&gt;

&lt;p&gt;At the other end, you have React:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSX prefers properties to HTML attributes&lt;/li&gt;
&lt;li&gt;Server-side React pretends to be in a browser&lt;/li&gt;
&lt;li&gt;The ecosystem strives to imitate DOM features on the server &lt;/li&gt;
&lt;li&gt;Differences between server and browser renders are considered failures of isomorphic JavaScript that should be fixed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pretending the server is a browser makes sense if you only use HTML as a fancy skeleton screen, and hydration is only a hiccup at the start of long sessions. But that abstraction gets leakier and more annoying the more times the original web page lifecycle occurs. Why bother with the &lt;code&gt;className&lt;/code&gt; alias if markup may only ever render as &lt;code&gt;class&lt;/code&gt;? Why treat SSR markup as a tree when it’s only ever a stream?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;React plus forms (and other DOM-stored state) is annoying, controlled or not — will that be painful when we lean more on native browser features?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React’s special handling of &lt;code&gt;.defaultValue&lt;/code&gt; and &lt;code&gt;.defaultChecked&lt;/code&gt; vs. &lt;code&gt;.value&lt;/code&gt; and &lt;code&gt;.checked&lt;/code&gt; can get very confusing across SSR-only vs. CSR-only vs. both&lt;/li&gt;
&lt;li&gt;React’s &lt;code&gt;onChange&lt;/code&gt; pretending to be the &lt;code&gt;input&lt;/code&gt; event, and the resulting bugs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Is it more likely we’d persist SPA habits that are MPA antipatterns if we continue with React?&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Using another language/template system/whatever for non-React bits seems not ideal, but that’s how it’s been done for years anyway — &lt;a href="https://medium.com/@dan_abramov/two-weird-tricks-that-fix-react-7cf9bbdef375#486f" rel="noopener noreferrer"&gt;especially since React straight-up can’t handle the outer scaffold of a web page&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;React’s abstractions/features are designed to work at runtime&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even without a build step, which is an impedance mismatch for apps with build steps. Not an intractable problem, but one with ripple effects that disfavor it.&lt;/li&gt;
&lt;li&gt;Leads to unusual code that JS engines haven’t optimized, such as &lt;a href="https://stackoverflow.com/questions/59791769/what-is-the-react-official-position-for-throwing-promises-inside-render-function" rel="noopener noreferrer"&gt;the infamous thrown &lt;code&gt;Promise&lt;/code&gt;&lt;/a&gt;, or Hooks populating a call index state graph via multiple closures.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Many new React features don’t currently work on the server, with vacillating or unclear timeframes for support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://reactjs.org/docs/code-splitting.html#reactlazy" rel="noopener noreferrer"&gt;&lt;code&gt;.lazy()&lt;/code&gt; and Suspense&lt;/a&gt; &lt;em&gt;[EDITOR’S NOTE: yes I know they do now]&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Portals&lt;/li&gt;
&lt;li&gt;&lt;a href="https://asmagin.com/2019/01/18/catching-server-side-rendering-errors-in-react-jss-apps/" rel="noopener noreferrer"&gt;Error Boundaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;How long does it usually take for React to bring client APIs to server parity? That lag may forecast similar problems in the future.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Speaking of problems in the future…&lt;/p&gt;

&lt;h2&gt;
  
  
  What about where React is going?
&lt;/h2&gt;

&lt;p&gt;As we’ve seen, there’s much prior art of features desirable for fast SSR: incremental HTML streams, in-page patching for out-of-order rendering, omitting JS for components that never rerender on the client, compiling to a more efficient output target for the server, SSRing React APIs that otherwise aren’t, etc.&lt;/p&gt;

&lt;p&gt;There is no reason to think that these individual upgrades are inherently incompatible with each other — with &lt;a href="https://github.com/Ephem/react-lightyear#does-reactlazy-work-with-lightyear" rel="noopener noreferrer"&gt;enough glue code&lt;/a&gt;, debugging, and linting, React theoretically &lt;em&gt;could&lt;/em&gt; have the rendering tricks I found useful in Marko.&lt;/p&gt;

&lt;p&gt;But we’ve already gotten a taste of future compat problems with our React SSR relying on deprecated APIs. What about React’s upcoming APIs? Do we want to redo our SSR optimizations with every major version?&lt;/p&gt;

&lt;p&gt;This risk is made more annoying by &lt;a href="https://reactjs.org/docs/concurrent-mode-adoption.html#what-to-expect" rel="noopener noreferrer"&gt;the upcoming APIs’ expected drawbacks&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our experience, code that uses idiomatic React patterns and doesn’t rely on external state management solutions is the easiest to get running in the Concurrent Mode.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This makes me worry that the previously-mentioned possibility of a state manager to synchronize multiple React “islands” will be mutually exclusive with Concurrent Mode. Less specifically, I doubt we’d be sticking to “idiomatic React patterns”.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://twitter.com/acdlite/status/1228727481764466688" rel="noopener noreferrer"&gt;the words of a Facebook engineer&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Better comparison is to a hard fork where we don’t maintain any backwards compatibility at all. React itself might be the sunk cost fallacy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essentially, future React will be different enough to break many of the reasons companies rely on it today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Knowledge gained from experience with the library&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://twitter.com/AdamRackis/status/1236460463199850496" rel="noopener noreferrer"&gt;3rd-party libraries&lt;/a&gt; (in &lt;a href="https://twitter.com/dai_shi/status/1236276385489883137" rel="noopener noreferrer"&gt;deep ways&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Established patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Potentially worse is how Suspense would affect React’s memory consumption. The VDOM roughly triples the DOM’s memory usage (real DOM + incoming VDOM + diffed-against VDOM), and &lt;a href="https://reactjs.org/docs/concurrent-mode-intro.html#interruptible-rendering" rel="noopener noreferrer"&gt;the “double-buffering” used in Suspense’s examples&lt;/a&gt; will worsen that. React also considered slimming its synthetic event system, but Concurrent Mode would break without it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you look at how Fiber is written, the architecture truly makes no sense and is unnecessarily complex… except to support Concurrent Mode.&lt;/p&gt;

&lt;p&gt;Similar case in design of hooks. If not for concurrency, we woulda just used mutation.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://twitter.com/acdlite/status/1228728395564867584" rel="noopener noreferrer"&gt;Andrew Clark&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So Concurrent Mode promises smart scheduling about long JavaScript tasks so React’s cost can be interrupted, but the ways React had to change for Concurrent Mode caused other possible issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worse raw performance from frequently yielding the main thread&lt;/li&gt;
&lt;li&gt;Garbage collection pressure from Hooks&lt;/li&gt;
&lt;li&gt;Memory consumption from multiple simultaneous React trees&lt;/li&gt;
&lt;li&gt;Risks &lt;a href="https://gist.github.com/bvaughn/054b82781bec875345bd85a5b1344698" rel="noopener noreferrer"&gt;“tearing” during hydration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Doubling down on synthetic events instead of omitting them for less bundle size&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/reactjs/rfcs/pull/147" rel="noopener noreferrer"&gt;Existing patterns that didn’t break before, but will&lt;/a&gt; — often &lt;a href="https://www.framer.com/motion/component/##performance" rel="noopener noreferrer"&gt;patterns that libraries needed to eke out performance&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What if, instead of trying to be clever about doing a lot of work in the browser, we instead &lt;em&gt;did less work&lt;/em&gt; in the browser? &lt;em&gt;That’s&lt;/em&gt; why Kroger Lite is fast. It’s &lt;a href="https://timkadlec.com/remembers/2023-06-01-performance-is-not-a-checklist/" rel="noopener noreferrer"&gt;not just from box-ticking the features I mentioned&lt;/a&gt;, it’s because its technologies were chosen and app code was written in service of that principle.&lt;/p&gt;




&lt;p&gt;It may be wise to judge future React not as a migration, but as a completely different framework. Its guarantees, risks, mental model, and benefits are no longer the same. And it really seems to be pushing the depths of framework-level cleverness.&lt;/p&gt;

&lt;p&gt;Assume we nail the above; it takes little time to augment React, there are no unforeseen bugs, and teams quickly update components to reap the rewards. We would nevertheless shoulder some drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More painful upgrades to React versions with internal or breaking changes&lt;/li&gt;
&lt;li&gt;Required linting/CI/etc. to ensure React features aren’t used in ways or contexts that would cause problems&lt;/li&gt;
&lt;li&gt;Unknown compatibility issues with ecosystem code like Jest, 3&lt;sup&gt;rd&lt;/sup&gt; party components, React DevTools, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What new rehydration problems will we see with Concurrent Mode, async rendering, and time-slicing?&lt;/li&gt;
&lt;li&gt;Will hook call order remain persistent across React islands, Suspense, deferred updating, different renderers, combinations of all of those, &lt;a href="https://github.com/reactjs/rfcs/blob/84d88774f8edc2210c147572c724fae36d8dc26d/text/0000-anonymous-components.md#motivation" rel="noopener noreferrer"&gt;or scenarios I haven’t anticipated&lt;/a&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This analysis is harsh on React’s MPA suitability. But is that so odd?&lt;/p&gt;

&lt;p&gt;It was created to client-render non-core bits of Facebook. Its maintainers only recently used it for server rendering, navigation, or delivering traditional web content. In fact, &lt;a href="https://twitter.com/sebmarkbage/status/1516907614566854659" rel="noopener noreferrer"&gt;its SSR was a happy accident&lt;/a&gt;. And finally, &lt;a href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/" rel="noopener noreferrer"&gt;longstanding evidence holds React trends antagonistic towards performance.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why &lt;strong&gt;would&lt;/strong&gt; React be good at the things we ask it to do?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the FB5 redesign, Facebook is finally using React in the ways that we are, &lt;a href="https://twitter.com/dan_abramov/status/1259614150386425858" rel="noopener noreferrer"&gt;and they have found it wanting&lt;/a&gt;. On the one hand, this means React will surely become much better at desirable SSR features. On the other, &lt;em&gt;when&lt;/em&gt; this will happen is unsure, it will heavily change React’s roadmap, and React could change so much that familiarity with how it works today could be a liability rather than a strength.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For the target audience of rural/new/poorly-connected customers, does Facebook even use React to serve them? Did FB5 change anything, or &lt;strong&gt;does &lt;code&gt;m.facebook.com&lt;/code&gt; still not use React?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If we want a version of Kroger.com as fast as the demo, but &lt;strong&gt;using the same framework, processes, management, and developers as the existing site — wouldn’t that just become our existing site?&lt;/strong&gt; We can’t change our personnel, but we &lt;em&gt;can&lt;/em&gt; change the technologies we build on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Last, but certainly not least: &lt;strong&gt;can you make an industry-beating app out of industry-standard parts?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Men would rather ignore &lt;a href="https://www.usac.org/lifeline/resources/program-data/" rel="noopener noreferrer"&gt;20% of the US&lt;/a&gt; than stop using React.&lt;sup id="fnref3"&gt;3&lt;/sup&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Well, &lt;a href="https://en.wikipedia.org/wiki/HTML_Components" rel="noopener noreferrer"&gt;&lt;em&gt;one&lt;/em&gt; browser can natively parse &lt;code&gt;text/x-component&lt;/code&gt;&lt;/a&gt;, sorta. I guess RSCs reused the MIME type as an easter egg? ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Or &lt;a href="https://blogs.microsoft.com/on-the-issues/2019/04/08/its-time-for-a-new-approach-for-mapping-broadband-data-to-better-serve-americans/" rel="noopener noreferrer"&gt;50% of the US, depending on what you count&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>Follow-ups to the “Streets” series</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Sun, 17 Sep 2023 04:22:40 +0000</pubDate>
      <link>https://dev.to/tigt/follow-ups-to-the-streets-series-3h4b</link>
      <guid>https://dev.to/tigt/follow-ups-to-the-streets-series-3h4b</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/tigt/series/16560"&gt;The original Streets series&lt;/a&gt; was adapted from a bunch of internal writeups I did at Kroger to explain myself and my demo, and there’s plenty of leftovers that didn’t fit into the original “tight five”. Since nothing came of that, and I’ve since given up on my original plan to reuse that knowledge (more on that later), I’m putting the rest onto the Web so that maybe &lt;em&gt;someone&lt;/em&gt; can find them useful or interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Answers to some questions
&lt;/h2&gt;

&lt;p&gt;Feel free to ask more questions in the comments!&lt;/p&gt;

&lt;h3&gt;
  
  
  Why should we trust a framework team member about front-end framework performance?
&lt;/h3&gt;

&lt;p&gt;I was laid off by eBay in February 2023. I am now equally untrustworthy as I was before.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should you do when a server error happens in the middle of an HTML stream?
&lt;/h3&gt;

&lt;p&gt;There isn’t a perfect solution (yet, the HTTPWG has been thinking about it), but there’s at least &lt;em&gt;something&lt;/em&gt; you can do to avoid errors being cached on clients and prevent search engines from indexing pages with mid-stream errors.&lt;/p&gt;

&lt;p&gt;You can emulate 5XX error semantics (that is: do not cache &lt;strong&gt;and&lt;/strong&gt; permissible to retry the request to see if error is fixed) by mucking around at the HTTP level: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HTTP version&lt;/th&gt;
&lt;th&gt;Error signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;td&gt;Corrupt the &lt;code&gt;chunked&lt;/code&gt; &lt;code&gt;Transfer-Encoding&lt;/code&gt; syntax or never terminate it properly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2&lt;/td&gt;
&lt;td&gt;Send a &lt;code&gt;RST_STREAM&lt;/code&gt; frame with a code of &lt;code&gt;0x2 INTERNAL_ERROR&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/3&lt;/td&gt;
&lt;td&gt;Like h2, but &lt;a href="https://github.com/quicwg/base-drafts/issues/3300" rel="noopener noreferrer"&gt;the frame name differs I think?&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPDY&lt;/td&gt;
&lt;td&gt;Why are you still using this&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Research and further details on doing this correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marko-js/community/pull/1" rel="noopener noreferrer"&gt;marko-js/community#1 Handling mid-stream errors&lt;/a&gt; (it’s a WIP pull request with an evolving discussion, sorry you’ll have to read the whole thing before the advice becomes clear)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/httpwg/http-core/issues/895" rel="noopener noreferrer"&gt;httpwg/http-core#895 Mid-stream error semantics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why did you think Kroger Lite would work?
&lt;/h3&gt;

&lt;p&gt;I didn’t, but it was worth a shot. And then for a while, it really seemed like it &lt;em&gt;was&lt;/em&gt; working. But yeah, big companies historically seem content to let internal efforts to improve speed wither:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://twitter.com/AndyDavies/status/1519578423433351168" rel="noopener noreferrer"&gt;Andy Davies on Twitter&lt;/a&gt;:&lt;br&gt;
Shortly after I did this talk BazaarVoice demo’d a slimmed down version but don’t think it ever saw the light of day&lt;/p&gt;

&lt;p&gt;Amazing really, especially after their internal panic when a customer talked about how disabling BazaarVoice increased revenue by several millions pounds / year&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://twitter.com/grigs/status/1519456108712058881" rel="noopener noreferrer"&gt;Jason Grigsby on Twitter&lt;/a&gt;:&lt;br&gt;
I doubt I’ll be able to publish about it, but I’m not seeing anything different than what  @AndyDavies talked about in &lt;a href="https://noti.st/andydavies/dCBdI2/fast-fashion-how-missguided-revolutionised-their-approach-to-site-performance" rel="noopener noreferrer"&gt;noti.st/andydavies/dCB…&lt;/a&gt; and &lt;a href="https://youtube.com/watch?v=mLzt23ZVGx0" rel="noopener noreferrer"&gt;youtube.com/watch?v=mLzt23…&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s four years later and BV’s performance is basically the same. It’s depressing.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;…and indeed it also happened to me. I’m partial to Baldur Bjarnason’s explanation that &lt;a href="https://softwarecrisis.dev/letters/ai-and-software-quality/" rel="noopener noreferrer"&gt;software quality and software income are basically uncorrelated&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In most sectors of the software industry, sales performance and product quality are disconnected.&lt;/p&gt;

&lt;p&gt;By its nature software has enormous margins which further cushion it from the effect of delivering bad products.&lt;/p&gt;

&lt;p&gt;The objective impact of poor software quality on the bottom lines of companies like Microsoft, Google, Apple, Facebook, or the retail side of Amazon is a rounding error. The rest only need to deliver usable early versions, but once you have an established customer base and an experienced sales team, you can coast for a long, &lt;em&gt;long&lt;/em&gt; time without improving your product in any meaningful way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe only a competitor with a really fast site could make big slow React sites get faster?&lt;/p&gt;

&lt;h3&gt;
  
  
  Was it the choice of Marko that doomed Kroger Lite?
&lt;/h3&gt;

&lt;p&gt;I dunno. Marko at the time had components, syntax, and a VDOM like React; &lt;em&gt;and&lt;/em&gt; powered a bigger, more successful ecommerce site; &lt;em&gt;and&lt;/em&gt; had an agreeable license thanks to the Open JS Foundation.&lt;/p&gt;

&lt;p&gt;If &lt;em&gt;that&lt;/em&gt; wasn’t a realistic alternative to React, then what possibly could be?&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s with the cover images?
&lt;/h3&gt;

&lt;p&gt;I made them. I tried outsourcing because doing it myself took longer than my ADHD liked, but finding an artist open to commissions for this kind of thing took &lt;em&gt;even longer&lt;/em&gt;. (This was before popular text-to-image models, but those are gauche anyway.) &lt;/p&gt;

&lt;p&gt;I made most of the images by combining photographs in the &lt;a href="https://www.gimp.org" rel="noopener noreferrer"&gt;The GIMP&lt;/a&gt;. (Tip: use the hell out of &lt;a href="https://www.gimp.org/tutorials/Layer_Masks/" rel="noopener noreferrer"&gt;layer masks&lt;/a&gt;.) The source images were from &lt;a href="https://help.duckduckgo.com/duckduckgo-help-pages/features/image-license/" rel="noopener noreferrer"&gt;DuckDuckGo’s Image Search by License&lt;/a&gt;, &lt;a href="https://commons.wikimedia.org/wiki/Main_Page" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;, and photos by US government personnel that &lt;a href="https://en.wikipedia.org/wiki/Copyright_status_of_works_by_the_federal_government_of_the_United_States" rel="noopener noreferrer"&gt;default into the public domain&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I made the train map for &lt;a href="https://dev.to/tigt/routing-im-not-smart-enough-for-a-spa-5hki"&gt;&lt;cite&gt;Routing: I’m not smart enough for a SPA&lt;/cite&gt;&lt;/a&gt; with &lt;a href="https://metromapmaker.com" rel="noopener noreferrer"&gt;Metro Map Maker&lt;/a&gt;. (Which, yes, is a SPA. It could be fun to replicate it as an MPA with &lt;code&gt;&amp;lt;input type=image&amp;gt;&lt;/code&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is the series called “Streets”, anyway?
&lt;/h3&gt;

&lt;p&gt;See next question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where’s that code you threatened us with?
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;And by the end, some code I &lt;em&gt;dare&lt;/em&gt; you to try.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na"&gt;&lt;cite&gt;Making the world’s fastest website, and other mistakes&lt;/cite&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was to be &lt;code&gt;streets&lt;/code&gt;, an NPM package to help produce a web thang as fast as the demo site I showed off. (Without using the demo’s site original code, since Kroger owns the copyright on it.)&lt;/p&gt;

&lt;p&gt;Why “streets”? Turns out a street is different from a road in some important ways:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A feature universal to all streets is a human-scale design that gives its users the space and security to feel engaged in their surroundings, whatever through traffic may pass. — &lt;a href="https://en.wikipedia.org/wiki/Street" rel="noopener noreferrer"&gt;Street · Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;small&gt;(In this analogy, SPAs are &lt;a href="https://en.wikipedia.org/wiki/Stroad" rel="noopener noreferrer"&gt;stroads&lt;/a&gt;.)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;That seemed perfect for my &lt;a href="https://adactio.com/journal/18628" rel="noopener noreferrer"&gt;design priorities&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🛡 Reliable:&lt;/strong&gt; respect everyone’s safety and circumstances

&lt;ul&gt;
&lt;li&gt;Security features built-in to where your server is in control&lt;/li&gt;
&lt;li&gt;Offline support without SPA overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;♿️ Accessible:&lt;/strong&gt; respect the web’s diversity, longevity, and ubiquity

&lt;ul&gt;
&lt;li&gt;Fast, accessible-by-default navigations&lt;/li&gt;
&lt;li&gt;Browser support as deep as you need&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💨 Fast:&lt;/strong&gt; respect users’ time and money

&lt;ul&gt;
&lt;li&gt;Interactive in 150ms on $20 devices&lt;/li&gt;
&lt;li&gt;Payload small enough for 2G networks&lt;/li&gt;
&lt;li&gt;Avoid jams with streaming&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But I burnt out. Yes, I’m seeing a therapist. I also started drawing as a hobby for the first time in ≈10 years.&lt;/p&gt;

&lt;p&gt;I’d love to make Streets, but I can’t keep going it alone anymore. I’m also not sure who would really bother using it — my original idea was developers beholden to the public good via government sites, charities, etc., but I realized those organizations probably require certified vendors or something.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/tigt/97448740d829d8dd341413086d1595f3" rel="noopener noreferrer"&gt;Here’s a Rollup config very similar to the demo’s&lt;/a&gt;, though. That’ll be handy if you want the kind of speed it had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now what?
&lt;/h2&gt;

&lt;p&gt;The next post will be adapted from my original analysis of why I doubted Kroger.com could approach the demo’s speed if it remained wedded to React. It &lt;em&gt;was&lt;/em&gt; part of the original Streets posts, but at the time I was afraid of people getting angry at me over it.&lt;/p&gt;

&lt;p&gt;The one nice thing about burnout? I don’t care about that sort of thing anymore.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>discuss</category>
      <category>performance</category>
    </item>
    <item>
      <title>I think I finally “get” JS objects</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Tue, 30 May 2023 14:31:53 +0000</pubDate>
      <link>https://dev.to/tigt/i-think-i-finally-get-js-objects-o6f</link>
      <guid>https://dev.to/tigt/i-think-i-finally-get-js-objects-o6f</guid>
      <description>&lt;p&gt;While learning JavaScript, at some point I looked up what “object-oriented” meant.&lt;/p&gt;

&lt;p&gt;That was a mistake.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;first reason&lt;/strong&gt; it was a mistake was because I was exposed to way too many unhelpful OOP diagrams. You know the kind:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://commons.wikimedia.org/wiki/File:Oop_class_diagram.svg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9folkh627ckbaamgnl6w.png" alt="A diagram about class vs. instance vs. extensions that looks like it was drawn in Powerpoint. It’s about faces and hats or something; if you really want to read the text, this links to the original SVG." width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://commons.wikimedia.org/wiki/File:Oop_class_diagram.svg" rel="noopener noreferrer"&gt;This diagram from Wikimedia Commons&lt;/a&gt; is typical: it models something you’d never code in a real program. (Why is that so common?)&lt;/p&gt;



&lt;p&gt;The &lt;strong&gt;other reason&lt;/strong&gt; it was a mistake was because it taught me about Java and C++ programmers’ opinions on objects… but those opinions barely mattered to the language I was learning. I read about “information hiding”, “encapsulation”, and “polymorphism”, but I didn’t understand how they were relevant.&lt;/p&gt;

&lt;p&gt;And as it were, they mostly &lt;em&gt;weren’t&lt;/em&gt; relevant!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Some jargon was for &lt;strong&gt;techniques difficult in other languages, but so easy in JavaScript&lt;/strong&gt; that you don’t even think about them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some jargon was for &lt;strong&gt;how low-level languages would optimize performance&lt;/strong&gt;, in ways that JavaScript is too high-level to care about.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some jargon described techniques &lt;strong&gt;that JavaScript objects can’t even &lt;em&gt;do&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And, lastly, some jargon was for &lt;strong&gt;bad ideas&lt;/strong&gt; — even in the originating languages.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For years, I was dimly aware that it seemed unnecessary to mess with prototypes, classes, constructors, and &lt;code&gt;this&lt;/code&gt; for like, 98% of my code — so why was I expected to use them for everything, or I wasn’t doing object-orientation “properly”?&lt;/p&gt;

&lt;h2&gt;
  
  
  It turns out object-oriented code is great at some things, but not all things
&lt;/h2&gt;

&lt;p&gt;What I’ve since realized is that JavaScript reuses its full-power Objects for many non-OO purposes, most of which are much simpler. I’ve used JS objects for roughly 4 things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grouping variables&lt;/li&gt;
&lt;li&gt;Namespacing&lt;/li&gt;
&lt;li&gt;Typed-ish data&lt;/li&gt;
&lt;li&gt;Actual full-power Objects&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first 3 aren’t “real” object-orientation (or whatever that means), but they reuse the object machinery JavaScript already has because it’s convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Grouping variables
&lt;/h3&gt;

&lt;p&gt;Sometimes, you want to collect related variables together. Other languages features for that are called dictionaries, hashes, maps, or structs. &lt;a href="https://docs.raku.org/language/setbagmix" rel="noopener noreferrer"&gt;Perl, being Perl, has 6 ways to do it, with names like “BagHash”.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s like prefixing variable names that go together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;player_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;player_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;player_health&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// or:&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usually the specifics of &lt;em&gt;how exactly&lt;/em&gt; to group variables in other languages is for performance reasons. For example, C’s structs group values together to make them efficient byte-wise, but if you want to look up the properties at runtime by name, you’ll need a hash function/table instead.&lt;/p&gt;

&lt;p&gt;These sorts of grouped variables as objects are also used to 1.) return multiple values, 2.) implement optional arguments in a friendly way, and 3.) other places where the language doesn’t support passing around multiple variables at once. As a counterexample, &lt;a href="https://stackoverflow.com/a/47785854/2522637" rel="noopener noreferrer"&gt;Python has optional keyword function arguments&lt;/a&gt; &lt;em&gt;and&lt;/em&gt; &lt;a href="https://pythonbasics.org/multiple-return/" rel="noopener noreferrer"&gt;multiple return values&lt;/a&gt;, so it doesn’t use objects for either. But in JavaScript, we use object and object accessories for both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCoordinates&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;relativeTo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Using object destructuring for optional arguments&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relativeTo&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relativeTo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHorizontalOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relativeTo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getVerticalOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relativeTo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHorizontalOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Returning 3 related variables in the same object&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Namespacing
&lt;/h3&gt;

&lt;p&gt;There’s no &lt;em&gt;technical&lt;/em&gt; reason that &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math" rel="noopener noreferrer"&gt;the &lt;code&gt;Math&lt;/code&gt; object&lt;/a&gt; exists. But there is a &lt;em&gt;human&lt;/em&gt; reason: it’s easy to remember math-related stuff lives in this object.&lt;/p&gt;

&lt;p&gt;They &lt;em&gt;could&lt;/em&gt; be separate identifiers with &lt;code&gt;math_&lt;/code&gt; prefixes — older languages did that all the time. But this seems a little cleaner; contrast &lt;a href="https://www.php.net/manual/en/funcref.php" rel="noopener noreferrer"&gt;the myriad functions in PHP’s global scope&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Modern-day JavaScript now handles namespacing with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules" rel="noopener noreferrer"&gt;module &lt;code&gt;import&lt;/code&gt;s and &lt;code&gt;export&lt;/code&gt;s&lt;/a&gt;, but you can still wrap those up into an object, the syntax is similar, and many old browser APIs and libraries still use objects for namespacing.&lt;/p&gt;

&lt;p&gt;(This is also known as &lt;code&gt;static&lt;/code&gt; properties/methods, which kind of sucks as a name and infected JS from other, less fun programming languages. Go @ TC39 about it.)&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Typed-ish data objects
&lt;/h3&gt;

&lt;p&gt;Typed-ish data objects (a very rigorous term I just made up) bundle data with ways to read and manipulate said data, like &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;DOMRect&lt;/code&gt;, or &lt;code&gt;Array&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Typed-ish data objects are when &lt;code&gt;this&lt;/code&gt; and methods become useful — methods that don’t use &lt;code&gt;this&lt;/code&gt; are just namespaced functions.&lt;/li&gt;
&lt;li&gt;I think this is the level where &lt;code&gt;instanceof&lt;/code&gt; would become useful, but JS/DOM design mistakes popularized duck typing in the JavaScript world anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These objects let you change their data in different ways, and the different ways to read that data will update to match your changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "Sat Apr 11 2020"&lt;/span&gt;
&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "Tue Aug 11 2020"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These kinds of objects can use (and often do use) information-hiding/internal representations, but it’s not required. For example, a &lt;code&gt;Date&lt;/code&gt; only stores one UNIX timestamp as an integer, and it even lets you look at and change that integer directly if you want. And as for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray" rel="noopener noreferrer"&gt;TypedArray&lt;/a&gt;, well, it’s in the name.&lt;/p&gt;

&lt;p&gt;For example, a &lt;code&gt;Color&lt;/code&gt; object where the constructor takes any string that CSS can parse as a color, then expands it into an object with &lt;code&gt;R&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, and &lt;code&gt;A&lt;/code&gt; properties. (This part is left as an exercise to the reader, because it’s surprisingly hard.)&lt;/p&gt;

&lt;p&gt;At first, an object shaped like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;R&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;G&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;177&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…doesn’t seem &lt;em&gt;that&lt;/em&gt; much more useful than using hex codes directly in JS like &lt;code&gt;0xff28b1ff&lt;/code&gt;. But by making an actual color &lt;code&gt;Object&lt;/code&gt; you can &lt;code&gt;new&lt;/code&gt; up, we can add useful features, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We could enforce that &lt;code&gt;R&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, and &lt;code&gt;B&lt;/code&gt; never exceed 255 or go lower than 0 with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set" rel="noopener noreferrer"&gt;a setter&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;It could have &lt;code&gt;tint()&lt;/code&gt; and &lt;code&gt;shade()&lt;/code&gt; methods that are easier to understand than manual RGB tinkering.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Color&lt;/code&gt; object could return the numeric equivalent of its hexadecimal code for its &lt;code&gt;.valueOf(),&lt;/code&gt; output a human-friendly &lt;code&gt;hsla()&lt;/code&gt; syntax for its &lt;code&gt;.toString()&lt;/code&gt;, or output to an array for data transfer with &lt;code&gt;.toJSON()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Full-force capital-O internal state Objects, also known as &lt;em&gt;Art&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Finally, you have the full-force objects that act as discrete… well, objects. Like DOM elements, &lt;code&gt;Document&lt;/code&gt;, and so on. They use the hell out of &lt;code&gt;this&lt;/code&gt;, and need to. (&lt;a href="https://prog21.dadgum.com/228.html" rel="noopener noreferrer"&gt;It’s &lt;em&gt;possible&lt;/em&gt; to model these sorts of things in functional programming&lt;/a&gt;, but it involves trying to avoid &lt;code&gt;this&lt;/code&gt; so hard you end up inventing &lt;code&gt;that&lt;/code&gt;.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HTMLInputElement.validity.validate()&lt;/code&gt;, though, is very good. It uses a bunch of properties on one specific user-visible object, then changes what the element is doing to reflect its calculations. And if you call the &lt;code&gt;.focus()&lt;/code&gt; method on one &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, it changes the properties of others and the &lt;code&gt;activeElement&lt;/code&gt; property of the &lt;code&gt;document&lt;/code&gt; that owns them all.&lt;/p&gt;

&lt;p&gt;For an even more complex example, consider jQuery objects and their seemingly-simple API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fadeIn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activateWidget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is &lt;a href="https://stackoverflow.com/questions/7475336/how-does-jquery-chaining-work" rel="noopener noreferrer"&gt;a &lt;em&gt;great deal&lt;/em&gt; going on under the hood there&lt;/a&gt;. But because jQuery is doing those object operations under the hood, it turns into a wonderful API that has stood the test of time.&lt;/p&gt;

&lt;p&gt;Anyway, I’m not a good enough programmer to really understand or work on level 4 yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what?
&lt;/h2&gt;

&lt;p&gt;My first draft originally tried to tie this all into a lesson for readers, but I suspect this post is too confusing for beginners and also too mundane for advanced devs, so I’m publishing it &lt;strong&gt;for me&lt;/strong&gt;. As a treat.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;No, I will not explain what I mean. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>javascript</category>
      <category>oop</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Succeed in Open Source Without Really Trying (Really)</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Wed, 10 May 2023 01:29:00 +0000</pubDate>
      <link>https://dev.to/tigt/how-to-succeed-in-open-source-without-really-trying-really-55pj</link>
      <guid>https://dev.to/tigt/how-to-succeed-in-open-source-without-really-trying-really-55pj</guid>
      <description>&lt;p&gt;≥8 years ago, I wrote about &lt;a href="https://codepen.io/tigt/post/optimizing-svgs-in-data-uris" rel="noopener noreferrer"&gt;an extremely niche improvement to a very specific use of SVGs.&lt;/a&gt; It got enough positive feedback that I turned that knowledge into an NPM package: &lt;a href="https://www.npmjs.com/package/mini-svg-data-uri" rel="noopener noreferrer"&gt;&lt;code&gt;mini-svg-data-uri&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, it’s both one of the &lt;strong&gt;most&lt;/strong&gt; and &lt;strong&gt;least important&lt;/strong&gt; web dev things I’ve ever done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Somehow, it’s important
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;As of this writing, NPM says it gets ≈1.7 million downloads/week. That also means NPM foisted 2FA onto my account that I almost never use. (If you compare the dates when the repo accepted a PR to the NPM updates, you’ll probably see &lt;em&gt;exactly&lt;/em&gt; when they did that.) &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://snyk.io/advisor/npm-package/mini-svg-data-uri" rel="noopener noreferrer"&gt;Snyk rates it “Influential”&lt;/a&gt;, which means it’s in the top 5% of all NPM packages actually added to a &lt;code&gt;package.json&lt;/code&gt;, not just a transitive dependency.&lt;/li&gt;
&lt;li&gt;I know with some certainty that &lt;em&gt;lots&lt;/em&gt; of people &lt;a href="https://npm.runkit.com/mini-svg-data-uri" rel="noopener noreferrer"&gt;use &lt;code&gt;mini-svg-data-uri&lt;/code&gt; solely on RunKit&lt;/a&gt;, so both Snyk and NPM don’t see a huge chunk of users. (Never underestimate how meaningful a “try online” feature is.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normally, I’d be super smug about this and start being insufferable in developer conversations, but…&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s also unimportant and ridiculous
&lt;/h2&gt;

&lt;p&gt;Snyk and NPM both have algorithms to rate packages for quality and maintenance, but in this case they should consider letting me submit a rating of “lol i dunno”.&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;It’s not &lt;em&gt;that&lt;/em&gt; effective.
&lt;/dt&gt;
&lt;dd&gt;The README points out it optimizes by as much as, uh, 20%. And unlike the README’s example, most websites don’t consist entirely of SVG &lt;code&gt;data:&lt;/code&gt; URIs.
&lt;dd&gt;Unless you’re using &lt;code&gt;mini-svg-data-uri&lt;/code&gt; in an offline build tool, it’s almost certainly not worth the download of its JS. (So &lt;a href="https://www.npmjs.com/browse/depended/mini-svg-data-uri" rel="noopener noreferrer"&gt;all of you &lt;code&gt;import&lt;/code&gt;ing it&lt;/a&gt; to use on the client-side… are you sure you want to do that?)

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;It has no tests.
&lt;/dt&gt;
&lt;dd&gt;I don’t remember where &lt;a href="https://github.com/tigt/mini-svg-data-uri/blob/master/index.test-d.ts" rel="noopener noreferrer"&gt;its &lt;code&gt;index.test-d.ts&lt;/code&gt;&lt;/a&gt; came from, but it’s certainly not clear what it accomplishes.

&lt;/dd&gt;
&lt;dt&gt;It rarely updates.
&lt;/dt&gt;
&lt;dd&gt;Snyk penalizes this, but honestly it’s kind of a feature. It rarely &lt;em&gt;needs&lt;/em&gt; to update.

&lt;/dd&gt;
&lt;dt&gt;It’s written in like, ES3½.
&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://github.com/tigt/mini-svg-data-uri/pull/5" rel="noopener noreferrer"&gt;Folks wanted to use it untranspiled&lt;/a&gt;, or something? In principle &lt;strong&gt;that’s terrible&lt;/strong&gt; and Alex Russell will appear in your mirrors at night chanting “who made this mess”, but &lt;a href="https://unpkg.com/browse/mini-svg-data-uri@1.4.4/" rel="noopener noreferrer"&gt;in practice it’s ≈3.5kB unminified&lt;/a&gt;.

&lt;/dd&gt;
&lt;dt&gt;I am objectively kind of a bad maintainer.
&lt;/dt&gt;
&lt;dd&gt;I automated nothing, which means I must relearn how to &lt;code&gt;npm publish&lt;/code&gt; each update, which means folks’ contributions can get needlessly delayed.
&lt;dd&gt;If you look through the issues, you’ll see me say “I don’t know” a lot. Humility’s one thing, but I wouldn’t begrudge anyone not trusting me to do things right.
&lt;dd&gt;There’s no code of conduct or contributing guidelines, and it still defaults to &lt;code&gt;master&lt;/code&gt;. (Somehow I’ve innovated the cognitive dissonance of believing those things are important, and yet also &lt;em&gt;too&lt;/em&gt; important for my rinkydink package.)

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;The features it accrued are niche.
&lt;/dt&gt;
&lt;dd&gt;Its “types”: a function that accepts a string and returns a string. &lt;a href="https://github.com/tigt/mini-svg-data-uri/pull/15#issuecomment-624412182" rel="noopener noreferrer"&gt;I assume the TypeScript was for autocomplete or purism or something&lt;/a&gt;, because it sure doesn’t constrain much.

&lt;dd&gt;
&lt;a href="https://simonewebdesign.it/1req/#the-favicon" rel="noopener noreferrer"&gt;Its CLI is from a dev who whipped it up for themselves then contributed back on a whim.&lt;/a&gt; Sure, without a real shell arguments parser, it might break or be needlessly slow. But if you can identify those problems, you probably already know how to fix them with your own &lt;code&gt;#!/usr/bin/env node&lt;/code&gt;.

&lt;dd&gt;
&lt;a href="https://github.com/tigt/mini-svg-data-uri/issues/9" rel="noopener noreferrer"&gt;It sprouted a &lt;code&gt;.toSrcset()&lt;/code&gt; method&lt;/a&gt; because &lt;code&gt;srcset&lt;/code&gt; uses spaces as part of its syntax, so you need &lt;code&gt;%20&lt;/code&gt; escapes. &lt;a href="https://github.com/tigt/mini-svg-data-uri/blob/00dc78c8f77eb7b47299a9e3d564749105810c9c/index.js#L51-L53" rel="noopener noreferrer"&gt;It accomplishes this with zero cleverness whatsoever&lt;/a&gt;, so I suspect most of the feature’s worth is from how it emphasizes the &lt;code&gt;srcset&lt;/code&gt; pitfall in the README.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;To be clear, I appreciate that those devs took the time to add features that scratched their own itch. I know there’s something to be said for refusing features to keep software lean and mean. But even though I don’t use those contributed features myself, it takes up a whopping 10kB uncompressed on disk. So, like, refusing those features would have ruined a lot of peoples’ days for no real reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody &lt;em&gt;really&lt;/em&gt; gives that much of a shit about it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The code is handy, but if it became even slightly annoying to use — say if I added whatever makes &lt;code&gt;npm fund&lt;/code&gt; do its thing — people would just fork or vendor it.&lt;/li&gt;
&lt;li&gt;It’s in a sweet spot of usefulness with almost no leverage, so it chugs along without any temptation for me to do anything spicy and/or money-related with it.&lt;/li&gt;
&lt;li&gt;Putting its stats on my résumé would be impressive, until anyone looked closer.&lt;/li&gt;
&lt;li&gt;It’s somehow avoided getting slapped with &lt;a href="https://overreacted.io/npm-audit-broken-by-design/" rel="noopener noreferrer"&gt;the “regular expression denial of service” bullshit that everyone loves&lt;/a&gt;, despite having regular expressions I authored in 3 minutes.&lt;/li&gt;
&lt;li&gt;I haven’t even had people try to grift me over it, which I assume is automated based on package stats nowadays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yet, it’s undeniably popular. I guess it ticks all the boxes for “is this worth using?” — it handles enough annoying details that you’d rather not yourself, it has no dependencies, and you can read the entire source code almost by accident. &lt;a href="https://www.johndcook.com/blog/2012/05/25/unix-doesnt-follow-the-unix-philosophy/" rel="noopener noreferrer"&gt;The UNIX philosophy is a scam&lt;/a&gt;, but it’s a nice racket if you can get it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s dying, but not how’d you think
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;svg-mini-data-uri&lt;/code&gt; will probably become obsolete as evergreen browsers completely take over, since their parsing is loose enough that &lt;code&gt;"data:image/svg+xml," + str.replace(/#/g, '%23')&lt;/code&gt; gets you 80% there.&lt;/p&gt;

&lt;p&gt;Honestly, it’s kind of nice that it’ll die with the problem it helped solve. It was there when needed, but not a moment longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what?
&lt;/h2&gt;

&lt;p&gt;Maybe the system worked this time? I wrote code for me, then shared it, and now tons of people benefit without me suffering the &lt;a href="https://nolanlawson.com/2017/03/05/what-it-feels-like-to-be-an-open-source-maintainer/" rel="noopener noreferrer"&gt;usual problems of being a popular open-source maintainer&lt;/a&gt;. The software does what it says, boringly, and is small enough that it doesn’t make developers or users suffer even when it’s not used “right”.&lt;/p&gt;

&lt;p&gt;And it happened without me writing tests, doing any outreach, being good at code, or even &lt;a href="https://github.com/tigt/mini-svg-data-uri/issues/9#issuecomment-503397923" rel="noopener noreferrer"&gt;using &lt;code&gt;this&lt;/code&gt; correctly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When I put it that way, it sounds like a pretty good trick.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>svg</category>
      <category>javascript</category>
      <category>npm</category>
    </item>
    <item>
      <title>Marking up colors, revisited with GitHub’s color swatches</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Thu, 05 Jan 2023 23:21:14 +0000</pubDate>
      <link>https://dev.to/tigt/marking-up-colors-revisited-with-githubs-color-swatches-42eo</link>
      <guid>https://dev.to/tigt/marking-up-colors-revisited-with-githubs-color-swatches-42eo</guid>
      <description>&lt;p&gt;&lt;a href="https://codepen.io/tigt/post/semantic-html-for-colors" rel="noopener noreferrer"&gt;I wrote about semantic HTML for colors 5 years ago&lt;/a&gt;, for when a color’s appearance is important content, not just style:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design systems’ available color palettes&lt;/li&gt;
&lt;li&gt;Choosing what color of clothing you would like to buy&lt;/li&gt;
&lt;li&gt;Preview swatches for sites about paint&lt;/li&gt;
&lt;li&gt;Probably lots more — colors are pretty important!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My original post suggested a labeled &lt;code&gt;&amp;lt;input type=color readonly&amp;gt;&lt;/code&gt;, but since then I’ve thought about how that falls short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can’t display &lt;a href="https://matthiasott.com/notes/world-wide-gamut-web" rel="noopener noreferrer"&gt;wide-gamut colors&lt;/a&gt;, or &lt;a href="https://github.com/whatwg/html/issues/3400" rel="noopener noreferrer"&gt;managed color&lt;/a&gt; in general&lt;/li&gt;
&lt;li&gt;Can’t display semitransparency, gradients, patterns, or other fancy swatches&lt;/li&gt;
&lt;li&gt;Can’t nest inside &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, another form field’s &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;readonly&lt;/code&gt; still isn’t allowed on &lt;code&gt;&amp;lt;input type=color&amp;gt;&lt;/code&gt;… and &lt;a href="https://a11ysupport.io/tech/html/input(type-color)_element" rel="noopener noreferrer"&gt;the color picker/inspection popup isn’t accessible yet anyway&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, my original conclusion’s &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; is probably a better idea. (I’d use my Edit button on that post… &lt;a href="https://blog.codepen.io/2020/08/28/posts-sunset/" rel="noopener noreferrer"&gt;&lt;em&gt;if I had one&lt;/em&gt;&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Now, those issues are important, but the real reason I’m writing this is much pettier: I used Inspect Element on github.com and got mad about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub’s hex code swatches
&lt;/h2&gt;

&lt;p&gt;GitHub has a feature where if you type a 6-digit hex code in Markdown files/comments/whatever, it displays a color dot of that hex in RRGGBB format:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fug94fqv8oej3vmaonhwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fug94fqv8oej3vmaonhwj.png" alt="Screenshot of GitHub’s rendering the hex code #aa4400. The usual gray box around the &amp;lt;code&amp;gt; element extends to the right to hold a small circle of a dusky brick red." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, GitHub’s HTML output if you type &lt;code&gt;`#aa4400`&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;#aa4400
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ml-1 d-inline-block border circle color-border-subtle"&lt;/span&gt;
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background-color: #aa4400; height: 8px; width: 8px;"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has problems:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Not exposed accessibly
&lt;/dt&gt;
&lt;dd&gt;Forget text alternatives, right now it’s completely nonexistent to assistive technology

&lt;/dd&gt;
&lt;dt&gt;Too easily overridden
&lt;/dt&gt;
&lt;dd&gt;By Dark/Light Mode browser extensions, &lt;a href="https://css-tricks.com/a-complete-guide-to-dark-mode-on-the-web/" rel="noopener noreferrer"&gt;builtin Dark Mode browser support&lt;/a&gt;, user styles, etc.

&lt;/dd&gt;
&lt;dt&gt;Invisible under forced colors
&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors" rel="noopener noreferrer"&gt;&lt;code&gt;forced-colors&lt;/code&gt;&lt;/a&gt; is the standardized name for features like Windows High-Contrast Mode, which turn off &lt;code&gt;background-color&lt;/code&gt; altogether

&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;We can fix most of those problems with inline SVG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;#aa4400
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt;
    &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#aa4400"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ml-1 border circle color-border-subtle"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on the above markup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I haven’t exposed it accessibly yet, because that turned out to be a whole section’s worth of considerations
&lt;/li&gt;
&lt;li&gt;No longer needs the &lt;code&gt;d-inline-block&lt;/code&gt; class (&lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; is &lt;code&gt;inline-block&lt;/code&gt; by default)&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;style&lt;/code&gt; attribute lets it play nice with strict Content-Security Policies&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  But why stop there when we could be perfectionist
&lt;/h3&gt;

&lt;p&gt;GitHub’s current implementation also has what I presume is a bug, where it looks silly inside larger text:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfq7n57gotfv7gndxjif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfq7n57gotfv7gndxjif.png" alt="Almost like the earlier screenshot, but while the gray box scales proportionally around the text, the color dot stays 8×8 pixels and awkwardly sits near the bottom — it honestly resembles a colorful punctuation mark." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Inside an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, for example.&lt;/p&gt;



&lt;p&gt;SVG’s &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes can use &lt;code&gt;em&lt;/code&gt;s to scale the dot with the font size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;#aa4400
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;".667em"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;".667em"&lt;/span&gt;
    &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#aa4400"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ml-1 border circle color-border-subtle"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heck, you could even replace &lt;a href="https://primer.style/css/utilities" rel="noopener noreferrer"&gt;GitHub Primer’s utility classes&lt;/a&gt; with SVG’s builtins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;#aa4400
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;".667em"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;".667em"&lt;/span&gt;
    &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#aa4400"&lt;/span&gt;
    &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"-9 -8 17 16"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"var(--border-subtle)"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;circle&lt;/span&gt; &lt;span class="na"&gt;radius=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub probably won’t, since once you choose utility classes for something you need to use them &lt;em&gt;everywhere&lt;/em&gt; or their core performance promise falls apart&lt;sup id="fnref1"&gt;1&lt;/sup&gt;, but maybe &lt;strong&gt;&lt;em&gt;you&lt;/em&gt;&lt;/strong&gt; might?&lt;/p&gt;

&lt;h3&gt;
  
  
  By the way, GitLab does this too
&lt;/h3&gt;

&lt;p&gt;And as usual, GitLab implemented the feature more thoughtfully than GitHub did&lt;sup id="fnref2"&gt;2&lt;/sup&gt;. They support &lt;a href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#colors" rel="noopener noreferrer"&gt;more color syntaxes &lt;em&gt;and&lt;/em&gt; transparency&lt;/a&gt;, it &lt;a href="https://gitlab.com/gitlab-org/gitlab-foss/-/blob/2b5bd61ab9de9b203ff42f23051770585805d5e3/app/assets/stylesheets/framework/gfm.scss#L30-35" rel="noopener noreferrer"&gt;doesn’t have the &lt;code&gt;font-size&lt;/code&gt; scaling bug&lt;/a&gt;, and the markup is refreshingly simple:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;HSLA(540,70%,50%,0.3)
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"gfm-color_chip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background-color: HSLA(540,70%,50%,0.3);"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It still breaks with forced colors and dark mode, so maybe the GitLab peeps would be interested in this article anyway. (&lt;a href="https://codepen.io/tigt/pen/QWBEWpo?editors=1100" rel="noopener noreferrer"&gt;I also refactored theirs to be shorter, simpler, and with one less &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;&lt;/a&gt;, because I’m insufferable.)&lt;/p&gt;

&lt;p&gt;Speaking of accessibility, there’s something I haven’t addressed…&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibly exposing the color
&lt;/h2&gt;

&lt;p&gt;So, that &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;. What’s its &lt;code&gt;role&lt;/code&gt;? Its accessible name? Should it be announced at all?&lt;/p&gt;

&lt;p&gt;For this specific case, it has a text equivalent right alongside it, and GitHub’s audience is tech-savvier than usual — I can imagine that GitHub may have decided announcing the color dot separately was superfluous.&lt;/p&gt;

&lt;p&gt;…but I disagree. Displaying the color adds information for sighted users, hence why the feature exists at all; for the ideal of information parity across disability boundaries, we should try to be equitable.&lt;/p&gt;

&lt;p&gt;Since the hex code is already there, a proper text alternative would be the information equivalent of displaying the color dot. Maybe a color name via something like &lt;a href="https://en.wikipedia.org/wiki/ISCC%E2%80%93NBS_system" rel="noopener noreferrer"&gt;the ISCC–NBS system&lt;/a&gt;?&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Trying &lt;a href="https://hub.jhu.edu/2021/08/17/blind-people-understand-color/" rel="noopener noreferrer"&gt;to &lt;em&gt;explain&lt;/em&gt; colors to people who can’t see them&lt;/a&gt; isn’t the goal — but &lt;a href="https://pubs.acs.org/doi/10.1021/acs.jchemed.1c00664" rel="noopener noreferrer"&gt;naming a color consistently can be quite useful by itself&lt;/a&gt;. Not to mention, &lt;a href="https://austinseraphin.net/2010/06/12/my-first-week-with-the-iphone/" rel="noopener noreferrer"&gt;blind people may want to know for its own sake&lt;/a&gt;!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;#aa4400
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"img"&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"_Z5jdHi6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"_Z5jdHi6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Color: “firebrick”&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    …
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I’m not 100% sure you need the &lt;code&gt;role&lt;/code&gt; — the state of the art with inline SVG accessibility has changed a lot recently, &lt;a href="https://stackoverflow.com/questions/49197524/what-is-the-default-aria-role-for-an-svg" rel="noopener noreferrer"&gt;as this StackOverflow question, answer, and interspersed comments show&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; shows up on hover, which helps colorblind folks disambiguate when they need to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I’m using &lt;code&gt;aria-labelledby&lt;/code&gt; because &lt;a href="https://heydonworks.com/article/aria-label-is-a-xenophobe/" rel="noopener noreferrer"&gt;&lt;code&gt;aria-label&lt;/code&gt; still isn’t machine translated.&lt;/a&gt; (Then again, I’m not sure Google/Bing translate inline SVG yet either.)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  I don’t have a real conclusion but here’s a fun fact
&lt;/h2&gt;

&lt;p&gt;When I first started learning about color accessibility, the explanations of what each colorblindness variety sees instead of the “real” colors bothered me — how could they be so sure? While normally I’m happy to leave &lt;a href="https://en.wikipedia.org/wiki/Qualia" rel="noopener noreferrer"&gt;“do you and I see the same red” to the philosophers&lt;/a&gt;, this was too important for a hand-wavy justification to satisfy me.&lt;/p&gt;

&lt;p&gt;Days later, lost and confused from scientific papers about human retinas, the occipital lobe, signal processing, and other complexities around one of the first skills babies learn, I found an answer that was so prosaic it made me laugh: some people are colorblind in only &lt;em&gt;&lt;strong&gt;one&lt;/strong&gt;&lt;/em&gt; eye.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Any feature you have to “earn” by adhering to a technology and you shall have no technologies before it is one I distrust. Migrating &lt;em&gt;away&lt;/em&gt; from something like Tailwind is suspiciously difficult. (This is my footnote and I can be crankety if I want to.) ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Using GitHub Actions after working in GitLab CI is like disassembling a perfectly good sandwich and making it into sushi. It all eventually goes where you need, but nobody’s happy. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;There’s a bunch of color naming systems and &lt;a href="https://www.npmjs.com/search?q=color%20names" rel="noopener noreferrer"&gt;even more color naming NPM packages&lt;/a&gt;, and I’m not qualified to recommend any of them. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>html</category>
      <category>svg</category>
    </item>
    <item>
      <title>So what?</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Wed, 08 Jun 2022 14:20:26 +0000</pubDate>
      <link>https://dev.to/tigt/so-what-c8j</link>
      <guid>https://dev.to/tigt/so-what-c8j</guid>
      <description>&lt;p&gt;Last time, I promised to write about “getting the benefits that SPAs enjoy, without suffering the consequences they extremely don’t enjoy”. And then &lt;a href="https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-spas/" rel="noopener noreferrer"&gt;Nolan Lawson wrote basically that&lt;/a&gt;, and then &lt;a href="https://nolanlawson.com/2022/05/25/more-thoughts-on-spas/" rel="noopener noreferrer"&gt;the madlad did it &lt;em&gt;again&lt;/em&gt;&lt;/a&gt;. He included almost everything I would’ve:&lt;/p&gt;

&lt;dl&gt;

&lt;dt&gt;MPA pageloads are surprisingly tough to beat nowadays
&lt;/dt&gt;
&lt;dd&gt;Paint holding, streaming HTML, cross-page code caching, back/forward caching, etc.

&lt;/dd&gt;
&lt;dt&gt;Service Worker rendering
&lt;/dt&gt;
&lt;dd&gt;Also see &lt;a href="https://alistapart.com/article/now-thats-what-i-call-service-worker/" rel="noopener noreferrer"&gt;Jeremy Wagner on why offline-first MPAs are cool&lt;/a&gt;

&lt;/dd&gt;
&lt;dt&gt;In theory, MPA page transitions are Real Soon Now
&lt;/dt&gt;
&lt;dd&gt;In practice, Kroger.com had none and our native app barely had any, so I didn’t care

&lt;/dd&gt;
&lt;dt&gt;And his main point:
&lt;/dt&gt;
&lt;dd&gt;
&lt;blockquote&gt;If the only reason you’re using an SPA is because “it makes navigations faster,” then maybe it’s time to re-evaluate that.&lt;/blockquote&gt;

&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;&lt;small&gt;(I don’t think he talked about &lt;a href="https://chriscoyier.net/2022/05/04/it-doesnt-much-matter-how-cdny-your-jamstack-site-is-if-everything-important-happens-from-a-single-origin-server-edge-functions-are-probably-part-of-the-solution/" rel="noopener noreferrer"&gt;how edge rendering and MPAs are good buds&lt;/a&gt;, but I mentioned it so &lt;a href="https://www.vswong.com/articles/optimising-webapps-for-high-read-density/" rel="noopener noreferrer"&gt;here’s ticking that box&lt;/a&gt;.)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Since Nolan said what I would’ve (in less words!), I’ll cut to the chase: &lt;strong&gt;did my opinions in this series make a meaningfully fast site?&lt;/strong&gt; This is the part where I put my money &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#the-backstory-developer-bitten-by-a-radioactive-webpagetest"&gt;where my mouth was&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Proving that speed mattered wasn’t enough: we also had to convince people &lt;em&gt;emotionally&lt;/em&gt;. To &lt;em&gt;show&lt;/em&gt; everyone, &lt;strong&gt;god dammit&lt;/strong&gt;, how much better our site would be if it were fast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The best way to get humans to feel something is to have them experience it. Is our website painful on the phones we sell? Time to inflict some pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo
&lt;/h2&gt;

&lt;p&gt;I planned to demonstrate the importance of speed at our monthly product meeting. It went a little something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Buy enough &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#some-sort-of-higherlevel-goal"&gt;Poblano phones&lt;/a&gt; for attendees.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On those phones and a throttled connection, try using Kroger.com:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in&lt;/li&gt;
&lt;li&gt;Search for “eggs”&lt;/li&gt;
&lt;li&gt;Add some to cart&lt;/li&gt;
&lt;li&gt;Try to check out&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Repeat those steps on the demo.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Note how performance is the bedrock feature: without it, no other features exist.&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmq03vwvpdoejp981xxq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmq03vwvpdoejp981xxq.jpg" alt="A MacBook Pro with “Who was Jake Wary?” scrawled on it, plugged into a dock for power and ethernet. Fanned out nearby are cheap Android phones not connected to anything, along with an Alcatel flip phone." width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Near the laptop with the horrible pun are 10 of the original 15 demo phones. (The KaiOS flip phone helped stop me from overspecializing for Chrome or the Poblano VLE5 specs.)&lt;/p&gt;



&lt;p&gt;A nice thing about targeting wimpy phones is that the demo hardware cost me relatively little. Each Poblano was ≈$35, and a sale at the time knocked some down to $25.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fast was it?
&lt;/h2&gt;

&lt;p&gt;Sadly, I can’t give you a demo, so this video will have to suffice:&lt;/p&gt;

&lt;p&gt;The browser UI differs because each video was recorded at different times. (Also, guess walmart.com’s framework.)


  Text description of video
  &lt;p&gt;Me racing to get the demo, kroger.com, Kroger’s native app, amazon.com, and walmart.com all to the start of checkout as fast as possible. In order of how long each took:

&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The demo, in 20 seconds
&lt;/li&gt;
&lt;li&gt;amazon.com, in 59 seconds
&lt;/li&gt;
&lt;li&gt;The Kroger native app, in 1 minute 21 seconds
&lt;/li&gt;
&lt;li&gt;walmart.com, in 2 minutes 14 seconds
&lt;/li&gt;
&lt;li&gt;kroger.com, in 3 minutes 44 seconds
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to improve dev.to’s video accessibility, consider voting for &lt;a href="https://github.com/forem/forem/discussions/16853" rel="noopener noreferrer"&gt;my feature request for the &lt;code&gt;&amp;lt;track&amp;gt;&lt;/code&gt; element&lt;/a&gt;.

&lt;/p&gt;


&lt;/p&gt;



&lt;p&gt;For a bit, our CDN contact got it semi-public on the real Internet. I was beyond excited to see this in &lt;a href="https://twitter.com/AmeliasBrain" rel="noopener noreferrer"&gt;@AmeliaBR&lt;/a&gt;’s Firefox devtools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferxri2q1xf2r8wkvm8qm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferxri2q1xf2r8wkvm8qm.png" alt="A Gantt chart showing Request Timing: Blocked 63 ms, DNS Resolution 19 ms, Connecting 17 ms, TLS Setup 24 ms, Sending 0 ms, Waiting 166 ms, and Receiving 1 ms. Summarized as Started at 63 ms, Downloaded at 293 ms." width="792" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s Cincinnati, Ohio → Edmonton, Canada. &lt;a href="https://web.dev/rail/" rel="noopener noreferrer"&gt;293 milliseconds ain’t bad for a network response&lt;/a&gt;, but I was so happy because I knew we could get much faster…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;About &lt;a href="https://wondernetwork.com/pings/Edmonton/Cincinnati" rel="noopener noreferrer"&gt;50–100ms was from geographical distance&lt;/a&gt;, which can be improved by edge rendering/caching/etc.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://community.pivotal.io/s/article/Gorouter-buffering-behavior-for-HTTP-requests-and-responses?language=en_US" rel="noopener noreferrer"&gt;PCF’s gorouters have a 50ms delay.&lt;/a&gt; Luckily, we were dropping PCF.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rachelbythebay.com/w/2020/10/14/lag/" rel="noopener noreferrer"&gt;40ms from Nagle’s algorithm&lt;/a&gt;, maybe even 80ms from both Node.js and the reverse proxy. &lt;a href="https://www.extrahop.com/company/blog/2016/tcp-nodelay-nagle-quickack-best-practices/" rel="noopener noreferrer"&gt;This is what &lt;code&gt;TCP_NODELAY&lt;/code&gt; is for.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tweaked gzip/brotli compression, like &lt;a href="https://www.euccas.me/zlib/#zlib_io_buffer" rel="noopener noreferrer"&gt;their buffer sizes&lt;/a&gt; and &lt;a href="https://zlib.net/zlib_how.html#:~:text=to%20control%20data%20latency%20on%20a%20link%20with%20compressed%20data" rel="noopener noreferrer"&gt;flushing behavior&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hpbn.co/transport-layer-security-tls/#optimizing-for-tls" rel="noopener noreferrer"&gt;Lower-latency HTTPS configuration&lt;/a&gt;, such as smaller TLS record sizes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s say that averages out to 200ms in the real world. Based on the numbers in the first post, that’s &lt;strong&gt;&lt;em&gt;$40 million/year based on kroger.com’s 1.2 TTFB today&lt;/em&gt;&lt;/strong&gt;. Or, &lt;a href="https://ir.kroger.com/CorporateProfile/press-releases/press-release/2021/Kroger-Delivers-Strong-Fourth-Quarter-and-Fiscal-Year-2020-Results/default.aspx" rel="noopener noreferrer"&gt;~5% of company profit at the time&lt;/a&gt;. (The actual number would probably be higher. With a difference this large, latency→revenue stops being linear.)&lt;/p&gt;

&lt;h2&gt;
  
  
  So… how’d it go?
&lt;/h2&gt;

&lt;p&gt;Or &lt;a href="https://twitter.com/grigs/status/1509307824924336129" rel="noopener noreferrer"&gt;as Jason Grigsby put it&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The burning questions are related to how it performed and what the organization thought about it? How much was adopted? Etc.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What did the organization think of it?
&lt;/h3&gt;

&lt;p&gt;The immediate reaction exceeded even my most indulgent expectations. Only the sternest Dad Voice in the room could get enough quiet to finish the presentation. Important people stood up to say they’d like to see more bottom-up initiative like it. VIPs who didn’t attend requested demos. Even some developers who disagreed with me on React and web performance admitted they were intrigued.&lt;/p&gt;

&lt;p&gt;Which was nice, but kroger.com was still butt-slow. As far as how to &lt;em&gt;learn&lt;/em&gt; anything from the demo, I think these were the options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Adapt new principles to existing code&lt;/li&gt;
&lt;li&gt;Rewrite (incremental or not)&lt;/li&gt;
&lt;li&gt;Separate MVP&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Adapt new principles to kroger.com’s existing code?
&lt;/h4&gt;

&lt;p&gt;Naturally, folks asked how to get our current React SSR architecture to be fast like the demo. And that’s fine! Why &lt;strong&gt;&lt;em&gt;not&lt;/em&gt;&lt;/strong&gt; React? Why not compromise and improve the existing site?&lt;/p&gt;

&lt;p&gt;We tried it. Developers toiled in the Webpack mines for smaller bundles. We dropped IE11 to polyfill less. We changed the footer to static HTML. After months of effort, we shrank our JS bundle by ≈10%.&lt;/p&gt;

&lt;p&gt;One month later, we were back where we started.&lt;/p&gt;

&lt;p&gt;Does that mean fast websites are too hard in React? C’mon, that’s a clickbait question impossible to answer. &lt;strong&gt;But it &lt;em&gt;was&lt;/em&gt; evidence that we as a company couldn’t handle ongoing development in a React SPA architecture without constant site speed casualties.&lt;/strong&gt; Maybe it was for management reasons, or education reasons, but after this cycle repeated a few times, a fair conclusion was we couldn’t hack it. When every new feature adds client-side JS, it felt like we were set up to lose before we even started. (Try telling a business that each new feature must replace an existing one. See how far you get.)&lt;/p&gt;

&lt;p&gt;At some point, I was asked to write a cost/benefit analysis for the MPA architecture that made the demo fast, but in React. It’s long enough I can’t repeat it here, so instead I’ll do a Classic Internet Move™: &lt;em&gt;gloss a nuanced topic into controversial points.&lt;/em&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Reasons not to use React for Multi-Page Apps
&lt;/h5&gt;

&lt;dl&gt;
&lt;dt&gt;React server-renders HTML slower than many other frameworks/languages
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;If you’re server rendering much more frequently, even small differences add up. &lt;a href="https://github.com/marko-js/isomorphic-ui-benchmarks" rel="noopener noreferrer"&gt;And the differences aren’t that small.&lt;/a&gt;

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;React is kind of bad at page loads
&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;&lt;a href="https://bundlephobia.com/scan-results?packages=react,react-dom" rel="noopener noreferrer"&gt;&lt;code&gt;react&lt;/code&gt; + &lt;code&gt;react-dom&lt;/code&gt; are bigger&lt;/a&gt; than many frameworks, and its growth trendline is disheartening.
&lt;/p&gt;
&lt;dd&gt;
&lt;p&gt;In theory, React pages can be fast. &lt;a href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/" rel="noopener noreferrer"&gt;In practice, they rarely are.&lt;/a&gt;
&lt;/p&gt;
&lt;dd&gt;
&lt;p&gt;&lt;a href="https://svelte.dev/blog/virtual-dom-is-pure-overhead#it-s-not-just-the-diffing-though" rel="noopener noreferrer"&gt;VDOM is not the architecture you’d design if you wanted fast loads.&lt;/a&gt;
&lt;/p&gt;
&lt;dd&gt;
&lt;p&gt;Its rehydration annoys users, does lots of work at the worst possible time, and is fragile and hard to reason about. Do you want those risks on &lt;em&gt;each page?&lt;/em&gt;
  ℹ️ Okay, I feel like I have to back this one up, at least.
  &lt;blockquote&gt;
&lt;p&gt;Performance metrics collected from real websites using SSR rehydration indicate its use should be heavily discouraged. Ultimately, the reason comes down to User Experience: it's extremely easy to end up leaving users in an “uncanny valley”.

&lt;/p&gt;
&lt;p&gt;— &lt;a href="https://web.dev/rendering-on-the-web/#a-rehydration-problem:-one-app-for-the-price-of-two" rel="noopener noreferrer"&gt;Rendering on the Web § A Rehydration Problem: One App for the Price of Two&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Virtual DOM approach inflicts a lot of overhead at page load:

&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Render the entire component tree
&lt;/li&gt;
&lt;li&gt;Read back the existing DOM
&lt;/li&gt;
&lt;li&gt;Diff the two
&lt;/li&gt;
&lt;li&gt;Render the reconciled component tree
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s a lot of unnecessary work if you’re going to show something mostly-identical to the initial &lt;code&gt;text/html&lt;/code&gt; response!

&lt;/p&gt;
&lt;p&gt;Forget the performance for a second. Even rehydrating &lt;em&gt;correctly&lt;/em&gt; in React is tricky, so using it for an MPA risks breakage on every page:

&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://farmdev.com/thoughts/107/why-server-side-rendering-in-react-is-so-hard/" rel="noopener noreferrer"&gt;Why Server Side Rendering In React Is So Hard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://joshwcomeau.com/react/the-perils-of-rehydration/" rel="noopener noreferrer"&gt;The Perils of Rehydration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.jakoblind.no/case-study-ssr-react/" rel="noopener noreferrer"&gt;Case study of SSR with React in a large e-commerce app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.logrocket.com/fixing-gatsbys-rehydration-issue/" rel="noopener noreferrer"&gt;Fixing Gatsby’s rehydration issue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/gatsbyjs/gatsby/issues/17914" rel="noopener noreferrer"&gt;gatsbyjs#17914: [Discussion] Gatsby, React &amp;amp; Hydration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react/issues?q=is%3Aissue+label%3A" rel="noopener noreferrer"&gt;React bugs for “Server Rendering”&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No, really, skim those links. The nature of their problems is more important than the specifics.

&lt;/p&gt;

 

&lt;/p&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;React fights the multi-page mental model
&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;It prefers JS properties to HTML attributes (you know, the &lt;code&gt;class&lt;/code&gt; vs. &lt;code&gt;className&lt;/code&gt; thing). That’s not a dealbreaker, but it’s symptomatic.
&lt;/p&gt;
&lt;dd&gt;&lt;p&gt;Server-side React and its ecosystem strive to pretend they’re in a browser. Differences between server and browser renders are considered isomorphic failures that should be fixed.

&lt;/p&gt;&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;React promises upcoming ways to address these problems, but testing, benching, and speculating on them would be a &lt;em&gt;whole other post&lt;/em&gt;. (They also extremely didn’t exist two years ago.) I’m not thrilled about how React’s upcoming streaming and partial hydration seem to be implemented — I should test for due diligence, but a separate HTTP connection for a not-quite-JSON stream doesn’t seem like it would play nice during page load. &lt;/p&gt;

&lt;p&gt;Taking it back to my goals, does Facebook even use React for its rural/low-spec/poorly-connected customers? There is one data point of the almost-no-JS &lt;a href="https://mbasic.facebook.com/" rel="noopener noreferrer"&gt;mbasic.facebook.com&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rewrite kroger.com, incrementally or not?
&lt;/h4&gt;

&lt;p&gt;Software rewrites are the Forever Joke. Developers say &lt;em&gt;this&lt;/em&gt; will be the last rewrite, because finally we know how to do it &lt;em&gt;right&lt;/em&gt;. Businesses, meanwhile, knowingly estimate how long each codebase will last based on how wrong the developers were in the past.&lt;/p&gt;

&lt;p&gt;Therefore, the natural question: should our next inevitable rewrite be Marko?&lt;/p&gt;

&lt;p&gt;I was able to pitch my approach vs. another for internal R&amp;amp;D. I can’t publish specifics, but I did make this inscrutable poster for it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdml7v67dhpgkuzkxfh5e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdml7v67dhpgkuzkxfh5e.png" alt="The Greatest Fight in History! Jared ‘The’ Fox’s Big Bean BBQ &amp;amp; BAR “No, I’m not giving you a quote for that” Presents: Rumpus in the Grumpus Proof-of-Concept Speed Showdown! Marko f.k.a. ‘Clipper’ vs. ‘Bridge’ a.k.a. poc-1! Monday 9AM, Direct from ringside at Atrium Two secret basement fight club, 221 East 4th St Cincinnati, Free Country USA. All seats reserved starting $20/GB. Ringside is where it’s at; No VoDs; No Microsoft Teams recording." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And because I’m an incorrigible web developer, &lt;a href="https://codepen.io/tigt/pen/JjKeygR/b467e6cf52e52f27436cca19afdd629d" rel="noopener noreferrer"&gt;I made it with HTML &amp;amp; CSS&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;That bakeoff’s official conclusion: “performance is an application concern, not the platform’s fault”. It was decided to target Developer Experience™ for the long-term, not site speed.&lt;/p&gt;

&lt;p&gt;I was secretly relieved: &lt;strong&gt;how likely will a new architecture &lt;em&gt;actually&lt;/em&gt; be faster if it’s put through the same people, processes, and culture as the last architecture?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With the grand big-bang rewrite successfully avoided, we could instead try small incremental improvements — speed A/B tests. If successful, that’s reason enough to try further improvements, and if &lt;em&gt;those&lt;/em&gt; were successful…&lt;/p&gt;

&lt;p&gt;The simplest thing that could possibly work seemed to be streaming static asset &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; elements before the rest of the HTML. We’d rewrite the outer scaffolding HTML in Marko, then embed React into the dynamic parts of the page. Here’s a simplified example of what I mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;import {
  renderReactRoot,
  fetchDataDependencies
} from './react-app'

&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en-us"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;for&lt;/span&gt;&lt;span class="err"&gt;|{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt; &lt;span class="err"&gt;}|&lt;/span&gt; &lt;span class="na"&gt;of=&lt;/span&gt;&lt;span class="s"&gt;input.webpackStaticAssets&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;if&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;url.endsWith&lt;/span&gt;&lt;span class="err"&gt;('.&lt;/span&gt;&lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="err"&gt;')&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/if&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;if&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;url.endsWith&lt;/span&gt;&lt;span class="err"&gt;('.&lt;/span&gt;&lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="err"&gt;')&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/if&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/for&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;PageMetadata&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="na"&gt;input.request&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;await&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;fetchDataDependencies&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input.request&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input.response&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      $!{renderReactRoot(data)}
    &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;then&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/await&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This had a number of improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Browsers could download and parse our static assets while the server waited on dynamic data and React SSR.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since Marko only serializes components with &lt;code&gt;state&lt;/code&gt;, the outer HTML didn’t add to our JS bundle. (This had more impact than the above example suggests; our HTML scaffolding was more complicated because it was a Real Codebase.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If successful, we could rewrite components from the outside-in, shrinking the bundle with each step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Marko also paid for itself with more efficient SSR and smaller HTML output (quote stripping, tag omission, etc.), so we didn’t regress server metrics unless we wanted to.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This almost worked! But we were thwarted by our Redux code. Our Reducers ‘n’ Friends contained enough redirect/page metadata/analytics/business logic that assumed the entire page would be sent all at once, where any code could walk back up the DOM at its leisure and change previously-generated HTML… like the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We tried to get dev time to overcome this problem, since we’d have to make Redux stream-friendly in a React 18 world anyway. Unfortunately, &lt;a href="https://github.com/reduxjs/redux-toolkit/discussions/1820" rel="noopener noreferrer"&gt;Redux and its ecosystem weren’t designed with streaming in mind&lt;/a&gt;, so assigning enough dev time to overcome those obstacles was deemed “not product-led enough”.&lt;/p&gt;

&lt;h4&gt;
  
  
  Launch a separate, faster version of kroger.com?
&lt;/h4&gt;

&lt;p&gt;While the “make React do this” attempts and the Streaming A/B test were, you know, fine, they weren’t my favorite options. I favored launching a separate low-spec site with respectful redirects — let’s call it &lt;code&gt;https://kroger.but.fast/&lt;/code&gt;. I liked this approach because…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum time it took for real people to benefit from a significant speedup&lt;/li&gt;
&lt;li&gt;Helped with the culture paradox: your existing culture gave you the current site. Pushing a new approach through that culture will change your current culture or the result, and the likelihood of which depends on how many people it has to go through. A small team with its own goals can incubate its own culture to achieve those goals.&lt;/li&gt;
&lt;li&gt;If it’s a big enough success, it can run on its own results while accruing features, until the question “should we swap over?” becomes an obvious yes/no.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How much was adopted?
&lt;/h3&gt;

&lt;p&gt;Well… that’s a long story.&lt;/p&gt;

&lt;p&gt;The Performance team got rolled into the Web Platform team. That had good intentions, but in retrospect a platform team’s high-urgency deploys, monitoring, and incident responses inevitably crowd out important-but-low-urgency speed improvement work.&lt;/p&gt;

&lt;p&gt;Many folks were also taken with the idea of a separate faster site. They volunteered skills and time to estimate the budget, set up CI/CD, and other favors. Their effort, kindness, and optimism amazed me. It seemed inevitable that &lt;em&gt;something&lt;/em&gt; would happen — at least, we’d get a concrete rejection that could inform what we tried next.&lt;/p&gt;

&lt;p&gt;The good news: something did happen.&lt;/p&gt;

&lt;p&gt;The bad news: it was the USA Spring 2020 lockdown.&lt;/p&gt;

&lt;p&gt;After the initial shock, I realized I was in a unique position:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;COVID-19 made it extremely dangerous to enter supermarkets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The pandemic was disproportionately hurting blue-collar jobs, high-risk folks, and the homeless.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I had a proof-of-concept where even cheap and/or badly-connected devices can quickly browse, buy, and order groceries online.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People won’t &lt;em&gt;stop&lt;/em&gt; buying food or medicine, even with stay-at-home orders. If we had a website that let even the poorest shop without stepping in our stores, it would &lt;strong&gt;&lt;em&gt;save lives&lt;/em&gt;&lt;/strong&gt;. Even if they could only browse, it would still cut down on in-store time.&lt;/p&gt;

&lt;p&gt;With a certainty of purpose I’ve never felt before or since, I threw myself into making a &lt;code&gt;kroger.but.fast&lt;/code&gt; &lt;a href="https://en.wikipedia.org/wiki/Minimum_viable_product" rel="noopener noreferrer"&gt;MVP&lt;/a&gt;. I knew it was asking for burnout, but I also knew I’d regret any halfheartedness for the rest of my life — it would have been morally wrong not to try.&lt;/p&gt;

&lt;p&gt;We had the demo running in a prod bucket, agonizingly almost-public, only one secret login away. We tried to get &lt;em&gt;anyone&lt;/em&gt; internally to use it to buy groceries.&lt;/p&gt;

&lt;p&gt;I’m not sure anyone bothered.&lt;/p&gt;

&lt;p&gt;I don’t know what exactly happened. My experience was very similar to &lt;a href="https://listen.casted.us/public/12/Mobile-Matters-748aa039/3b92ffbe" rel="noopener noreferrer"&gt;Zack Argyle’s with Pinterest Lite&lt;/a&gt;, without the happy ending. (It took him 5 years, so maybe I’m just impatient.) I was a contractor, not a “real employee”, so I wasn’t privy to internal decisions — this also meant I couldn’t hear why any of the proposals sent up the chain got lost or rejected.&lt;/p&gt;

&lt;p&gt;Once it filtered through the grapevine that Bridge maybe was competing for resources with a project like this… that was when I decided I was doing nothing but speedrunning hypertension by staying.&lt;/p&gt;

&lt;h2&gt;
  
  
  When bad things happen to fast code
&lt;/h2&gt;

&lt;p&gt;On the one hand, the complete lack of real change is obvious. The demo intentionally rejected much of our design, development, and even management decisions to get the speed it needed. Some sort of skunkworks to insulate from ambient organizational pressures is often the &lt;em&gt;only&lt;/em&gt; way a drastic improvement like this can work, and it’s hard getting clearance for that.&lt;/p&gt;

&lt;p&gt;Another reason: is that to make a drastic improvement on an existing product, there’s an inherent paradox: a lot of folks’ jobs depend on that product, and you can’t get someone to believe something they’re paid not to believe. Especially when the existing architecture was sold as faster than the even-more-previous one. (And isn’t that always the case?)&lt;/p&gt;

&lt;p&gt;It took me a while to understand how people could be personally enthusiastic, but professionally could do nothing. One thing that helped was &lt;a href="https://www.lesswrong.com/posts/45mNHCMaZgsvfDXbw/quotes-from-moral-mazes" rel="noopener noreferrer"&gt;Quotes from &lt;em&gt;Moral Mazes&lt;/em&gt;&lt;/a&gt;. Or, if you want a link less likely to depress you, I was trying to make &lt;a href="https://infrequently.org/2022/05/performance-management-maturity/" rel="noopener noreferrer"&gt;a Level 4 project happen in an org that could charitably be described as Level 0.5&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  But enough about me. What about you?
&lt;/h2&gt;

&lt;p&gt;Maybe you’re making a website that needs to be fast. &lt;strong&gt;The first thing you gotta do is get &lt;em&gt;real&lt;/em&gt; hardware that represents your users.&lt;/strong&gt; &lt;a href="https://adhoc.team/2022/04/12/setting-right-benchmarks-for-site-speed-in-government/" rel="noopener noreferrer"&gt;Set the right benchmarks for the people you serve.&lt;/a&gt; Your technology choices must be informed on that or you’re just posturing.&lt;/p&gt;

&lt;p&gt;If you’re targeting cheap phones, though, I can tell you what I’d look at today.&lt;/p&gt;

&lt;p&gt;For the closest performance to my demo, try &lt;a href="https://markojs.com/" rel="noopener noreferrer"&gt;Marko&lt;/a&gt;. &lt;del&gt;Yes, I’m paid to work on Marko now&lt;/del&gt;, [EDIT: not anymore] but what technology would better match my demo’s speed than the &lt;em&gt;same&lt;/em&gt; technology? (Specifically, I used &lt;a href="https://www.npmjs.com/package/@marko/rollup" rel="noopener noreferrer"&gt;&lt;code&gt;@marko/rollup&lt;/code&gt;&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;But, it’s gauche to only recommend my employer’s thing. What else, what else… If your site doesn’t need JS to work, then absolutely go for a static site. But for something with even sprinkles of interactivity like e-commerce — well, there’s a reason my demo didn’t run JAMstack.&lt;/p&gt;

&lt;p&gt;My checklist of requirements are…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming HTML. (See &lt;a href="https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2"&gt;part #2&lt;/a&gt; for why.)&lt;/li&gt;
&lt;li&gt;Minimum framework JS — at least half of &lt;code&gt;react&lt;/code&gt; + &lt;code&gt;react-dom&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The ability to only hydrate &lt;em&gt;some&lt;/em&gt; components, so your users only download JavaScript that &lt;em&gt;actually&lt;/em&gt; provides dynamic functionality.&lt;/li&gt;
&lt;li&gt;Can render in CDN edge servers. This unfortunately is hard to do for languages other than JavaScript, unless you do something like &lt;a href="https://fly.io/docs/getting-started/multi-region-databases/" rel="noopener noreferrer"&gt;Fly.io’s One Weird Trick&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/solidjs/solid-start" rel="noopener noreferrer"&gt;Solid&lt;/a&gt; is the closest runner-up to Marko; the only requirement it lacks is partial hydration.&lt;/p&gt;

&lt;p&gt;Svelte doesn’t stream, or have partial hydration, but tackles the too-much-app-JS problem via its culture discouraging it. If Svelte implemented streaming HTML, I’d recommend it. &lt;a href="https://github.com/sveltejs/svelte/issues/958" rel="noopener noreferrer"&gt;Maybe someday.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If Preact had partial hydration and streaming, I’d recommend it too; even though Preact’s goals don’t always match mine, I can’t argue with Jason Miller’s consistent results. Preact probably will have equivalents of React’s streaming and Server Components, right?&lt;/p&gt;

&lt;p&gt;Remix is &lt;em&gt;almost&lt;/em&gt; a recommend; its philosophies are 🧑‍🍳💋. &lt;a href="https://blog.jim-nielsen.com/2022/joining-remix/" rel="noopener noreferrer"&gt;Its progressive enhancement approach is exactly what I want&lt;/a&gt;, as of React 18 it can stream HTML, and they’re doing invaluable work &lt;a href="https://remix.run/blog/not-another-framework" rel="noopener noreferrer"&gt;successfully &lt;em&gt;convincing&lt;/em&gt; React devs that those things are important&lt;/a&gt;. This kind of stuff has me shaking my fists in agreement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Wouldn’t it be great, if we could just move all of that code out of the browser and onto the server? Isn’t it annoying to have to write a serverless function any time you need to talk to a database or hit an API that needs your private key? (yes it is). These are the sorts of things React Server Components promise to do for us, and we can definitely look forward to that for data loading, but they don’t do anything for mutations and it’d be cool to move that code out of the browser as well.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://kentcdodds.com/blog/remix-the-yang-to-react-s-yin" rel="noopener noreferrer"&gt;Remix: The Yang to React’s Yin&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
&lt;p&gt;We’ve learned that fetching in components is the quickest way to the slowest UX (not to mention all the content layout shift that usually follows).&lt;/p&gt;

&lt;p&gt;It’s not just the UX that suffers either. The developer experience gets complex with all the context plumbing, global state management solutions (that are often little more than a client-side cache of server-side state), and every component with data needing to own its own loading, error, and success states.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://remix.run/blog/remixing-react-router" rel="noopener noreferrer"&gt;Remixing React Router&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Really, the only thing I don’t like about Remix is… React. Check this perf trace:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94ys3ijbb72zqi2r5wyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94ys3ijbb72zqi2r5wyy.png" alt="Zoomed-in portion of CPU % graph from 2.0 seconds to 3.4 seconds. From 2.2 to 2.6, the main thread is saturated by purple and yellow (style calc and script execution, respectively), then an unknown gray factor saturates the main thread from roughly 2.9 to 3.3. The red long task indicator is not happy about either of those timespans." width="658" height="262"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Sample from &lt;a href="https://www.webpagetest.org/result/220322_AiDc5R_EB/1/details/cached/#waterfall_view_step1" rel="noopener noreferrer"&gt;this &lt;code&gt;remix-ecommerce.fly.dev WebPageTest trace&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Sure, the main thread’s only blocked for 0.8 seconds total, but I don’t want to do that to users on &lt;em&gt;every&lt;/em&gt; page navigation. That’s a good argument for why Remix progressively enhances to client-side navigation… &lt;a href="https://dev.to/tigt/routing-im-not-smart-enough-for-a-spa-5hki"&gt;but I’ve already made my case on that&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ideally, Remix would let you use other frameworks, and I’d shove Marko in there. &lt;a href="https://github.com/remix-run/remix/issues/425" rel="noopener noreferrer"&gt;They’ve discussed the possibility&lt;/a&gt;, so who knows?&lt;/p&gt;

</description>
      <category>performance</category>
      <category>webdev</category>
      <category>react</category>
      <category>management</category>
    </item>
    <item>
      <title>Routing: I’m not smart enough for a SPA</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Tue, 19 Apr 2022 11:30:39 +0000</pubDate>
      <link>https://dev.to/tigt/routing-im-not-smart-enough-for-a-spa-5hki</link>
      <guid>https://dev.to/tigt/routing-im-not-smart-enough-for-a-spa-5hki</guid>
      <description>&lt;p&gt;In part 2, I glossed over a &lt;em&gt;lot&lt;/em&gt; when I wrote…&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I decided this called for an MPA. (aka a traditional web app. Site. Thang. Not-SPA. Whatever.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay, but &lt;em&gt;why&lt;/em&gt; did I decide that? To demo the fastest possible Kroger.com, I should consider all options — why &lt;em&gt;not&lt;/em&gt; a Single-Page App?&lt;/p&gt;

&lt;h2&gt;
  
  
  Alright, let’s try a SPA
&lt;/h2&gt;

&lt;p&gt;While &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#okay-20kb-now-what"&gt;React was ≈2× too big&lt;/a&gt;, surely I could fit a SPA of &lt;em&gt;some&lt;/em&gt; kind in my 20kB budget. I’ve seen it done: the &lt;a href="https://web.dev/load-faster-like-proxx/" rel="noopener noreferrer"&gt;PROXX game’s initial load fits in 20kB&lt;/a&gt;, and &lt;a href="https://web.dev/off-main-thread/#proxx:-an-omt-case-study" rel="noopener noreferrer"&gt;it’s smooth on wimpier phones&lt;/a&gt; than the Poblano.&lt;/p&gt;

&lt;p&gt;So, rough estimate off the top of my head…&lt;/p&gt;

&lt;center&gt;


Source                  | Parse size | gzip size
------------------------|-----------:|---------:
`preact` 10.6.6         | 10.2kB     | 4kB
`preact-router` 4.0.1   | 4.5kB      | 1.9kB
`react-redux` 7.2.6     | 15.4kB     | 5kB
`redux` 4.1.2           | 4.3kB      | 1.6kB
My components[^2]       | 5.9kB      | 2.1kB
**Total**               | 40.3kB     | ≈14.6kB

&lt;/center&gt;

&lt;ul&gt;
&lt;li&gt;The HTML is little else than &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;s, so its size is negligible.&lt;/li&gt;
&lt;li&gt;The remaining 5.4kB is plenty for some hand-tuned CSS.&lt;/li&gt;
&lt;li&gt;This total is pessimistic: &lt;a href="https://bundlephobia.com/scan-results?packages=preact@10.6.6,preact-router@4.0.1,redux@4.1.2,react-redux@7.2.6" rel="noopener noreferrer"&gt;Bundlephobia’s estimate&lt;/a&gt; notes “Actual sizes might be smaller if only parts of the package are used or if packages share common dependencies.” &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seems doable.&lt;/p&gt;

&lt;h2&gt;
  
  
  …But is that &lt;em&gt;really&lt;/em&gt; all the code a SPA needs?
&lt;/h2&gt;

&lt;p&gt;Some code I knew I’d need &lt;em&gt;eventually:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Translation between UI elements and API request/response formats&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/preactjs/preact/issues/3285" rel="noopener noreferrer"&gt;Updates in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Instead of fullsize image links, a lightbox script to avoid unloading the SPA&lt;/li&gt;
&lt;li&gt;Checking and &lt;a href="https://jakearchibald.com/2020/multiple-versions-same-time/" rel="noopener noreferrer"&gt;reloading outdated SPA versions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Code_splitting" rel="noopener noreferrer"&gt;Code splitting&lt;/a&gt; to fit in the 20kB first load (my components estimate was only for the homepage), which means dynamic resource-loading code&lt;/li&gt;
&lt;li&gt;Reimplementing &lt;code&gt;beforeunload&lt;/code&gt; warnings for unsaved user input&lt;/li&gt;
&lt;li&gt;Analytics need extra code for SPAs. Consider two particularly relevant to our site:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://experienceleague.adobe.com/docs/analytics-learn/tutorials/implementation/spa-pages/using-best-practices-when-tracking-spa.html?lang=en#simple-diagram-of-working-with-spas-in-launch" rel="noopener noreferrer"&gt;A “simple diagram” of SPAs in Adobe’s tracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dynatrace.com/support/help/how-to-use-dynatrace/real-user-monitoring/setup-and-configuration/web-applications/initial-configuration/configure-dynatrace-real-user-monitoring-to-capture-xhr-actions" rel="noopener noreferrer"&gt;Dynatrace’s “Capturing &amp;gt; Async web requests and SPAs”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Buuuuuut I don’t have concrete numbers to back these up — I abandoned the SPA approach before grappling with them.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s not always about the performance
&lt;/h2&gt;

&lt;p&gt;I didn’t want to demo a toy site that was fast only because it ignored the responsibilities of the real site. To me, &lt;strong&gt;those responsibilities (of grocery ecommerce) are&lt;/strong&gt;…&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;🛡 &lt;b&gt;Security&lt;/b&gt; even over accessibility
&lt;/dt&gt;
&lt;dd&gt;Downgrading HTTPS ciphers for old browsers isn’t worth letting credit cards be stolen.
&lt;dd&gt;You must protect data customers trust you with before you can use it.&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;♿ &lt;b&gt;Accessibility&lt;/b&gt; even over speed
&lt;/dt&gt;
&lt;dd&gt;A fast site for only the able creates inequality, but a slower site for everyone creates equality.
&lt;dd&gt;Note that &lt;a href="https://bradfrost.com/blog/post/accessibility-and-low-powered-devices/" rel="noopener noreferrer"&gt;speed doubles as accessibility&lt;/a&gt;.&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;🏎️ &lt;b&gt;Speed&lt;/b&gt; even over slickness
&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://www.nngroup.com/articles/theory-user-delight/" rel="noopener noreferrer"&gt;Delight requires showing up on time.&lt;/a&gt;
&lt;dd&gt;Users think fast sites are &lt;a href="https://craigmod.com/essays/fast_software/" rel="noopener noreferrer"&gt;more easy-to-use, well-designed, trustworthy, and pleasant&lt;/a&gt;.
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;Accordingly, I refused to compromise on security or accessibility. A speedup was worthless to me if it conflicted with those two.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;

&lt;p&gt;MPAs and SPAs share most security concerns; they both care about &lt;a href="https://cheatsheetseries.owasp.org/" rel="noopener noreferrer"&gt;XSS/CSRF/other alphabet soups&lt;/a&gt;, and ultimately &lt;em&gt;some&lt;/em&gt; code performs the defenses. The interesting differences are &lt;strong&gt;where that code lives&lt;/strong&gt; and &lt;strong&gt;what that means for ongoing maintenance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;(Unless the stateless ideal of SPA endpoints leads to &lt;a href="https://latacora.micro.blog/2018/06/12/a-childs-garden.html" rel="noopener noreferrer"&gt;something like JWT, in which case now you have worse problems&lt;/a&gt;.)&lt;/small&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Where security code lives
&lt;/h4&gt;

&lt;p&gt;Take CSRF protection: in MPAs, it manifests in-browser as &lt;code&gt;&amp;lt;input type=hidden&amp;gt;&lt;/code&gt; in &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;s and/or &lt;code&gt;samesite=strict&lt;/code&gt; cookies; a small overhead added to the normal HTTP lifecycle of a website.&lt;/p&gt;

&lt;p&gt;SPAs have that same overhead&lt;sup id="fnref1"&gt;1&lt;/sup&gt;, and &lt;em&gt;also…&lt;/em&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JS to get and refresh anti-CSRF tokens&lt;/li&gt;
&lt;li&gt;JS to check which requests need those tokens, then attach them&lt;/li&gt;
&lt;li&gt;JS to handle problems in protected responses; to repair or surface them to the user somehow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat for authentication, escaping, session revocation, and all the other titchy bits of a robust, secure app. &lt;/p&gt;

&lt;p&gt;Additionally, &lt;strong&gt;the deeper and more useful your security, the more the SPA approach penalizes all users&lt;/strong&gt;. That rare-but-crucial warning to immediately contact support? It (or the code to dynamically load it) can either live in everyone’s SPA bundle, &lt;strong&gt;&lt;em&gt;or&lt;/em&gt;&lt;/strong&gt; only burden an MPA’s HTML &lt;em&gt;when&lt;/em&gt; that stress-case happens.&lt;/p&gt;

&lt;h4&gt;
  
  
  What that means for security maintenance
&lt;/h4&gt;

&lt;p&gt;With multiple exposed APIs, you must pentest/fuzz/monitor/etc. each of them. In theory that’s no different than the same for each &lt;code&gt;POST&lt;/code&gt;able URL in an MPA, but…&lt;/p&gt;

&lt;p&gt;Sure, 8 out of 9 teams jumped on updating that vulnerable input-parsing library. Unfortunately, Team 9’s senior dev was out this week and the juniors struggled with a dependency conflict and now an Icelandic teenage hacker ring threatens to release the records of anyone who bought laxatives and cake mix together.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This problem is usually tackled with a unified gateway, which can inflict SPAs with request chains and more script kB to contact it. But for MPAs, &lt;a href="https://iteo.com/blog/post/security-for-single-page-applications/#4-Session-Tracking-and-Authentication" rel="noopener noreferrer"&gt;they already &lt;em&gt;are&lt;/em&gt; that unified gateway&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility
&lt;/h3&gt;

&lt;p&gt;Unlike security, &lt;strong&gt;SPAs have accessibility problems &lt;em&gt;exclusive&lt;/em&gt; to them&lt;/strong&gt;. (Ditto any client-side routing, like &lt;a href="https://turbo.hotwired.dev/handbook/drive" rel="noopener noreferrer"&gt;Hotwire’s Turbo Drive&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;I’d need &lt;a href="https://nolanlawson.com/2019/11/05/what-ive-learned-about-accessibility-in-spas/" rel="noopener noreferrer"&gt;code to restore standard page navigation accessibility&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remember and restore scroll positions across navigations.&lt;/li&gt;
&lt;li&gt;Focus last-used element on back navigations (accounting for &lt;code&gt;autofocus&lt;/code&gt;, &lt;code&gt;tabindex&lt;/code&gt;, JS-driven &lt;code&gt;.focus()&lt;/code&gt;, other complications…)&lt;/li&gt;
&lt;li&gt;Focus a page-representative element on forward navigation, even if it’s not normally focusable. &lt;a href="https://www.gatsbyjs.org/blog/2019-07-11-user-testing-accessible-client-routing/#takeaways-from-users-relying-on-screen-magnification" rel="noopener noreferrer"&gt;Which is filled with gotchas.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And do all that while correctly handling &lt;strong&gt;the other hard parts of client-side routing&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Back/forward buttons&lt;/li&gt;
&lt;li&gt;Timeouts, retries, and error handling&lt;/li&gt;
&lt;li&gt;User cancellations and double-clicks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@dvoytenko/new-web-history-api-91a1a21ff7b8" rel="noopener noreferrer"&gt;History API footguns&lt;/a&gt; — see &lt;a href="https://github.com/WICG/app-history#summary" rel="noopener noreferrer"&gt;its proposed replacement’s reasoning&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Only use SPA navigation if a link is same-origin, not an in-page &lt;code&gt;#fragment&lt;/code&gt;, the same &lt;code&gt;target&lt;/code&gt;, needs authentication…&lt;/li&gt;
&lt;li&gt;Check for Ctrl/⌘/Alt/Shift/right/middle-click, keyboard shortcuts to open in new tabs/windows, or non-standard shortcuts configured by assistive tech (good luck with that last one)&lt;/li&gt;
&lt;li&gt;Reimplement browsers’ load/error UIs, and yours can’t be as good: they can report proxy misconfiguration, DNS failure, what part of the network stack they’re waiting on, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In theory, community libraries should help avoid these problems…&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The majority of routers for React, and other SPA frameworks, do this out of the box. This has been a solved problem for half a decade at least. A website has to go out of its way to mess this up.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://news.ycombinator.com/item?id=30534038" rel="noopener noreferrer"&gt;warning: HackerNews&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We use those libraries at work, and let me tell you: we &lt;em&gt;still&lt;/em&gt; accidentally mess it up &lt;em&gt;all the time&lt;/em&gt;. I asked a maintainer of a popular React router for his take:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We can tell you when and where in the UI you should focus something, but we won’t be focusing anything for you. You also have to consider scroll positions not getting messed up, which is very app-specific and depends on the screen size, UI, padding around the header, etc. It’s incredibly difficult to abstract and generalize.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even worse, some SPA accessibility problems are currently &lt;em&gt;impossible&lt;/em&gt; to fix:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For example, screen readers can produce a summary of a new page when it’s loaded, however it’s not possible to trigger that with JavaScript.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://www.gatsbyjs.org/blog/2020-01-30-why-gatsby-is-better-with-javascript/#accessibility" rel="noopener noreferrer"&gt;Why Gatsby is better with JavaScript § Accessibility&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also, remember how &lt;a href="https://bradfrost.com/blog/post/accessibility-and-low-powered-devices/" rel="noopener noreferrer"&gt;speed doubles as accessibility&lt;/a&gt;?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For accessibility as well as performance, you should limit costly lookups and operations all the time, but especially when a page is loading.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://marcysutton.com/accessibility-and-performance" rel="noopener noreferrer"&gt;Accessibility and Performance · Marcy Sutton&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Emphasis mine — if you should avoid heavy processing during page load, then SPAs have an obvious disadvantage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzou4uicvpd3q7v5q2vk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzou4uicvpd3q7v5q2vk.png" alt="A boy sits next to a skeleton on a sofa. The boy plays the cello, the skeleton holds his head in regret. Or it’s preclempt with the beauty of music? Either way, it dropped its scythe." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I needed something to break up the text.&lt;/p&gt;



&lt;p&gt;Let’s say I do all that, though. Sure, it sounds difficult and error-prone, but &lt;a href="https://www.baldurbjarnason.com/2021/single-page-app-morality-play/#i-am-avarice" rel="noopener noreferrer"&gt;&lt;em&gt;theoretically&lt;/em&gt; it can be done&lt;/a&gt;… by adding client-side JS. And thus we’re back to my original problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes it &lt;em&gt;is&lt;/em&gt; about the performance
&lt;/h2&gt;

&lt;p&gt;You probably don’t have my 20kB budget, but &lt;a href="https://dev.to/addyosmani/loading-web-pages-fast-on-a-20-feature-phone-8h6#prpl30-a-javascript-budget-for-feature-phones"&gt;30–50kB budgets&lt;/a&gt; are a thing I &lt;em&gt;didn’t&lt;/em&gt; invent. And remember: &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#the-goal-how-fast-is-possible"&gt;anything added to a page only makes it slower&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Beyond budget constraints, &lt;a href="https://twitter.com/malchata/status/1279906756861276161" rel="noopener noreferrer"&gt;seemingly-small JS downloads have real, measurable costs on cheap devices&lt;/a&gt;:&lt;/p&gt;

&lt;center&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Startup delay

&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;~163ms
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preact&lt;/td&gt;
&lt;td&gt;~43ms
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bare event listeners&lt;/td&gt;
&lt;td&gt;~7ms
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;&lt;a href="https://css-tricks.com/radeventlistener-a-tale-of-client-side-framework-performance/#aa-startup-time" rel="noopener noreferrer"&gt;Details in Jeremy’s follow-up post.&lt;/a&gt; Note these figures are from a &lt;a href="https://en.wikipedia.org/wiki/Nokia_2" rel="noopener noreferrer"&gt;Nokia 2&lt;/a&gt;, which is more powerful than my target device.&lt;/p&gt;



&lt;h3&gt;
  
  
  I see you with that “premature optimization” comment
&lt;/h3&gt;

&lt;p&gt;The usual advice for not worrying about front-end frameworks goes like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Popular frameworks power some sites that are fast, so which one doesn’t matter — they all can be fast.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Once&lt;/em&gt; you have a performance problem, your framework provides ways to optimize it.&lt;/li&gt;
&lt;li&gt;Don’t avoid libraries, patterns, or APIs &lt;em&gt;until&lt;/em&gt; they cause problems — or that’s how you get premature optimization.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://twitter.com/lexi_lambda/status/1488575204968345602" rel="noopener noreferrer"&gt;idea of “premature optimization” has always been more nuanced than that&lt;/a&gt;, but this logic seems sound.&lt;/p&gt;

&lt;p&gt;However… if you add a library to make future updates faster at the expense of a slower first load… isn’t that already a choice of what to optimize for? By that logic, you should only opt for a SPA once you’ve proven the MPA approach is too slow for your use.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even 10ms of high memory spikes can cause ZRAM kicking in (suuuuper slow) or even app kills. The amount of JS sent to P99 sites is &lt;em&gt;bad news&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;ZRAM impact is system wide. Keyboard may not show up quickly because the page used too much.&lt;/p&gt;

&lt;p&gt;Having less code can make &lt;em&gt;everything&lt;/em&gt; faster.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://twitter.com/slightlylate/status/1195420738762629120" rel="noopener noreferrer"&gt;Alex Russell&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had performance devtools open and dogfooded my own code as I made my demo. Each time I tested a heavier JS approach, I had tangible evidence that avoiding it was &lt;em&gt;not&lt;/em&gt; prematurely optimizing.&lt;/p&gt;

&lt;h3&gt;
  
  
  It’s not just bundle size
&lt;/h3&gt;

&lt;p&gt;Beyond their inexorable gravity of client-side JavaScript, SPAs have other non-obvious performance downsides.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In-page updates buffer &lt;a href="https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2"&gt;the Web’s streaming&lt;/a&gt;, resulting in &lt;a href="https://carter.sande.duodecima.technology/javascript-page-navigation/" rel="noopener noreferrer"&gt;JSON that renders slower than HTML&lt;/a&gt; for &lt;a href="https://jakearchibald.com/2016/fun-hacks-faster-content/" rel="noopener noreferrer"&gt;lack of incremental rendering&lt;/a&gt;. Even if you don’t &lt;em&gt;intentionally&lt;/em&gt; stream, &lt;a href="https://en.wikipedia.org/wiki/Incremental_rendering" rel="noopener noreferrer"&gt;browsers incrementally render&lt;/a&gt; bytes streaming in from the network.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://nolanlawson.com/2022/01/05/memory-leaks-the-forgotten-side-of-web-performance/" rel="noopener noreferrer"&gt;Memory leaks are inevitable&lt;/a&gt;, but they rarely matter in MPAs. In SPAs, one team’s leak ruins the rest of the session.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit" rel="noopener noreferrer"&gt;JS and &lt;code&gt;fetch()&lt;/code&gt; have lower network priority than “main resources”&lt;/a&gt; (&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;). This even affects how the OS prioritizes your app over other programs.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lastly, and &lt;strong&gt;most importantly: server code can be measured, scaled, and optimized&lt;/strong&gt; until you know you can stop worrying about performance. But &lt;a href="https://twitter.com/nathanhammond/status/1504193960482201601" rel="noopener noreferrer"&gt;&lt;strong&gt;clients are unboundedly bad&lt;/strong&gt;, with decade-old chips and miserly RAM in new phones&lt;/a&gt;. The Web’s size and diversity makes client-side “fast enough” impossible to judge. And if your usage statistics come from JS-driven analytics that must download, parse, and upload to record a user… can you be certain of low-end usage?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fresh loads happen more than you think
&lt;/h3&gt;

&lt;p&gt;The core SPA tradeoff: the first load is slower, but it sets up extra code to make future interactions snappier.&lt;/p&gt;

&lt;p&gt;But there are &lt;strong&gt;situations where we can’t control when fresh loads happen&lt;/strong&gt;, making that one-time payment more like compounding debt:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Page freezing/eviction
&lt;/dt&gt;
&lt;dd&gt;Mobile browsers aggressively &lt;a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api" rel="noopener noreferrer"&gt;background, freeze, and discard pages&lt;/a&gt;.
&lt;dd&gt;Switching apps/tabs unloads the first annoyingly often — ask any iOS user.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;Browser/OS updates and crashes
&lt;/dt&gt;
&lt;dd&gt;Many devices auto-update while charging.
&lt;dd&gt;I’m sure you can guess whether SPAs or MPAs crash more often.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;Deeplinks and new tabs
&lt;/dt&gt;
&lt;dd&gt;Users open things in new tabs whether we like it or not.
&lt;dd&gt;Outside links must do a fresh boot, weakening email/ad campaigns, search engine visits, and shared links.
&lt;dd&gt;Multi-device usage really doesn’t help.
&lt;dd&gt;
&lt;a href="https://thenextweb.com/opinion/2018/04/03/rant-app-browsers-annoying-mostly-useless/" rel="noopener noreferrer"&gt;In-app browsers&lt;/a&gt; share almost nothing with the default browser cache, which turns what users &lt;em&gt;think&lt;/em&gt; are return visits into fresh load→parse→execute.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;Intentional page refreshes
&lt;/dt&gt;
&lt;dd&gt;Users frequently refresh upon real/perceived issues, especially during support. The world considers Refresh ↻ the fixit button, &lt;a href="https://www.quora.com/Why-does-sometimes-stopping-a-page-load-and-then-refreshing-it-help-load-the-page-faster" rel="noopener noreferrer"&gt;and they’re not wrong&lt;/a&gt;.
&lt;dd&gt;SPAs often refresh themselves for logins, error-handling, etc.
&lt;dd&gt;A related problem; you know those “This site has updated. Please refresh” messages? They bother the user, invoke a refresh, and replicate a part of native apps &lt;em&gt;nobody&lt;/em&gt; likes.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;But when fresh pageloads are fast, you can cheat: who cares about reloading when it’s near-instant?&lt;/p&gt;

&lt;h2&gt;
  
  
  SPAs are more fragile
&lt;/h2&gt;

&lt;p&gt;While “rebooting” on every navigation can seem wasteful, it might be the best survival mechanism we have for the Web:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These errors come, in large part, from users running odd niche or out-of-date browsers with broken Javascript or DOM implementations, users with buggy browser extensions injecting themselves into your scope, adblockers blocking one of your &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, broken browser caches or middleboxes, and other weirder and more exotic failure modes.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://blog.nelhage.com/post/systems-that-defy-understanding/#client-side-javascript" rel="noopener noreferrer"&gt;Systems that defy detailed understanding § Client-side JavaScript&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F46w4m8r1ukljix8pt6ek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F46w4m8r1ukljix8pt6ek.png" alt="A line chart titled “Errors grouped by browser”. The chart and samples are illegible to me: my only takeaway is that the y-axis tops out at 3,000." width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A typical Datadog error summary.&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;Given that most bugs are transient&lt;sup&gt;3&lt;/sup&gt;, simply restarting processes back to a state known to be stable when encountering an error can be a surprisingly good strategy.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;sup&gt;3&lt;/sup&gt; 131 out of 132 bugs are transient bugs (they’re non-deterministic and go away when you look at them, and trying again may solve the problem entirely), according to Jim Gray in &lt;a href="http://www.hpl.hp.com/techreports/tandem/TR-85.7.html" rel="noopener noreferrer"&gt;Why Do Computers Stop and What Can Be Done About It?&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://s3.us-east-2.amazonaws.com/ferd.erlang-in-anger/text.v1.1.0.pdf#chapter*.3" rel="noopener noreferrer"&gt;Erlang in Anger § On Running Software&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We control our servers’ features and known bugs, and can monitor them more thoroughly than browsers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Client error monitoring must download, run, and upload to be recorded, which is much flakier and &lt;a href="https://nicj.net/side-effects-of-boomerangs-javascript-error-tracking/" rel="noopener noreferrer"&gt;adds its own fun drawbacks&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Relying on client JS is a hard known-unknown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://adamsilver.io/articles/javascript-isnt-always-available-and-its-not-the-users-fault/" rel="noopener noreferrer"&gt;JavaScript isn’t always available and it’s not the user’s fault&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kryogenix.org/code/browser/why-availability/" rel="noopener noreferrer"&gt;Why availability matters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@dan_abramov/two-weird-tricks-that-fix-react-7cf9bbdef375#486f" rel="noopener noreferrer"&gt;Dan Abramov’s warning against React roots on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/WICG/interventions" rel="noopener noreferrer"&gt;User-Agent Interventions&lt;/a&gt; are when browsers &lt;em&gt;intentionally&lt;/em&gt; mess with your JS.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Overall, SPAs’ reliance on client JS makes them fail unpredictably at the seams: the places we don’t control, the contexts we didn’t plan for. Enough edge-cases added up are the sum total of humanity.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a SPA is a good choice
&lt;/h2&gt;

&lt;p&gt;Remember &lt;a href="https://proxx.app/" rel="noopener noreferrer"&gt;PROXX&lt;/a&gt; from earlier? It did fit in 20kB, but it also doesn’t worry about a &lt;em&gt;lot&lt;/em&gt; of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its content is procedurally generated, with only a few simple interactions.&lt;/li&gt;
&lt;li&gt;It has few views, a single URL, and doesn’t load further data from a server.&lt;/li&gt;
&lt;li&gt;It doesn’t care about security: it’s a game without logins or multiplayer.&lt;/li&gt;
&lt;li&gt;If it weren’t accessible&lt;sup id="fnref3"&gt;3&lt;/sup&gt; or broke from client-side fragility… so what? It’s a game. Nobody’s missing out on useful information or services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PROXX is perfect as a SPA. You &lt;em&gt;could&lt;/em&gt; make &lt;a href="https://github.com/davux/mine" rel="noopener noreferrer"&gt;a Minesweeper clone with &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; and a server&lt;/a&gt;, but it would probably not feel as fun. Games often &lt;em&gt;should&lt;/em&gt; maximize fun at the expense of other qualities.&lt;/p&gt;

&lt;p&gt;Similarly, &lt;a href="https://squoosh.app/" rel="noopener noreferrer"&gt;the Squoosh SPA&lt;/a&gt; makes sense: the overhead of uploading unoptimized images probably outweighs the overhead of expensive client-side processing, plus offline and privacy benefits. But even then, there are many server-side image processors, like &lt;a href="https://ezgif.com/" rel="noopener noreferrer"&gt;ezgif&lt;/a&gt; or &lt;a href="https://imageoptim.com/online" rel="noopener noreferrer"&gt;ImageOptim online&lt;/a&gt;, so clearly there’s nuance.&lt;/p&gt;

&lt;p&gt;You don’t have to choose extremes! You can quarantine JS-heavy interactivity to individual pages when it makes sense: SPAs can easily embed in an MPA. (The reverse, though… if it’s even possible, it sounds like it’d inherit the weaknesses of both without any of their strengths.)&lt;/p&gt;

&lt;h2&gt;
  
  
  But if SPAs only bring ☹️, why would they exist?
&lt;/h2&gt;

&lt;p&gt;We’re seeing the pendulum &lt;em&gt;finally&lt;/em&gt; swing away from &lt;a href="https://www.thoughtworks.com/radar/techniques/spa-by-default" rel="noopener noreferrer"&gt;SPAs for everything&lt;/a&gt;, and maybe you’ll be in a position someday where you can choose. On the one hand, I’d be &lt;em&gt;delighted&lt;/em&gt; to hand you more literature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/@mlrawlings/maybe-you-dont-need-that-spa-f2c659bc7fec" rel="noopener noreferrer"&gt;Maybe you don’t need that SPA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adactio.com/journal/7706" rel="noopener noreferrer"&gt;Be progressive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thatemil.com/blog/2013/07/02/progressive-enhancement-still-not-dead/" rel="noopener noreferrer"&gt;Progressive Enhancement: Still Not Dead.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand…&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please beat offline-first ServiceWorker-cached application shells or even static HTML+JS on a local CDN with a cgi page halfway across the globe.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://dev.to/qm3ster/comment/1n3d4"&gt;Mihail Malo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is true. It’s not enough for me to say “don’t use client-side navigation” — those things are important for any site, whether MPA or SPA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offline-first reliability and speed&lt;/li&gt;
&lt;li&gt;Serving as near end-users as possible&lt;/li&gt;
&lt;li&gt;Not using CGI (it’s 2022! at least use FastCGI)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, next time: &lt;strong&gt;can we get the benefits that SPAs enjoy, without suffering the consequences they extremely don’t enjoy?&lt;/strong&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Some say that instead of CSRF tokens, SPAs may only need to fetch data from subdomains to guarantee an &lt;code&gt;origin&lt;/code&gt; header. Maybe, but &lt;a href="https://sites.google.com/a/chromium.org/dev/developers/design-documents/dns-prefetching" rel="noopener noreferrer"&gt;the added DNS lookup has its own performance tax&lt;/a&gt;, and &lt;a href="https://textslashplain.com/2022/03/31/chromiums-dns-cache/" rel="noopener noreferrer"&gt;more often than you’d think&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;No, this was not an incident we had. For starters, the teenagers were Luxembourgian. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;PROXX actually did put a lot of effort into accessibility, and that’s cool. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>a11y</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Speed Needs Design, or: You can’t delight users you’ve annoyed</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Thu, 24 Mar 2022 12:31:17 +0000</pubDate>
      <link>https://dev.to/tigt/speed-needs-design-or-you-cant-delight-users-youve-annoyed-bl6</link>
      <guid>https://dev.to/tigt/speed-needs-design-or-you-cant-delight-users-youve-annoyed-bl6</guid>
      <description>&lt;p&gt;To really sell streamed HTML’s impact, I should have shown the existing site’s design, but with streaming’s speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo’s design deviations
&lt;/h2&gt;

&lt;p&gt;Unfortunately, &lt;strong&gt;it was impossible to have the existing design and the speed I needed&lt;/strong&gt;. This post is about some of the differences.&lt;/p&gt;

&lt;p&gt;If I were &lt;em&gt;a good scientist&lt;/em&gt;, I’d have before/after traces for each deviation. Unfortunately, I do not — I was hastily testing and retesting &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#some-sort-of-higherlevel-goal"&gt;on real hardware and a packet-throttled connection&lt;/a&gt;. If something &lt;em&gt;felt&lt;/em&gt; slower, I tossed it.&lt;/p&gt;

&lt;p&gt;If I’m wrong about something, I’ll happily accept corrections with a good source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design goals
&lt;/h3&gt;

&lt;p&gt;Look, I’m jealous of native devs too. They get 60–120fps fullscreen animations with builtin platform support and minimum overhead.&lt;/p&gt;

&lt;p&gt;But this is &lt;strong&gt;a website that sells food&lt;/strong&gt;: I will relentlessly sacrifice delight for easier access. I can never &lt;em&gt;truly&lt;/em&gt; catch up to native apps’ interaction richness, platform fidelity, and SFX flaunting. But I &lt;em&gt;can&lt;/em&gt; beat native apps at &lt;a href="https://daringfireball.net/2004/06/location_field" rel="noopener noreferrer"&gt;the Web’s strength of getting things done, easily, with little fuss&lt;/a&gt;. I wasn’t just trying to outrace our existing site — I aimed at our native app too.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I figured I could add flourishes and rise to meet interesting challenges &lt;em&gt;after&lt;/em&gt; finishing the usable, accessible bones.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;My amateur login page&lt;/th&gt;
&lt;th&gt;The professional login page

&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;
&lt;img alt="A basic login page, with a “Sign in to Kroger” heading, a username field, a password field, a “Forgot your password?” link, a “Stay signed in on this device” checkbox, and “Sign in”/“Create an account” buttons." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fau4hu7hk1mfje2ejio8h.png" width="480" height="768"&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;img alt="A loading spinner in a sea of white." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fem2aus7uqasjghkwcxs1.png" width="480" height="768"&gt;
&lt;/td&gt;
&lt;/tr&gt;&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Which design looks better, 10 seconds in?&lt;/p&gt;



&lt;p&gt;That said, I’m no designer. &lt;strong&gt;A &lt;em&gt;real&lt;/em&gt; designer could probably make these tradeoffs with better results.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fonts
&lt;/h3&gt;

&lt;p&gt;Kroger.com’s &lt;a href="https://fonts.google.com/specimen/Nunito" rel="noopener noreferrer"&gt;Nunito&lt;/a&gt; &lt;code&gt;.woff2&lt;/code&gt; files are ≈14kB each, totaling 55.3kB.&lt;/p&gt;

&lt;p&gt;If I found a smaller lookalike font, subset aggressively, and applied &lt;a href="https://pixelambacht.nl/2016/font-awesome-fixed/" rel="noopener noreferrer"&gt;more arcane font optimizations&lt;/a&gt;, that could probably shrink to ~12kB total.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; But remember &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#googles-suggestions-to-be-fast-on-mobile"&gt;the 20kB budget&lt;/a&gt;? 60% of that for a decorative font is hard to justify.&lt;/p&gt;

&lt;p&gt;System fonts also spend less time on the main thread: each webfont &lt;code&gt;src&lt;/code&gt; causes a reflow (they can batch together, but don’t count on it). &lt;a href="https://twitter.com/csswizardry/status/1504507222491074560" rel="noopener noreferrer"&gt;Usually the reflow isn’t awful&lt;/a&gt;, but you know who’s not too good for free performance? 👉 This dork! 👈&lt;/p&gt;

&lt;p&gt;So, &lt;strong&gt;no webfonts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But that was an easy decision: avoiding custom fonts is &lt;em&gt;negative&lt;/em&gt; work. How about something harder?&lt;/p&gt;

&lt;h3&gt;
  
  
  Product carousels
&lt;/h3&gt;

&lt;p&gt;Displaying products is ecommerce’s most important job. Which is why it was a problem that our scrolling product lists didn’t fit on &lt;a href="https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na#some-sort-of-higherlevel-goal"&gt;the phone we sell&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgpivx7apssnhncjdjflf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgpivx7apssnhncjdjflf.png" alt="With the Poblano screen dimensions, the product cards are too tall, and you can only see one at a time." width="320" height="480"&gt;&lt;/a&gt;&lt;p&gt;I know whitespace is an important part of design, but this is probably not what they meant.&lt;/p&gt;


&lt;/p&gt;

&lt;p&gt;That was the obvious reason to change, but there’s also subtler drawbacks:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Memory pressure and increased layout cost
  &lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Scroll panes use RAM and VRAM for hardware-accelerated scrolling — but we need those for fast &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; scrolling, too!
  
  &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F05x25917kk5tbquxqc1s.png" class="article-body-image-wrapper"&gt;&lt;img alt="A long white bar of the total surface area of the vertically-scrolling page, with four other long bars poking out to the right for the horizontal scroll areas. One of them extends so far it’s not fully shown." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F05x25917kk5tbquxqc1s.png" width="800" height="400"&gt;&lt;/a&gt;&lt;p&gt;Current homepage in Chrome DevTools’ Layers view. It only rasterizes the non-blank parts, but still calculates layout for all areas.&lt;/p&gt;
  
  

  &lt;/p&gt;
&lt;p&gt;Remember how iOS Safari needed opting-in for accelerated scrolling with &lt;code&gt;-webkit-overflow-scrolling: touch&lt;/code&gt;? Now you know why.

&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;Scroll trap on touchscreens
  &lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;The lists take up so much vertical space that they become infuriating scroll traps on the demo phones, &lt;a href="https://bradfrost.com/blog/post/adaptive-maps/" rel="noopener noreferrer"&gt;like mobile map embeds&lt;/a&gt;. (It doesn’t help that cheap touchscreens often get swipe direction confused at the beginning of a gesture.)

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Reflow and pop-in
  &lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;As new cards load on scroll, they all resize their height to match the tallest. Even when the shift is minimal, the necessary layout calculations still hiccuped on the demo phones.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Poor use of small viewport
  &lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;The Poblano wasn’t the smallest screen I targeted. I wanted to preserve legibility down to the Apple Watch.
&lt;/p&gt;&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;Instead, I displayed products like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2a3scml4bgijyvbbglt6.png" class="article-body-image-wrapper"&gt;&lt;img alt="The demo’s products displayed as a vertical list, in block-level flow as websites do by default." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2a3scml4bgijyvbbglt6.png" width="320" height="480"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;No, it’s not very attractive. But it works a hell of a lot better.&lt;/p&gt;



&lt;p&gt;You can probably spot several tricks to save space in the above screenshot: product sizes inline with the names, relying on text’s horizontal nature for more wrapping compactness, etc. &lt;/p&gt;

&lt;p&gt;
  ⌛ Other things I wanted to do, but ran out of time
  &lt;p&gt;I wanted to use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/shape-outside" rel="noopener noreferrer"&gt;&lt;code&gt;shape-outside&lt;/code&gt;&lt;/a&gt; to flow text into the empty spaces of non-rectangular products, so I could make the image larger. (I had a burning desire to maximize how many onscreen pixels could be image pixels without sacrificing too much information density.) I might post how to do this, someday.&lt;/p&gt;

&lt;p&gt;Later, the “stickiness” of scrolling lists was cited to me as an important quality. I wanted to recoup some of that by decorating the “See more” links at the end of each product list with thumbnails of what more was on that list, but ran out of time before the demo. &lt;em&gt;C’est la vie.&lt;/em&gt;&lt;/p&gt;



&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;box-shadow&lt;/code&gt; more like &lt;code&gt;bourgeois-shadow&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I know, I know. Shadows are fashionable. If used correctly they make interfaces more understandable. The interplay of light and shadow is the sum total of visual art itself.&lt;/p&gt;

&lt;p&gt;But even from a strict design perspective, shadows aren’t all-upside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;area needed to spread/blur shadows requires space around elements&lt;/strong&gt;, which comes at a premium on the small screens I targeted.&lt;/li&gt;
&lt;li&gt;Current popular use is subtle, which means &lt;strong&gt;they can never be the workhorse of a design&lt;/strong&gt;. (Unless you make &lt;a href="https://axesslab.com/neumorphism/" rel="noopener noreferrer"&gt;shadows the &lt;em&gt;only&lt;/em&gt; design element, and that’s terrible&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An affordance that doesn’t meet contrast requirements is questionably useful for the people who need it most.&lt;/strong&gt; Or even most people: glaring sunlight, tired eyes, distracted attention, etc. need &lt;em&gt;strong&lt;/em&gt; affordances, not minimal ones. (I’m not even sure how many people &lt;em&gt;can&lt;/em&gt; notice trendy shadows, from a visual accessibility perspective.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And from &lt;em&gt;not&lt;/em&gt; a strict design perspective…&lt;/p&gt;

&lt;p&gt;Historically, &lt;a href="https://medium.com/airbnb-engineering/css-box-shadow-can-slow-down-scrolling-d8ea47ec6867" rel="noopener noreferrer"&gt;&lt;code&gt;box-shadow&lt;/code&gt; is a career performance criminal&lt;/a&gt;. Probably not as much nowadays, unless some combination of inset, blur radius, &lt;code&gt;border-radius&lt;/code&gt;, transparency, shadowed element size, screen resolution, browser, OS, or hardware falls off browsers’ fast paths.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9m3dkljc0u2sth4by04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9m3dkljc0u2sth4by04.png" alt="A flame chart bar, with a legend reading: 341ms requestAnimationFrame callbacks; Type: Paint." width="670" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Modern advice mostly &lt;a href="https://web.dev/animations-guide/#paint" rel="noopener noreferrer"&gt;warns about transitioning or animating around shadows&lt;/a&gt; — especially since &lt;a href="https://ishadeed.com/article/new-facebook-css/#using-an-image-for-the-shadow" rel="noopener noreferrer"&gt;scrolling under them also counts, as Facebook learned&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(Yes, Google’s Material Design went all-in on blurring and animating &lt;em&gt;lots&lt;/em&gt; of shadows. Have you seen &lt;a href="https://github.com/flutter/flutter/issues/54507" rel="noopener noreferrer"&gt;how much effort they spend because of that&lt;/a&gt;?)&lt;/p&gt;

&lt;p&gt;There &lt;em&gt;are&lt;/em&gt; ways to work around shadows’ performance risks, which might even be worth the effort.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6w053syissr3g9tyx7vx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6w053syissr3g9tyx7vx.png" alt="Coupons on Kroger.com, with shadows for each coupon and their primary action buttons." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But… man, these &lt;code&gt;box-shadow&lt;/code&gt;s just don’t seem &lt;em&gt;that&lt;/em&gt; worth it. They’re subtle! Intentionally! If I have to plan how to protect users from them, there needs to be &lt;em&gt;way&lt;/em&gt; more of a payoff. At least skeuomorphism was in-your-face.&lt;/p&gt;

&lt;p&gt;If a design effect is only noticeable to young people with good vision, and it can unpredictably penalize people with cheap or overworked devices… then I don’t care how delightful it is. The risks conflicted with my goals and this was &lt;em&gt;my&lt;/em&gt; stupid passion project, dammit. &lt;strong&gt;I didn’t trust them and I didn’t need them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So, I dropped the shadows. &lt;small&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/drop-shadow()" rel="noopener noreferrer"&gt;…but not like that.&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Homepage promotions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Carousel
&lt;/th&gt;
&lt;th&gt;Tiles

&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1q5bkx4xui4lxywunjmk.png" alt="An autorotating carousel with a pause button and 3 dots indicating how many slides it has. The first dot is darker to indicate the current slide." width="466" height="466"&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzpasqqaxi5c48vqum2p5.png" alt="3 tiles, showing the same amount of promotions as the carousel via the ancient and mystical art of “put them next to each other”." width="466" height="414"&gt;
&lt;/td&gt;
&lt;/tr&gt;&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Think about what code the carousel needs that tiles don’t:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next/previous swiping vs. buttons, and switching between them&lt;/li&gt;
&lt;li&gt;Position indicator&lt;/li&gt;
&lt;li&gt;Pause/Play (required for accessibility)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(prefers-reduced-motion)&lt;/code&gt; check and mitigation&lt;/li&gt;
&lt;li&gt;Accessibly hiding offscreen slides&lt;/li&gt;
&lt;li&gt;Autoforwarding&lt;/li&gt;
&lt;li&gt;Animation timing&lt;/li&gt;
&lt;li&gt;Repositioning slides to the left when wrapping&lt;/li&gt;
&lt;li&gt;
Tab ↹ handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No matter how efficiently those features are implemented, they’re still code users must download. (&lt;a href="https://jhalabi.com/blog/carousels-no-one-likes-you" rel="noopener noreferrer"&gt;If users actually &lt;em&gt;liked&lt;/em&gt; carousels&lt;/a&gt; that could be worth it, but, uh…)&lt;/p&gt;

&lt;h4&gt;
  
  
  But the demo didn’t use tiles either
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://hollypryce.com/text-images/" rel="noopener noreferrer"&gt;Encoding each promotion as one big image has problems&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scaled-down text becomes unreadable small viewports&lt;/li&gt;
&lt;li&gt;Doesn’t work with High-Contrast Mode or other &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors" rel="noopener noreferrer"&gt;&lt;code&gt;forced-colors&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Takes too long to display &lt;em&gt;anything&lt;/em&gt; on the target connection, &lt;a href="https://www.nngroup.com/articles/website-response-times/#:~:text=Fancy%20Widgets%2C%20Sluggish%20Response" rel="noopener noreferrer"&gt;which meant it was as good as not having it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://whatdoesmysitecost.com/" rel="noopener noreferrer"&gt;Costs users more&lt;/a&gt; than I was comfortable with. &lt;em&gt;Especially&lt;/em&gt; since I couldn’t aggressively compress the images without making their text look crusty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the demo went even further beyond, with an approach I’m calling “ribbons” just now. Here’s how those load over the target connection:&lt;/p&gt;

&lt;p&gt;The text and colored backgrounds are visible instantly, even though the images take a few seconds. (A media query rejiggers them into tiles on larger viewports.)&lt;/p&gt;



&lt;h3&gt;
  
  
  No modals, tooltips, toasts, etc.
&lt;/h3&gt;

&lt;p&gt;Unlike the parts of this post where it felt like I was compromising, this was a user experience &lt;em&gt;improvement&lt;/em&gt;. Do &lt;em&gt;you&lt;/em&gt; like dealing with &lt;a href="https://deathtobullshit.com/" rel="noopener noreferrer"&gt;modals and pop-up banners and all their annoying friends&lt;/a&gt;? Me neither.&lt;/p&gt;

&lt;p&gt;That’s an opinion, though. Some more objective reasons I eschewed widgets for boring alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Their JavaScript and CSS eat away at the performance budget&lt;/li&gt;
&lt;li&gt;They make less sense on small/touch screens — in particular, modals take up nearly the entire page anyway&lt;/li&gt;
&lt;li&gt;They’re hard to make accessible (modals in particular have been called “the final boss of web accessibility”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More on that last one… Even if I &lt;em&gt;did&lt;/em&gt; succeed in making widgets accessible, the &lt;a href="https://www.smashingmagazine.com/2021/06/css-javascript-requirements-accessible-components/" rel="noopener noreferrer"&gt;JavaScript required to do so&lt;/a&gt; really adds up. Check out the sizes of some popular modules, each a reasonable choice for tooltips, modals, and toasts respectively:&lt;/p&gt;
&lt;a href="https://bundlephobia.com/scan-results?packages=@popperjs/core@2.11.4,@reach/dialog@0.16.2,notistack@2.0.3" rel="noopener noreferrer"&gt;JS cost according to Bundlephobia&lt;/a&gt;




&lt;center&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;th&gt;Module
&lt;/th&gt;
&lt;th&gt;Minified
&lt;/th&gt;
&lt;th&gt;.min.gz
&lt;/th&gt;
&lt;th&gt;Slow 3G download

&lt;/th&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;@popperjs/core&lt;/code&gt; 2.11.4
&lt;/th&gt;
&lt;td&gt;20.5 kB&lt;/td&gt;
&lt;td&gt;7.2 kB&lt;/td&gt;
&lt;td&gt;144 ms

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;@reach/dialog&lt;/code&gt; 0.16.2
&lt;/th&gt;
&lt;td&gt;27.3 kB&lt;/td&gt;
&lt;td&gt;9.4 kB&lt;/td&gt;
&lt;td&gt;188 ms

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;notistack&lt;/code&gt; 2.0.3
&lt;/th&gt;
&lt;td&gt;18.8 kB&lt;/td&gt;
&lt;td&gt;6.4 kB&lt;/td&gt;
&lt;td&gt;128 ms

&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tfoot&gt;
&lt;tr&gt;
&lt;th&gt;Total
&lt;/th&gt;
&lt;td&gt;66.7 kB&lt;/td&gt;
&lt;td&gt;23 kB&lt;/td&gt;
&lt;td&gt;~½ second!
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tfoot&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;b&gt;✏️ Note:&lt;/b&gt; this table doesn’t include &lt;a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/javascript-startup-optimization#parsecompile" rel="noopener noreferrer"&gt;parse/execute time, which scales with minified kB&lt;/a&gt;.
&lt;/p&gt;
&lt;/center&gt;

&lt;p&gt;(Yes, there are &lt;a href="https://inclusive-components.design/#components" rel="noopener noreferrer"&gt;more efficient ways to script these widgets&lt;/a&gt;. I just grabbed whatever looked well-maintained and popular, like the vast majority of devs.)&lt;/p&gt;

&lt;p&gt;Finally, implementing complex interactivity would have taken a lot out of &lt;em&gt;me&lt;/em&gt;. Check out what it took for Pedro Duarte to make an accessible dropdown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdidkxbzarobe2xxzm0fe.png" class="article-body-image-wrapper"&gt;&lt;img alt="Dropdown Menu: 2,000+ hours, 6 months, 50 reviews, and 1,000s of commits." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdidkxbzarobe2xxzm0fe.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source: &lt;a href="https://medium.com/@nirbenyair/headless-components-in-react-and-why-i-stopped-using-ui-libraries-a8208197c268" rel="noopener noreferrer"&gt;Headless components in React and why I stopped using a UI library for our design system&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;But if these UI patterns are annoying, not very accessible, and costly, &lt;strong&gt;why do so many sites use them?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I think they’re easier to design, since they don’t have to care about the underlying page.&lt;/li&gt;
&lt;li&gt;I suspect analytics falsely report increased “engagement” because they need more interaction than less intrusive designs. Even if said interaction is, say, users trying to get the damn carousel to hold still.&lt;/li&gt;
&lt;li&gt;Sometimes, they can be the best solution for a problem — but as soon as you have that component, it’s tempting to reuse it for other, less-suited problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More on alternatives to these known user-aggravators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://adamsilver.io/blog/designing-for-actual-performance/#1.-simplify-the-interface" rel="noopener noreferrer"&gt;Designing for actual performance § Simplify the interface&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modalzmodalzmodalz.com/" rel="noopener noreferrer"&gt;The good news: It doesn’t have to be a modal!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://adamsilver.io/blog/the-problem-with-snackbars-and-toast-messages/" rel="noopener noreferrer"&gt;The problem with snackbars and toast messages (and what to use instead)&lt;/a&gt; and &lt;a href="https://adamsilver.io/articles/the-problem-with-tooltips-and-what-to-do-instead/" rel="noopener noreferrer"&gt;its companion on tooltips&lt;/a&gt;, by Adam Silver&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Better design via speed
&lt;/h2&gt;

&lt;p&gt;Unlike the other sections where design was in service to speed, this section is about how fast page loads can enable better design!&lt;/p&gt;

&lt;p&gt;Our existing checkout flow had expanding/contracting accordions, intertwingled error constraints, and tricky &lt;code&gt;.focus()&lt;/code&gt; management because of the first two. I wanted to avoid all that, so I broke up checkout into a series of small, quick pages:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9ozmw8o2enwf579qjfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9ozmw8o2enwf579qjfe.png" alt="The pages of my checkout flow: 1. My shopping cart, 2. Order summary, 3. Contact information, 4. Thank you for choosing Pickup!" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve heard of &lt;a href="https://www.smashingmagazine.com/2017/05/better-form-design-one-thing-per-page/" rel="noopener noreferrer"&gt;One Thing Per Page&lt;/a&gt;, that’s what I did. It didn’t take long to code, and was surprisingly easy.&lt;/p&gt;

&lt;p&gt;But wait! There’s more!&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;low-confidence users find them easier to use&lt;/li&gt;
&lt;li&gt;they work well on mobile devices&lt;/li&gt;
&lt;li&gt;they’re better at handling things like errors, branches, loops and saving progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— &lt;a href="https://designnotes.blog.gov.uk/2015/07/03/one-thing-per-page/" rel="noopener noreferrer"&gt;One thing per page · Design in government&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn’t implement them for the demo, but functionality like user’s Account info and settings would also be well-served by this pattern.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[W]hen we started user research with the general public, we saw a very positive response to the simple step by step approach, even on large screens. Though it added more clicks, people said it made the process feel simple and easy - there wasn’t too much to take in and process at any one time. So we stuck with the simpler screens for everyone.&lt;/p&gt;

&lt;p&gt;— &lt;a href="https://designnotes.blog.gov.uk/2014/07/14/things-we-learnt-designing-register-to-vote/" rel="noopener noreferrer"&gt;GOV.UK: Things we learnt designing ‘Register to vote’&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And thanks to &lt;a href="https://developers.google.com/web/updates/2019/05/paint-holding" rel="noopener noreferrer"&gt;Paint Holding&lt;/a&gt; these page sequences looked and felt as seamless as SPA navigation. (&lt;em&gt;Lots&lt;/em&gt; more on that in the next post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  So what?
&lt;/h2&gt;

&lt;p&gt;Like how woodworking involves sculpting along the grain, web design needs an understanding of what’s easy and natural, to save effort for things that &lt;em&gt;really&lt;/em&gt; need it.&lt;/p&gt;

&lt;p&gt;Our current practices of pushing designers away from HTML &amp;amp; CSS thwarts &lt;a href="https://frankchimero.com/blog/2015/the-webs-grain/" rel="noopener noreferrer"&gt;an intuitive understanding of what’s easy vs. what’s hard on the Web&lt;/a&gt;. More cooperation and faster feedback loops could help — because I was designing and developing myself, the loop was as tight as possible as I switched hats on the fly.&lt;/p&gt;

&lt;p&gt;But, uh, it’s also true my designs won’t win any beauty contests. So don’t trust me, listen to some actual designers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sparkbox.com/foundry/beginners_guide_to_performant_design_decisions" rel="noopener noreferrer"&gt;A Beginner’s Guide to Performant Design Decisions&lt;/a&gt; &amp;amp; &lt;a href="https://speakerdeck.com/katiekovalcin/the-path-to-performance" rel="noopener noreferrer"&gt;The Path to Performance&lt;/a&gt; by Katie Kovalcin&lt;/li&gt;
&lt;li&gt;&lt;a href="https://designingforperformance.com/preface/" rel="noopener noreferrer"&gt;Designing for Performance · Lara Hogan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.smashingmagazine.com/2019/06/web-designers-speed-mobile-websites/" rel="noopener noreferrer"&gt;What Web Designers Can Do To Speed Up Mobile Websites · Suzanne Scacca&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bradfrost.com/blog/post/performance-as-design/" rel="noopener noreferrer"&gt;Performance As Design · Brad Frost&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next post, I reveal that while I may be a twentysomething paid to work in React, &lt;em&gt;I’m secretly a filthy progressive enhancement liker&lt;/em&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Native has much improved since &lt;a href="https://daringfireball.net/2004/06/location_field" rel="noopener noreferrer"&gt;Gruber wrote that post&lt;/a&gt;. It’s time web devs &lt;em&gt;also&lt;/em&gt; stepped up our game, and not by playing catch-up: “What I missed when I dismissed them a decade ago is that web apps don’t need to beat desktop apps on the same terms.” ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Based on results with &lt;a href="https://fonts.google.com/specimen/Poppins" rel="noopener noreferrer"&gt;Poppins&lt;/a&gt; from an earlier design. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>css</category>
      <category>design</category>
    </item>
    <item>
      <title>The weirdly obscure art of Streamed HTML</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Tue, 15 Mar 2022 15:30:58 +0000</pubDate>
      <link>https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2</link>
      <guid>https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2</guid>
      <description>&lt;p&gt;My goal from last time: reuse our existing APIs in a demo of the fastest possible version of our ecommerce website… and keep it under 20 kilobytes.&lt;/p&gt;

&lt;p&gt;I decided this called for an MPA. (aka a traditional web app. Site. Thang. Not-SPA. Whatever.)&lt;/p&gt;

&lt;h2&gt;
  
  
  And with that decision, I doomed the site to feel slow and clunky
&lt;/h2&gt;

&lt;p&gt;In theory, there’s no reason MPA interactions need be as slow as commonly encountered. But in practice, there are &lt;em&gt;many&lt;/em&gt; reasons.&lt;/p&gt;

&lt;p&gt;Here’s an example. At Kroger, product searches take two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send user’s query to an API to get matching product codes&lt;/li&gt;
&lt;li&gt;Send those product codes to an API to get product names, prices, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using those APIs to generate a search results page would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultsData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usersSearchString&lt;/span&gt;
  &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;upcs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/products/details?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;upcs&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultsData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html;charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;htmlResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchPageTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;searchedQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usersSearchString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resultsData&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;htmlResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each fetch takes time, and &lt;code&gt;/api/products/details&lt;/code&gt; only happens &lt;em&gt;after&lt;/em&gt; &lt;code&gt;/api/search&lt;/code&gt; finishes. Plus, those requests traveled from my computer to the datacenter and back. A real server making those calls would &lt;a href="https://serverfault.com/questions/953169/what-is-the-latency-within-a-data-center-i-ask-this-assuming-there-are-orders-o" rel="noopener noreferrer"&gt;sit next to the others for &lt;em&gt;very&lt;/em&gt; fast requests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But on my demo machine, the calls usually took ~200 milliseconds, sometimes spiking as high as 800ms. Combined with the target 3G network, server processing time, and other inconvenient realities, I was frequently flouting &lt;a href="https://web.dev/rail/#focus-on-the-user" rel="noopener noreferrer"&gt;the 100–1000ms limit for “natural and continuous progression of tasks”&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  So the problem is high Time to First Byte, huh?
&lt;/h2&gt;

&lt;p&gt;No worries! High &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/time_to_first_byte" rel="noopener noreferrer"&gt;Time to First Byte (TTFB)&lt;/a&gt; is a known performance culprit. Browser devtools, Lighthouse, and other speed utensils all warn about it, so there’s lots of advice for fixing it!&lt;/p&gt;

&lt;p&gt;Except, none of the &lt;a href="https://web.dev/ttfb/#how-to-improve-ttfb" rel="noopener noreferrer"&gt;easily-found advice for improving TTFB&lt;/a&gt; helps:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Optimize the server application to prepare pages faster
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;Node.js spent 30ms or less handling the request and sending HTML. Very little to be gained there.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Optimize database queries or migrate to faster database systems
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;I was not allowed to touch our databases or API servers.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Upgrade server hardware to have more memory or CPU
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;This ran on a MacBook Pro with plenty of unused RAM and CPU.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Cache the expensive lookups
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;Caching can’t help the first requests, unless I precache all known endpoints or something. Even then, that wouldn’t work for search: users can and will search for strings never seen before.
&lt;/p&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;h2&gt;
  
  
  The problem: web performance is other people
&lt;/h2&gt;

&lt;p&gt;If only two API calls were a struggle, I was in for ruination. Here are some data sources our homepage uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication and user info&lt;/li&gt;
&lt;li&gt;Items in shopping cart&lt;/li&gt;
&lt;li&gt;Selected store, pickup vs. delivery, etc.&lt;/li&gt;
&lt;li&gt;Recommended products&lt;/li&gt;
&lt;li&gt;Products on sale&lt;/li&gt;
&lt;li&gt;Previous purchases&lt;/li&gt;
&lt;li&gt;Sponsored products&lt;/li&gt;
&lt;li&gt;Location-specific promotions&lt;/li&gt;
&lt;li&gt;Recommended coupons&lt;/li&gt;
&lt;li&gt;A/B tests&lt;/li&gt;
&lt;li&gt;Subscription to Kroger Boost&lt;/li&gt;
&lt;li&gt;…and so on. You get it, there’s a lot — and that’s only the stuff you can &lt;em&gt;see&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like many large companies, each data source may be owned by different teams. With their own schedules, SLAs, and bugs.&lt;/p&gt;


&lt;img alt="A unamused man in front of a whiteboard, upon which scads of shapes with silly labels like “Magic Baby” and “Hell Proxy”, festooned by arrows pointing every which way." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjes7dwinkjn6qf7qzebi.jpg" width="500" height="320"&gt;&lt;p&gt;After you see real API charts, &lt;a href="https://www.youtube.com/watch?v=y8OnoxKotPQ" rel="noopener noreferrer"&gt;Krazam’s satirical microservices diagram&lt;/a&gt; gets either more or less funny. Still figuring out which.&lt;/p&gt;




&lt;p&gt;Let’s say the 10 data sources I listed are each one API call. What are the odds my server can respond quickly enough?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let’s say 1 client request creates 10 downstream requests to a longtail-latency affected subsystem. And assume it has a 1% probability of responding slowly to a single request. Then the probability that at least 1 of the 10 downstream requests are affected by the longtail latencies is equivalent to the complement of all downstream requests responding fast (99% probability of responding fast to any single request) which is:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;1−(0.99)10=0.095
1 - (0.99)^{10} = 0.095
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0.99&lt;/span&gt;&lt;span class="mclose"&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.095&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;That’s 9.5 percent! This means that the 1 client request has an almost 10 percent chance of being affected by a slow response. That is equivalent to expecting 100,000 client requests being affected out of 1 million client requests. That’s a lot of members!&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://engineering.linkedin.com/performance/who-moved-my-99th-percentile-latency" rel="noopener noreferrer"&gt;Who moved my 99th percentile latency?&lt;/a&gt;&lt;/p&gt;


&lt;/blockquote&gt;

&lt;p&gt;And since users visit multiple pages in MPAs, the chances of suffering a high TTFB approaches “guaranteed”:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gil walks through a simple, hypothetical example: a typical user session involves five page loads, averaging 40 resources per page. How many users will &lt;em&gt;not&lt;/em&gt; experience something worse than the 95th percentile? 0.003%.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://bravenewgeek.com/everything-you-know-about-latency-is-wrong/" rel="noopener noreferrer"&gt;Everything You Know About Latency Is Wrong&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I believe this is why Kroger.com used a SPA in the first place — if disparate teams’ APIs can’t be trusted, at least they won’t affect other teams’ code. (Similar insulation from other teams’ components is probably one reason for React’s industry adoption.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: streamed HTML
&lt;/h2&gt;

&lt;p&gt;It’s easier to show than to explain:&lt;/p&gt;



&lt;a href="https://assets.codepen.io/183091/HTML+streaming+vs.+non.mp4" rel="noopener noreferrer"&gt;Download video: HTML Streaming vs. Non-streaming&lt;/a&gt;


&lt;p&gt;Both pages show search results in 2.5 seconds. But they sure don’t &lt;em&gt;feel&lt;/em&gt; the same.&lt;/p&gt;

&lt;p&gt;Not all sites have my API bottlenecking issue, but many have its cousins: database queries and reading files. &lt;strong&gt;Showing pieces of a page as data sources finish is useful for almost any dynamic site.&lt;/strong&gt; For example…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Showing the header before potentially-slow main content&lt;/li&gt;
&lt;li&gt;Showing main content before sidebars, related posts, comments, and other non-critical information&lt;/li&gt;
&lt;li&gt;Streaming paginated or batched queries as they progress instead of big expensive database queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond the obvious visual speedup, streamed HTML has other benefits:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Interactive ASAP
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;If a user visits the homepage and immediately tries searching, they don’t have to wait for anything but the header to submit their query.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Optimized asset delivery
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;Even with no &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; to show, you can stream the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. That lets browsers download and parse styles, scripts, and other assets while waiting for the rest of the HTML.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Less server effort
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;Streamed HTML uses less memory. Instead of building the full response in RAM, it sends generated bytes immediately.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;More robust and faster than incremental updates via JavaScript
&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;Fewer roundtrips, happens before/while JS boots, immune to JS errors and &lt;a href="https://adamsilver.io/blog/javascript-isnt-always-available-and-its-not-the-users-fault/" rel="noopener noreferrer"&gt;the other reasons 1% of visits have broken JavaScript&lt;/a&gt;…
&lt;/p&gt;
&lt;dd&gt;And because it’s more efficient, that leaves more CPU and RAM for the JavaScript we &lt;em&gt;do&lt;/em&gt; run, not to mention painting, layout, and user interactions.
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;But don’t take my word for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.phpied.com/progressive-rendering-via-multiple-flushes/" rel="noopener noreferrer"&gt;Progressive rendering via multiple flushes · Stoyan Stefanov&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jakearchibald.com/2016/fun-hacks-faster-content/" rel="noopener noreferrer"&gt;Fun hacks for faster content · Jake Archibald&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/the-thinkmill/progressive-rendering-the-key-to-faster-web-ebfbbece41a4" rel="noopener noreferrer"&gt;Progressive Rendering — The Key to Faster Web · Dinesh Pandiyan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jakearchibald.com/2016/streams-ftw/" rel="noopener noreferrer"&gt;Streams FTW · Jake Archibald&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hopefully, you see why I considered HTML streaming a must.&lt;/p&gt;

&lt;h2&gt;
  
  
  And that’s why not Svelte
&lt;/h2&gt;

&lt;p&gt;Previously…&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Maybe if I sprinkled the HTML with just enough CSS to look good… and if I had any room left, some laser-focused JavaScript for the pieces that benefit most from complex interactivity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s &lt;em&gt;exactly&lt;/em&gt; what Svelte excels at. So why didn’t I use it?&lt;/p&gt;

&lt;p&gt;Because &lt;a href="https://github.com/sveltejs/svelte/issues/958" rel="noopener noreferrer"&gt;Svelte does not stream HTML.&lt;/a&gt; (I hope it does someday.)&lt;/p&gt;

&lt;h2&gt;
  
  
  If not Svelte, then what?
&lt;/h2&gt;

&lt;p&gt;I found only 2 things on NPM that could stream HTML:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://www.dustjs.com/" rel="noopener noreferrer"&gt;Dust&lt;/a&gt;, a template language that seems to have died twice.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://markojs.com/" rel="noopener noreferrer"&gt;Marko&lt;/a&gt;, some library with an ungoogleable name and a rainbow logo… oh, and JSX-like syntax? And a client-side virtual DOM that fit in my budget? &lt;em&gt;And&lt;/em&gt; eBay has battle-tested it for &lt;em&gt;its&lt;/em&gt; ecommerce websites? &lt;strong&gt;&lt;em&gt;And&lt;/em&gt; it only uses client-side JS for &lt;code&gt;state&lt;/code&gt;ful components?&lt;/strong&gt; &lt;em&gt;You don’t say.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s nice when a decision makes itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://markojs.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kkg0zc4q52izorcb9hz.png" alt="And thus, Marko." width="400" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Marko’s &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt; made streaming easy
&lt;/h3&gt;

&lt;p&gt;Marko streams HTML with &lt;a href="https://markojs.com/docs/core-tags/#await" rel="noopener noreferrer"&gt;its &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt; tag&lt;/a&gt;. I was pleasantly surprised at how easily it could optimize browser rendering, with all the control I wanted over HTTP, HTML, and JavaScript.&lt;/p&gt;

&lt;p&gt;
  Disclaimer
  &lt;br&gt;
I now work for eBay, but I didn’t yet when I wrote this post. &lt;br&gt;


&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ssntpycuf50nda90o5r.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ssntpycuf50nda90o5r.gif" alt="Buffered pages don’t show content as it loads, but Marko’s streaming pages show content incrementally." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://markojs.com/#streaming" rel="noopener noreferrer"&gt;Source: markojs.com/#streaming&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;As seen in &lt;a href="https://dev.to/tigt/skeleton-screens-but-fast-48f1"&gt;&lt;cite&gt;Skeleton screens, but fast&lt;/cite&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;SiteHead&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Search for “${searchQuery}”&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div.SearchSkeletons&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;await&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;searchResultsFetch&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- stalls the HTML stream until the API returns search results --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;for&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="na"&gt;of=&lt;/span&gt;&lt;span class="s"&gt;result.products&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ProductCard&lt;/span&gt; &lt;span class="na"&gt;product=&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/for&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;then&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/await&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt; for nice-to-haves
&lt;/h3&gt;

&lt;p&gt;Imagine a component that displays recommended products. Fetching the recommendations is usually fast, but every once in a while, the API hiccups. &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt;’s got your back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;await&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;productRecommendations&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="na"&gt;timeout=&lt;/span&gt;&lt;span class="s"&gt;50&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- wait up to 50ms --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;recs&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RecommendedProductList&lt;/span&gt; &lt;span class="na"&gt;of=&lt;/span&gt;&lt;span class="s"&gt;recs&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;then&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;catch&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- don’t render anything; no big deal if this fails --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;catch&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/await&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you know how much money product recommendations make, you can fine-tune the &lt;code&gt;timeout&lt;/code&gt; so the cost of the performance hit never exceeds that revenue.&lt;/p&gt;

&lt;p&gt;And that’s not all!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;await&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;productRecommendations&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="na"&gt;client-reorder&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- immediately render placeholder to prevent content jumping around --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RecommendedProductsPlaceholder&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt; 
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;placeholder&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;recs&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RecommendedProductList&lt;/span&gt; &lt;span class="na"&gt;of=&lt;/span&gt;&lt;span class="s"&gt;recs&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;then&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/await&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;client-reorder&lt;/code&gt; attribute turns the &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt; into an HTML fragment that doesn’t delay the rest of the page behind it, but asynchronously renders when ready. &lt;code&gt;client-reorder&lt;/code&gt; requires JavaScript, so you can weigh the tradeoffs of using it vs. a &lt;code&gt;timeout&lt;/code&gt; with no fallback. (I think you can even combine them.)&lt;/p&gt;

&lt;p&gt;That’s &lt;a href="https://engineering.fb.com/2010/06/04/web/bigpipe-pipelining-web-pages-for-high-performance/" rel="noopener noreferrer"&gt;how Facebook’s BigPipe renderer worked&lt;/a&gt;, which once lived on the same page as React. Wouldn’t it be nice to have the best of both?&lt;/p&gt;

&lt;p&gt;Let me tell you: it &lt;em&gt;is&lt;/em&gt; nice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marko’s &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt; is awesome
&lt;/h3&gt;

&lt;p&gt;Best of all, these &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt; techniques are Marko’s golden path — heck, &lt;a href="https://tech.ebayinc.com/engineering/async-fragments-rediscovering-progressive-html-rendering-with-marko/" rel="noopener noreferrer"&gt;its very reason for being&lt;/a&gt;. Marko has stream control no other renderer makes easy, a way to automatically upgrade streamed HTML with JavaScript, and 8+ years of experience with the inevitable bugs and edge cases.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;(Yes, I was quite taken with Marko. Let me have my fun.)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;However, the fact that Marko was apparently my &lt;em&gt;one&lt;/em&gt; option does raise a certain question…&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is HTML streaming not common?
&lt;/h2&gt;

&lt;p&gt;Or in the words of another developer after my demo: &lt;strong&gt;“if Chunked &lt;code&gt;Transfer-Encoding&lt;/code&gt; is so useful, how come I’ve never heard of it?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a very fair question. It’s not because it’s poorly-supported — &lt;a href="https://web.archive.org/web/20040617070232/http://www.boutell.com/newfaq/history/fbrowser.html" rel="noopener noreferrer"&gt;HTML rendered progressively in Netscape 1.0. &lt;em&gt;Beta&lt;/em&gt; Netscape 1.0&lt;/a&gt;. And it’s not because the technique is barely-used — Google search results stream after the top navbar, for instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  I think one reason is the inconsistent name
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.stevesouders.com/blog/2009/05/18/flushing-the-document-early/" rel="noopener noreferrer"&gt;Steve Souders called it “early flushing”&lt;/a&gt;, which is not… the &lt;em&gt;best&lt;/em&gt; name.&lt;/li&gt;
&lt;li&gt;“Chunked transfer-encoding” is the most unique, but it’s only in HTTP/1.1. HTTP/2, HTTP/3, and &lt;a href="http://1997.webhistory.org/www.lists/www-talk.1994q3/1147.html" rel="noopener noreferrer"&gt;even HTTP/0.9 stream&lt;/a&gt; differently.&lt;/li&gt;
&lt;li&gt;It was known as “HTTP streaming” before HLS, DASH, and other forms of video-over-HTTP took up that mindspace. &lt;/li&gt;
&lt;li&gt;The catch-all term is “progressive rendering”, but that applies to many other things: interlaced images, visualizing large datsets, video game engine optimizations, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Many languages/frameworks don’t care for streaming
&lt;/h3&gt;

&lt;p&gt;Older languages/frameworks have long been able to stream HTML, but were never really &lt;em&gt;good&lt;/em&gt; at it. Some examples:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;PHP 🐘
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;Requires calling &lt;a href="https://www.php.net/manual/en/book.outcontrol.php" rel="noopener noreferrer"&gt;inscrutable output-buffering functions&lt;/a&gt; in a finicky order.

&lt;/p&gt;&lt;/dd&gt;
&lt;dt&gt;Ruby on Rails 🛤
&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;&lt;a href="https://api.rubyonrails.org/classes/ActionController/Streaming.html" rel="noopener noreferrer"&gt;&lt;code&gt;ActionController::Streaming&lt;/code&gt;&lt;/a&gt; has a lot of caveats. In particular:

&lt;/p&gt;
&lt;blockquote&gt;This approach was introduced in Rails 3.1 and is still improving. Several Rack middlewares may not work and you need to be careful when streaming. Those points are going to be addressed soon.&lt;/blockquote&gt;

&lt;p&gt;Rails hit 3.1 in 2011. There was clearly not much demand to address those points.

&lt;/p&gt;
&lt;p&gt;(Rails’ modern way is &lt;a href="https://turbo.hotwired.dev/handbook/streams" rel="noopener noreferrer"&gt;Turbo Streams&lt;/a&gt;, but those need JS to render, so not the same thing.)

&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;Django 🐍
&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;&lt;a href="https://docs.djangoproject.com/en/4.0/ref/request-response/#django.http.StreamingHttpResponse" rel="noopener noreferrer"&gt;Django really doesn’t like streaming at all&lt;/a&gt;:

&lt;/p&gt;
&lt;blockquote&gt;
&lt;code&gt;StreamingHttpResponse&lt;/code&gt; should only be used in situations where it is absolutely required that the whole content isn’t iterated before transferring the data to the client.&lt;/blockquote&gt;

&lt;/dd&gt;
&lt;dt&gt;Perl 🐪
&lt;/dt&gt;
&lt;dd&gt;&lt;p&gt;&lt;a href="https://www.perlmonks.org/?node_id=671400" rel="noopener noreferrer"&gt;Perl’s autostream behavior is controlled by a &lt;code&gt;$|&lt;/code&gt; variable&lt;/a&gt; (yes, that’s a pipe), but that sort of nonsense is normal for it. God I love Perl.

&lt;/p&gt;&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;Because streaming was never their default happy path, languages/frameworks considered it a last resort where you gained performance at the expense of the “real” render features. Here’s a telling quote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can still write ASP.NET pages that properly stream data to the browser using &lt;code&gt;Response.Write&lt;/code&gt; and &lt;code&gt;Response.Flush&lt;/code&gt;. But you can’t do it within the normal ASP.NET page lifecycle. Maybe this is a natural consequence of the ASP.NET abstraction layer.&lt;/p&gt;

&lt;p&gt;Regardless, it still sucks for users.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://blog.codinghorror.com/the-lost-art-of-progressive-html-rendering/" rel="noopener noreferrer"&gt;The Lost Art of Progressive HTML Rendering&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Node.js is a happy exception.&lt;/strong&gt; As proudly described on &lt;a href="https://nodejs.org/en/about/" rel="noopener noreferrer"&gt;Node’s About page&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;HTTP is a first-class citizen in Node.js, designed with streaming and low latency in mind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Despite that, the “new” hot JavaScript frameworks have been struggling to stream for a while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React’s had &lt;code&gt;renderToNodeStream&lt;/code&gt; since 2016, but &lt;a href="https://reactjs.org/docs/concurrent-mode-suspense.html" rel="noopener noreferrer"&gt;using it was tricky&lt;/a&gt;. Ergonomic streaming is the focus of a &lt;em&gt;lot&lt;/em&gt; of &lt;a href="https://github.com/reactwg/react-18/discussions/37" rel="noopener noreferrer"&gt;upcoming React SSR work&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Ember’s &lt;a href="https://blog.emberjs.com/glimmer-progress-report/#toc_server-side-rendering" rel="noopener noreferrer"&gt;Glimmer can stream HTML&lt;/a&gt;, but it’s been “experimental” since 2016, too.&lt;/li&gt;
&lt;li&gt;Vue &lt;em&gt;can&lt;/em&gt; stream, but &lt;a href="https://github.com/nuxt/nuxt.js/issues/4753" rel="noopener noreferrer"&gt;with caveats and incompatibilities since it’s not the default&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These frameworks have the staff, funding, and incentives to make streaming work, so the holdup must be something else. Maybe it’s hard to retrofit streaming onto their abstractions, especially without ruining established third-party integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming rarely mentioned as a TTFB fix
&lt;/h3&gt;

&lt;p&gt;As mentioned near the beginning, when high TTFB is detected, streaming is almost never suggested as a fix.&lt;/p&gt;

&lt;p&gt;I think &lt;em&gt;that’s&lt;/em&gt; the biggest problem. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest" rel="noopener noreferrer"&gt;A Web API with a bad name&lt;/a&gt; can become popular if it’s mentioned enough.&lt;/p&gt;

&lt;p&gt;Personally, I’ve only seen streaming HTML recommended for TTFB &lt;em&gt;once&lt;/em&gt;, and it’s &lt;a href="https://hpbn.co/primer-on-web-performance/#optimizing-time-to-first-byte-ttfb-for-google-search" rel="noopener noreferrer"&gt;in chapter 10 of High-Performance Browser Networking&lt;/a&gt;. In an aside. At the bottom.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;(Inside a &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; labeled &lt;a href="https://hitchhikersguidetothegalaxy.wordpress.com/2006/04/09/beware-of-the-leopard/" rel="noopener noreferrer"&gt;“Beware of The Leopard”&lt;/a&gt;.)&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So that’s one silver bullet down
&lt;/h2&gt;

&lt;p&gt;I had streaming HTML, but that was &lt;a href="https://a16z.com/2011/11/13/lead-bullets/" rel="noopener noreferrer"&gt;no substitute for the other 999 lead bullets to back it up&lt;/a&gt;. Now I had to… make the website.&lt;/p&gt;

&lt;p&gt;You know, write the components, style the design, build the features. How hard could &lt;em&gt;that&lt;/em&gt; be? (Hint: people are paid to do those things.)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>marko</category>
      <category>svelte</category>
    </item>
    <item>
      <title>Making the world’s fastest website, and other mistakes</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Tue, 15 Mar 2022 15:30:47 +0000</pubDate>
      <link>https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na</link>
      <guid>https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na</guid>
      <description>&lt;p&gt;This is a story about a lot of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fitting &lt;strong&gt;a Fortune 20 site in 20kB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diving into site speed&lt;/strong&gt; so deep we’ll see fangly fish&lt;/li&gt;
&lt;li&gt;React thwarting &lt;strong&gt;my goal of serving users as they are&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Burning out &lt;strong&gt;trying to do the right thing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;And by the end, some code I &lt;em&gt;dare&lt;/em&gt; you to try.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The situation: frustratingly typical
&lt;/h2&gt;

&lt;p&gt;I work on Kroger’s ecommerce sites for their regional chains, most of which share a codebase. You’d probably guess the front-end stack: React, Redux, and &lt;a href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/#javascript-main-thread-time" rel="noopener noreferrer"&gt;their usual symptoms of too much JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx8e9vma1m4cr94j5g1sr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx8e9vma1m4cr94j5g1sr.png" alt="The WebPageTest activity graph shows a dense forest of yellow representing JavaScript execution, with an majority of red underneath showing when interaction was impossible. The worst part? The x-axis is over 46 seconds." width="800" height="103"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://bundlephobia.com/scan-results?packages=react@16.8.6,react-dom@16.8.6,redux@4.1.2,react-redux@7.2.6,redux-thunk@2.4.1,redux-act@1.8.0" rel="noopener noreferrer"&gt;React/Redux packages used&lt;/a&gt; totaled 44.7 kB &lt;em&gt;before&lt;/em&gt; any feature code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.webpagetest.org/result/220608_BiDc6P_ATQ/" rel="noopener noreferrer"&gt;Our WebPageTest results&lt;/a&gt; spoke for themselves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This was &lt;em&gt;after&lt;/em&gt; investing in Server-Side Rendering (SSR), a performance team, and automated regression testing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In particular, &lt;a href="https://dev.to/this-is-learning/why-efficient-hydration-in-javascript-frameworks-is-so-challenging-1ca3"&gt;React SSR was one of those changes that &lt;em&gt;looks&lt;/em&gt; faster, but looks can be deceiving&lt;/a&gt;. In retrospect, I’m amazed developers &lt;a href="https://developers.google.com/web/updates/2019/02/rendering-on-the-web#rehydration" rel="noopener noreferrer"&gt;get away with considering SSR+rehydration an improvement&lt;/a&gt; at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make your code faster… by running it &lt;strong&gt;twice!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;—&lt;em&gt;how React SSR works, apparently&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The backstory: developer bitten by a radioactive WebPageTest
&lt;/h2&gt;

&lt;p&gt;I used to ask other developers to stop writing slow code.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; Such as…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Please cut down on the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s, they make our DOM big and slow.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Please avoid CSS like &lt;code&gt;.Component &amp;gt; * + *&lt;/code&gt;, it combines with our big DOM into noticeable lag.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Please don’t use React for everything, it caps how fast we can be.” (Especially if it renders big DOMs with complex styles…)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody listened. But, honestly, why would they?&lt;/p&gt;

&lt;p&gt;This carried on, and it was &lt;a href="https://www.youtube.com/embed/gNTtCGZOdtI?start=89&amp;amp;end=92&amp;amp;autoplay=1&amp;amp;rel=0" rel="noopener noreferrer"&gt;cool/cool/depressing/cool&lt;/a&gt;. But a new design system inflicted enough Tailwind to hurt desktop Time to First Paint by 0.5 seconds, and that was enough to negotiate for a dedicated Web Performance team.&lt;/p&gt;

&lt;p&gt;Which went well, until it didn’t. Behold, the industry-standard life of a speed optimization team:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Success with uncontroversial changes&lt;/strong&gt; like better build configuration, deduplicating libraries, and deleting dead code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditing other teams’ code&lt;/strong&gt; and suggesting improvements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doing the improvements ourselves&lt;/strong&gt; after said suggestions never escaped backlogs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trying to make the improvements stick&lt;/strong&gt; with bundle size monitoring, Lighthouse checks in PRs, and other new layers of process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hearing wailing and gnashing of teeth&lt;/strong&gt; about having to obey said layers of process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realizing we need to justify why we were annoying everyone else&lt;/strong&gt; before we were considered a net negative to the bottom line&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing was, WebPageTest frowning at our speed didn’t translate into bad mobile traffic — in fact, most users were on iPhone.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; From a business perspective, when graphs go up and to the right, who cares if the site could be faster?&lt;/p&gt;

&lt;p&gt;To prove we weren’t wasting everyone’s time, we used &lt;a href="https://wpostats.com/" rel="noopener noreferrer"&gt;WPO Stats&lt;/a&gt; and internal data to calculate that each kB of client-side JavaScript cost us ≈$100,000 per year, and every millisecond until Time to Interactive at least $40,000.&lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;But proving speed = money only moved us from Anger to the Bargaining stage of performance grief: hoarding improvements to use later, empty promises to fix massive regressions after a deadline, and protesting numbers with &lt;a href="https://twitter.com/Rich_Harris/status/1039847400062574596" rel="noopener noreferrer"&gt;appeals to “developer experience”&lt;/a&gt;.&lt;/p&gt;
The 5 Stages of Performance Grief



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;th&gt;Denial&lt;/th&gt;
&lt;td&gt;It’s fast enough. You’ve seen those M1 benchmarks, right?

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;Anger&lt;/th&gt;
&lt;td&gt;You mean I have to &lt;em&gt;care&lt;/em&gt; about this, too!? We just got done having to care about accessibility!

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;Bargaining&lt;/th&gt;
&lt;td&gt;I promise we will eventually consolidate on just three tooltip libraries if you let us skip the bundle check

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;Sadness&lt;/th&gt;
&lt;td&gt;I should have realized the dark path I was going down when I tried to see if &lt;code&gt;npm install *&lt;/code&gt; worked.

&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;Acceptance&lt;/th&gt;
&lt;td&gt;I love my slow website.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Proving that speed mattered wasn’t enough: we also had to convince people &lt;em&gt;emotionally&lt;/em&gt;. To &lt;em&gt;show&lt;/em&gt; everyone, &lt;strong&gt;&lt;em&gt;god dammit&lt;/em&gt;&lt;/strong&gt;, how much better our site would be if it were fast.&lt;/p&gt;

&lt;p&gt;So I decided to make a demo site that reused our APIs, but in a way that was as fast as possible.&lt;/p&gt;

&lt;p&gt;Spoiler: surprising myself, I succeeded. And then things got weird. But before I can tell you that story, I have to tell you &lt;em&gt;this&lt;/em&gt; story…&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal: how fast is possible?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;204&lt;/span&gt; &lt;span class="ne"&gt;No Content&lt;/span&gt;
&lt;span class="na"&gt;Cache-Control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max-age=999999999,immutable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the fastest web page. You may not like it, but this is what peak performance looks like.&lt;/p&gt;

&lt;p&gt;That may seem unhelpful — of course a useful page is slower than literally nothing! — but &lt;a href="https://css-tricks.com/add-less/" rel="noopener noreferrer"&gt;anything added to a frontend can only slow it down&lt;/a&gt;. The further something pushes you from the Web’s natural speed, the more work needed to claw it back.&lt;/p&gt;

&lt;p&gt;That said, some leeway is required, or I’d waste time micro-optimizing every little facet. You &lt;strong&gt;do&lt;/strong&gt; want to know when your content, design, or development choices start impacting your users. For everything added, you should balance its benefits with its costs. That’s &lt;a href="https://timkadlec.com/2014/01/fast-enough/" rel="noopener noreferrer"&gt;why performance budgets exist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But to figure out &lt;em&gt;my&lt;/em&gt; budget, I first needed some sort of higher-level goal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some sort of higher-level goal
&lt;/h3&gt;

&lt;p&gt;🎯 Be so fast it’s fun on the worst devices and networks our customers use.&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Target device: bestselling phone at a local Kroger
  &lt;/dt&gt;
&lt;dd&gt;Hot Pepper’s Poblano VLE5
  &lt;dd&gt;$35 ($15 on sale)
  &lt;dd&gt;
&lt;a href="https://whatismyphone.com/vendors/hot-pepper/poblano-vle5" rel="noopener noreferrer"&gt;Specs&lt;/a&gt;: 1 GB RAM, 8 GB total disk storage, and a 1.1 GHz processor.

&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;dt&gt;Target connection: “slow 3G”
  &lt;/dt&gt;
&lt;dd&gt;400kbps bandwidth
  &lt;dd&gt;400ms round-trip time latency
  &lt;dd&gt;At the time, what Google urged to test on and what WebPageTest’s “easy” configuration &amp;amp; Lighthouse used
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;Unfortunately, connections get worse than the “slow 3G” preset, and one example is cellular data &lt;em&gt;inside&lt;/em&gt; said Kroger. Big-box store architectures double as &lt;a href="https://en.wikipedia.org/wiki/Faraday_cage" rel="noopener noreferrer"&gt;Faraday cages&lt;/a&gt;, losing enough packets to sap bandwidth and latency.&lt;/p&gt;

&lt;p&gt;Ultimately, I went with “slow 3G” because it balanced the USA’s mostly-faster speeds with the signal interference inside stores. Alex Russell also mentioned “we still see latency like that in rural areas” when I had him fact-check this post.&lt;/p&gt;

&lt;p&gt;(These device and connection targets are highly specific to this project: I walked inside stores with a network analyzer, asked the front desk which phone was the most popular, etc. I would not consider them a “normal” baseline.)&lt;/p&gt;

&lt;p&gt;
  (Wait, don’t spotty connections mean you should reach for a Service Worker?)
  &lt;p&gt;Yes, &lt;a href="https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi" rel="noopener noreferrer"&gt;when networks are so bad you must treat them as optional&lt;/a&gt;, that’s a job for Service Workers.&lt;/p&gt;

&lt;p&gt;I &lt;em&gt;will&lt;/em&gt; write about special SW sauce (teaser: offline streams, navigation preload cache digests, and the frontier of critical CSS), but even the best service worker is irrelevant for a site’s &lt;em&gt;first&lt;/em&gt; load.&lt;/p&gt;





&lt;/p&gt;

&lt;p&gt;Although I knew what specs I was aiming for, I didn’t know what they meant for my budget. Luckily, someone else did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google’s suggestions to be fast on mobile
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.thinkwithgoogle.com/marketing-resources/data-measurement/mobile-page-speed-new-industry-benchmarks/" rel="noopener noreferrer"&gt;Google seems to know their way around web performance&lt;/a&gt;, but they never officially endorse a specific budget, since it can’t be one-size-fits-all. &lt;/p&gt;

&lt;p&gt;But while Google is cagey about an specific budget, Alex Russell — their former chief performance mugwump — &lt;em&gt;isn’t&lt;/em&gt;. He’s written vital information showing &lt;a href="https://elixirforum.com/t/will-low-end-mobile-phones-make-the-web-irrelevant/26218" rel="noopener noreferrer"&gt;how much the Web needs to speed up to stay relevant&lt;/a&gt;, and this post was exactly what I needed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Putting it all together, under ideal conditions, our rough budget for critical-path resources (CSS, JS, HTML, and data) at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;170KB&lt;/em&gt; for sites without much JS&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;130KB&lt;/em&gt; for sites built with JS frameworks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;—&lt;a href="https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/" rel="noopener noreferrer"&gt;&lt;cite&gt;Can You Afford It? Real-world Performance Budgets&lt;/cite&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;small&gt;(&lt;a href="https://infrequently.org/2021/03/the-performance-inequality-gap/" rel="noopener noreferrer"&gt;Alex has since updated these numbers&lt;/a&gt;, but they were the ones I used at the time. Please read both if you’re at all interested — Alex accounts for those worse-than-usual networks I mentioned, shows his work behind the numbers, and makes no bones about what &lt;em&gt;exactly&lt;/em&gt; slows down web pages.)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, the hardware Alex cited clocks 2GHz to the Poblano’s 1.1GHz. That means the budget &lt;em&gt;should&lt;/em&gt; lower to 100kB or so, but I couldn’t commit to that. Why?&lt;/p&gt;

&lt;h3&gt;
  
  
  Engineering around analytics
&lt;/h3&gt;

&lt;p&gt;As usual, &lt;a href="https://adactio.com/articles/18676" rel="noopener noreferrer"&gt;third-parties ruin everything&lt;/a&gt;. You can see &lt;a href="https://www.webpagetest.org/result/220127_BiDcZY_1567a4f929526931f1bc1e8f522edcef/2/domains/" rel="noopener noreferrer"&gt;the 2022 site’s cross-origin bytes situation&lt;/a&gt;, and it doesn’t include same-origin third-parties like Dynatrace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9pozq2cjz0v0yw1o8my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9pozq2cjz0v0yw1o8my.png" alt="Cross-section diagram of clowns packed into a car." width="576" height="310"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;small&gt;from &lt;a href="https://www.caranddriver.com/features/a15125743/the-physics-of-clown-cars-feature/" rel="noopener noreferrer"&gt;The Physics Of: Clown Cars · Car and Driver&lt;/a&gt;&lt;/small&gt;&lt;br&gt;&lt;br&gt;



&lt;p&gt;I can’t publish exact figures, but at the time it was scarcely better. Barring discovery of the anti-kilobyte, I needed to figure out which third-parties had to go. Sure, most of them made $, but I was out to show that dropping them could make $$$.&lt;/p&gt;

&lt;p&gt;After lots of rationalizing, I ended with ≈138kB of third-party JS I figured the business wouldn’t let me live without. Like &lt;a href="http://www.appleseeds.org/big-rocks_covey.htm" rel="noopener noreferrer"&gt;the story of filling a jar with rocks, pebbles, and sand&lt;/a&gt;, I figured engineering around those boulders would be easier than starting with a “fast enough” site and having it ruined later.&lt;/p&gt;

&lt;p&gt;Some desperate lazy-loading experiments later, I found my code couldn’t exceed 20kB (after compression) to heed Alex’s advice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Okay, 20kB. Now what?
&lt;/h3&gt;

&lt;p&gt;20 kilobytes ain’t much. &lt;a href="https://bundlephobia.com/scan-results?packages=react@16.8.6,react-dom@16.8.6" rel="noopener noreferrer"&gt;&lt;code&gt;react&lt;/code&gt; + &lt;code&gt;react-dom&lt;/code&gt; are nearly twice that.&lt;/a&gt; An obvious alternative is &lt;a href="https://bundlephobia.com/package/preact@10.6.5" rel="noopener noreferrer"&gt;the 4kB Preact&lt;/a&gt;, but that wouldn’t help the component code or the Redux disaster — and I still needed HTML and CSS! I had to look beyond the obvious choices.&lt;/p&gt;

&lt;p&gt;What does a website truly &lt;em&gt;need?&lt;/em&gt; If I answered that, I could omit everything else.&lt;/p&gt;

&lt;p&gt;Well, what can’t a website omit, even if you tried?&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; make a real site with only HTML — people did it all the time, before CSS and JS existed.&lt;/p&gt;

&lt;p&gt;Maybe if I sprinkled the HTML with &lt;em&gt;just enough&lt;/em&gt; CSS to look good… and if I had any room left, some laser-focused JavaScript for the pieces that benefit most from complex interactivity.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;(Yes, I see you with the &lt;a href="https://svelte.dev/" rel="noopener noreferrer"&gt;Svelte.js&lt;/a&gt; shirt in the back. I talk about it in the next post.)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Amazon serves basically what I just described if you visit with a &lt;em&gt;really&lt;/em&gt; bad User-Agent:&lt;/p&gt;
&lt;p&gt;What Amazon shows for &lt;code&gt;Opera/9.80 (J2ME/MIDP; Opera Mini/5.1.21214/28.2725; U; ru) Presto/2.8.119 Version/11.10&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pzabj3gcnqokgqqpctc.png" class="article-body-image-wrapper"&gt;&lt;img alt="Amazon.com as viewed by Opera Mini: one product on screen, navigation at bottom, and a site header stripped down to a logo, links to deals/cart/lists, and a searchbar." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pzabj3gcnqokgqqpctc.png" width="320" height="480"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;And this is the lowest-fidelity version of Amazon, which appears with a &lt;code&gt;skin=noskin&lt;/code&gt; cookie.&lt;/p&gt;
&lt;img alt="A product page for two mason jars in glorious Web 1.0 style. Times New Roman on a blank backdrop, and the only formatting is centering the image and Add buttons and some horizontal rules." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljiq92q0dmvtt07p702t.png" width="320" height="858"&gt;




&lt;p&gt;So my plan seemed &lt;em&gt;possible&lt;/em&gt;, and apparently profitable enough that Amazon does it. Seemed good enough to try.&lt;/p&gt;

&lt;h3&gt;
  
  
  But everyone knows classic page navigation is slow!
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://gomakethings.com/how-to-make-mpas-that-are-as-fast-as-spas/" rel="noopener noreferrer"&gt;Are you sure about that?&lt;/a&gt; The way I figured…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you inline CSS and generate HTML efficiently, their overhead is negligible compared to the network round-trip.&lt;/li&gt;
&lt;li&gt;A SPA still requests JSON data to render, yeah? Even if you inline that JSON into the initial response, JSON→JavaScript→HTML cannot possibly be faster than skipping straight to the HTML part.&lt;/li&gt;
&lt;li&gt;Concatenating strings on a server should not be a huge bottleneck. And if it were, how does React SSR justify concatenating those strings &lt;em&gt;twice&lt;/em&gt; into both HTML and hydration data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But don’t take my word for it — we’ll find out how that stacks up next time. In particular, I first need to solve a problem: &lt;em&gt;how do you send a page before all its slow data sources finish?&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;I still ask other developers to stop writing slow code, &lt;a href="https://en.wikiquote.org/wiki/Mitch_Hedberg#Strategic_Grill_Locations" rel="noopener noreferrer"&gt;but I used to, too&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;That does not count as insider information. Any US website with a similar front-end payload will tell you the same. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Those numbers were &lt;em&gt;very&lt;/em&gt; loose, conservative estimates. They’re also no longer accurate — they’re much higher now — but they still work as a bare minimum. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
      <category>react</category>
    </item>
    <item>
      <title>That Dang Material Design Spinner in One Element</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Sat, 12 Feb 2022 21:11:20 +0000</pubDate>
      <link>https://dev.to/tigt/that-dang-material-design-spinner-in-one-element-3b3n</link>
      <guid>https://dev.to/tigt/that-dang-material-design-spinner-in-one-element-3b3n</guid>
      <description>&lt;p&gt;You know the one, looks like this:&lt;/p&gt;

&lt;p&gt;
&lt;a href="https://material.io/components/progress-indicators#circular-progress-indicators" rel="noopener noreferrer"&gt;&lt;img alt="A thick colored circle outline with ¼ missing. If animated, it’d spin counter-clockwise and the missing segment would grow and shrink." src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fa5jdechf7ifrio3qh00s.png" width="176" height="176"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;a href="https://material.io/components/progress-indicators#circular-progress-indicators" rel="noopener noreferrer"&gt;material.io/components/progress-indicators&lt;b&gt;#circular-progress-indicators&lt;/b&gt;&lt;/a&gt;

&lt;p&gt;I was going to show it animated, but dev.to removes the &lt;code&gt;controls&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; so you can’t pause it, and that’s terrible.&lt;/p&gt;




&lt;p&gt;I want the markup to be as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;progress&amp;gt;&amp;lt;/progress&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…because &lt;a href="https://codepen.io/tigt/post/semantic-single-element-loading-spinners-with-html-css" rel="noopener noreferrer"&gt;that’s the right tool for the job, dangit&lt;/a&gt;. It’s easy, accessible, and semantic.&lt;/p&gt;

&lt;p&gt;CSS is powerful enough to &lt;a href="https://codepen.io/tigt/pen/ddXgoX?editors=1100" rel="noopener noreferrer"&gt;style &lt;code&gt;&amp;lt;progress&amp;gt;&lt;/code&gt; into all sorts of fancy loading indicators&lt;/a&gt;, so it &lt;em&gt;should&lt;/em&gt; also be able to animate Google’s funny looping circle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing implementations
&lt;/h2&gt;

&lt;p&gt;The first thing I did was look for code to steal. You know, like a developer does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Material Design Lite
&lt;/h3&gt;

&lt;p&gt;Surely Google has its own spinner for its sites, right? Who better to rip off than the inventor?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://getmdl.io/components/index.html#loading-section/spinner" rel="noopener noreferrer"&gt;Material Design Lite’s Spinner component&lt;/a&gt;… was the worst. But, sadly, also the most official. I expected an abundance of &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s because &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fyokbmigzviyne7zkhcvd.png" class="article-body-image-wrapper"&gt;&lt;img alt="“modern web development”" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fyokbmigzviyne7zkhcvd.png" width="800" height="400"&gt;&lt;/a&gt; but not &lt;em&gt;this&lt;/em&gt; many:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner mdl-spinner--single-color mdl-js-spinner is-active is-upgraded"&lt;/span&gt;
  &lt;span class="na"&gt;data-upgraded=&lt;/span&gt;&lt;span class="s"&gt;",MaterialSpinner"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__layer mdl-spinner__layer-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__gap-patch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__layer mdl-spinner__layer-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__gap-patch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__layer mdl-spinner__layer-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__gap-patch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__layer mdl-spinner__layer-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__gap-patch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle-clipper mdl-spinner__right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mdl-spinner__circle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s 29 — &lt;em&gt;count ‘em, &lt;strong&gt;twenty-nine&lt;/strong&gt;&lt;/em&gt; — &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;I was able to steal some variables like animation timings from this implementation, but mostly it made me wonder how Google can bang the performance drum and also suggest we use code like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.webcomponents.org/element/@polymer/paper-spinner/demo/demo/index.html" rel="noopener noreferrer"&gt;Polymer’s &lt;code&gt;&amp;lt;paper-spinner-lite&amp;gt;&lt;/code&gt; gets it done with only 7 elements&lt;/a&gt; &lt;em&gt;and&lt;/em&gt; allows customization, so I don’t think the inherent design is something inexpressible in CSS. Its &lt;code&gt;&amp;lt;paper-spinner&amp;gt;&lt;/code&gt; is still 22 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s, though, and &lt;a href="https://github.com/PolymerElements/paper-spinner/blob/43e9a23634c629278329a0d3666ad447232c2f1d/paper-spinner.js#L28-L30" rel="noopener noreferrer"&gt;its only difference is it doesn’t cycle through colors&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The default spinner cycles between four layers of colors; by default they are blue, red, yellow and green. It can be customized to cycle between four different colors. Use &lt;code&gt;&amp;lt;paper-spinner-lite&amp;gt;&lt;/code&gt; for single color spinners.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To be fair, MDL/Polymer’s spinners probably had some requirements I don’t:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They date from at least 2015, so the techniques I use may have had spottier browser support back then. (In particular, it &lt;a href="https://github.com/PolymerElements/paper-spinner/pull/89" rel="noopener noreferrer"&gt;looks like Safari did not animate &lt;code&gt;::after&lt;/code&gt; pseudo-elements inside shadow roots&lt;/a&gt;.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Google probably had to match their official design specs &lt;em&gt;perfectly&lt;/em&gt; — which I don’t much care to. As far as I’m concerned, if my users stare at a spinner long enough to notice animation inconsistency, the page has a bigger problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I think the reason the 4-color version has quadruple the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s is because they animate &lt;code&gt;opacity&lt;/code&gt; for hardware-accelerated color changes, because &lt;a href="https://csstriggers.com/" rel="noopener noreferrer"&gt;changing colors on the Web triggers paint invalidation&lt;/a&gt;, which can hiccup animations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SVG?
&lt;/h3&gt;

&lt;p&gt;Okay, so the official implementations were out. &lt;a href="https://codepen.io/search/pens?q=material+spinner" rel="noopener noreferrer"&gt;The next best place to steal front-end code? CodePen.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And thanks to &lt;a href="https://codepen.io/mrrocks/details/EiplA" rel="noopener noreferrer"&gt;Fran Pérez's Material Design Spinner pen&lt;/a&gt;, I had a starting point that I forked and rewrote to fit my harebrained sensibilities. I then embedded the SVG as a &lt;code&gt;data:&lt;/code&gt; URI, in order to style my lone &lt;code&gt;&amp;lt;progress&amp;gt;&lt;/code&gt; element with it:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/tigt/embed/PLWyZL?height=600&amp;amp;default-tab=css,result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I almost went with this, but it had problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Annoying to update — tweaking the code involves decoding the URI, understanding the SVG, then re-encoding to a &lt;a href="https://www.npmjs.com/package/mini-svg-data-uri" rel="noopener noreferrer"&gt;mini SVG data URI&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Risks &lt;a href="https://oreillymedia.github.io/Using_SVG/extras/ch19-performance.html#animation-performance-properties-section" rel="noopener noreferrer"&gt;desyncs and sputtery animation in browsers that don’t hardware-accelerate SVG animations&lt;/a&gt; — which used to be everything but Firefox and IE, but thankfully I believe Chrome might have that fix in the works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nothing shows if High-Contrast Mode is on or images are turned off, because it’s a &lt;code&gt;background-image&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;clip-path&lt;/code&gt; one I ultimately riffed on
&lt;/h3&gt;

&lt;p&gt;I then found &lt;a href="https://codepen.io/acronamy/pen/bNRJyP" rel="noopener noreferrer"&gt;a spinner by Adam “acronamy” Crockett&lt;/a&gt; which used &lt;code&gt;clip-path&lt;/code&gt;. It didn’t &lt;em&gt;quite&lt;/em&gt; match the shape and timing needed, but it showed it could be done with only one element and a single &lt;code&gt;@keyframes&lt;/code&gt; rule:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/acronamy/embed/bNRJyP?height=600&amp;amp;default-tab=css,result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Final result
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/tigt/embed/JjXjKEP?height=600&amp;amp;default-tab=css,result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tweak the thickness by changing the &lt;code&gt;border-width&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tweak the color with the &lt;code&gt;color&lt;/code&gt; property. You can even animate color changes that way!&lt;/li&gt;
&lt;li&gt;Tweak the size with the &lt;code&gt;font-size&lt;/code&gt; property&lt;/li&gt;
&lt;li&gt;Builtin accessible name/caption via &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: backdrop circle that matches the top one&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  But should you use it?
&lt;/h3&gt;

&lt;p&gt;Not if what you’re loading behind the spinner runs on the main thread, like React or another JS-heavy thing. If it’s only a &lt;code&gt;fetch&lt;/code&gt; that inserts some HTML, like &lt;a href="https://turbo.hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire’s Turbo&lt;/a&gt;, then yeah, go ahead.&lt;/p&gt;

&lt;p&gt;Since this was originally code I wrote for work almost three years ago, I thought I wouldn’t need this section anymore, because in the meantime &lt;a href="https://developer.chrome.com/blog/hardware-accelerated-animations/" rel="noopener noreferrer"&gt;Chrome shipped hardware acceleration for &lt;code&gt;clip-path&lt;/code&gt;&lt;/a&gt;! 🎉&lt;/p&gt;

&lt;p&gt;Unfortunately, the world isn’t just Chrome. If the animated &lt;code&gt;clip-path&lt;/code&gt; (and &lt;code&gt;color&lt;/code&gt; if you want the fancy color-shifting one too) run on the main thread, you can run into a heap of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any JS work/re-layouts/etc. can make the animation stutter and hitch.&lt;/li&gt;
&lt;li&gt;If it can’t run on &lt;a href="https://developers.google.com/web/fundamentals/design-and-ux/animations/animations-and-performance#css_vs_javascript_performance" rel="noopener noreferrer"&gt;the accelerated compositor thread&lt;/a&gt;, it chews up more battery life.&lt;/li&gt;
&lt;li&gt;Since spinners show while the main thread is busy with whatever you needed a loading animation for, a spinner running on the main thread delays whatever’s behind it!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So be careful, and make sure you’re being responsible to your users before reusing code from some dork’s blog post. You know what’s better than a spinner? Showing content faster.&lt;/p&gt;

&lt;p&gt;(Was it irresponsible of me to publish this? Yes, probably.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Failing to math for “fun” and “profit”
&lt;/h3&gt;

&lt;p&gt;I may have gotten a 4 in AP Calculus, but I managed to forget all of it because I couldn’t even express a circular rotation animation the “right” way. Ultimately, I hacked it by dividing the element into quadrants, and sweeping a side of each &lt;code&gt;polygon()&lt;/code&gt; as part of the animation.&lt;/p&gt;

&lt;p&gt;That probably didn’t make any sense, did it? Here’s the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// (x,y) points expressed in %, for use in CSS’s `clip-path`&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;150%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;150%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;// These four are used once each for the final “sliver” in the 4 different orientations&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bottomLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-50%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bottomRight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;150%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topRight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;150%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;// Edit these if you want to change the animation.&lt;/span&gt;
&lt;span class="c1"&gt;// It’s a list of `clip-path` coordinates that the animation uses as keyframes.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyFrames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;topLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottomLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottomLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottomLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottomRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottomRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottomRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;topRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cssText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/* &amp;lt;generated-keyframes&amp;gt; */
@keyframes inchworm {
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keyFrames&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLastFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;keyFrames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;keyFrames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toPercent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;isLastFrame&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0%,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; { clip-path: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;polygon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; }`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
}
/* &amp;lt;/generated-keyframes&amp;gt; */`&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;polygon&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cssPoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`polygon(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cssPoints&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;);`&lt;/span&gt; &lt;span class="c1"&gt;// final point is always the center&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toPercent&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;num&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decimalPlaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decimalPlaces&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that didn’t make sense to you either, well… yeah, me too. It’s been a while since I wrote it.&lt;/p&gt;

&lt;p&gt;(I tried writing that in Sass, but couldn’t manage it.)&lt;/p&gt;

</description>
      <category>css</category>
      <category>html</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Skeleton screens, but fast</title>
      <dc:creator>Taylor Hunt</dc:creator>
      <pubDate>Wed, 28 Apr 2021 17:18:05 +0000</pubDate>
      <link>https://dev.to/tigt/skeleton-screens-but-fast-48f1</link>
      <guid>https://dev.to/tigt/skeleton-screens-but-fast-48f1</guid>
      <description>&lt;p&gt;Here’s a fun HTTP+HTML+CSS technique for skeleton screens that works in almost* any stack, and some small but important details we need to do right by it.&lt;/p&gt;

&lt;p&gt;Most importantly, it involves &lt;strong&gt;no client-side JavaScript&lt;/strong&gt;, because adding JS to make a website feel faster usually is counter-productive. In fact, Zach Leatherman inspired this post by saying:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1352314419343052800-605" src="https://platform.twitter.com/embed/Tweet.html?id=1352314419343052800"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1352314419343052800-605');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1352314419343052800&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1352317811285237761-845" src="https://platform.twitter.com/embed/Tweet.html?id=1352317811285237761"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1352317811285237761-845');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1352317811285237761&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;&lt;small&gt;* The newer isomorphic ones like React struggle mightily to stream over HTTP, with one exception — I’ll get to it later.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Skeleton screens?
&lt;/h2&gt;

&lt;p&gt;Or indicators/placeholders/whatever. The “new” design hotness for when computers aren’t ready to show you something: skeleton screens!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fi21eavef7r8q6ml10nm3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fi21eavef7r8q6ml10nm3.jpg" alt="Screens depicting skeletons?" width="700" height="657"&gt;&lt;/a&gt;&lt;/p&gt;
No, not nearly that entertaining.



&lt;p&gt;Instead of a spinner or progress bar, show something &lt;em&gt;shaped&lt;/em&gt; like the eventual content — it orients the user faster, hints at what to expect, and avoids the page jumping around as it loads:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Finjgls3xysadxii4p5r0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Finjgls3xysadxii4p5r0.png" alt="The Polar app used a skeleton screen for the name, image, and profile info of its user details page." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;a href="https://www.lukew.com/ff/entry.asp?1797" rel="noopener noreferrer"&gt;The loading process of Polar&lt;/a&gt;, one of the first apps to popularize the concept of skeleton screens.



&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;We can’t avoid the time it takes to call a search results API — we can cache its responses, but how can you cache all possible search queries ahead of time?&lt;/p&gt;

&lt;p&gt;Here’s what these search skeletons look like with an artificial search API response delay of 5 seconds:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/503580605" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;And here’s some code for how they work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;SiteHead&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Search for “${searchQuery}”&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div.SearchSkeletons&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;await&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;searchResultsFetch&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- stalls the HTML stream until the API returns search results --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;for&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="na"&gt;of=&lt;/span&gt;&lt;span class="s"&gt;result.products&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ProductCard&lt;/span&gt; &lt;span class="na"&gt;product=&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/for&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;then&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/await&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is that “one exception” I mentioned earlier. &lt;a href="https://markojs.com/" rel="noopener noreferrer"&gt;Marko&lt;/a&gt; is a JS component framework similar to React, but is actually good at server-side rendering — in particular, built-in support for HTTP streaming. (And last I checked, it’s nearly the only thing in Node that does. RIP Dust)&lt;/p&gt;

&lt;p&gt;If you’re more familiar with other languages/frameworks, here’s how they accomplish something similar to &lt;a href="https://markojs.com/docs/core-tags/#await" rel="noopener noreferrer"&gt;Marko’s &lt;code&gt;&amp;lt;await&amp;gt;&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;&lt;b&gt;PHP&lt;/b&gt;&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://www.php.net/manual/en/function.flush.php" rel="noopener noreferrer"&gt;&lt;code&gt;flush()&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.php.net/manual/en/function.ob-flush.php" rel="noopener noreferrer"&gt;&lt;code&gt;ob_flush()&lt;/code&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;dt&gt;&lt;b&gt;Ruby on Rails&lt;/b&gt;&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://api.rubyonrails.org/classes/ActionController/Streaming.html" rel="noopener noreferrer"&gt;&lt;code&gt;ActionController::Streaming&lt;/code&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;dt&gt;&lt;b&gt;Spring&lt;/b&gt;&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.html" rel="noopener noreferrer"&gt;&lt;code&gt;StreamingResponseBody&lt;/code&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;dt&gt;&lt;b&gt;ASP.net&lt;/b&gt;&lt;/dt&gt;
&lt;dd&gt;I recommend searching for ASP’s &lt;code&gt;.BufferOutput&lt;/code&gt; and &lt;code&gt;.Flush()&lt;/code&gt; yourself, because it’ll also turn up results &lt;a href="https://blog.maartenballiauw.be/post/2018/06/14/how-http-chunked-encoding-was-killing-a-request.html" rel="noopener noreferrer"&gt;warning about possible footguns&lt;/a&gt;.&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;dt&gt;&lt;b&gt;Django&lt;/b&gt;&lt;/dt&gt;
&lt;dd&gt;
&lt;a href="https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.StreamingHttpResponse" rel="noopener noreferrer"&gt;There’s a &lt;code&gt;StreamingResponseBody&lt;/code&gt;&lt;/a&gt;, but Django really doesn’t care for it. You may need to get creative.&lt;br&gt;&lt;br&gt;

&lt;/dd&gt;
&lt;dt&gt;&lt;b&gt;Others not listed here&lt;/b&gt;&lt;/dt&gt;
&lt;dd&gt;Try searching for them plus “http stream” or “chunked transfer-encoding”.&lt;br&gt;&lt;br&gt;
&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;By not waiting on search results before sending HTML, browsers get a head start downloading assets, booting JS, calculating styles, and showing the &lt;code&gt;&amp;lt;SiteHeader&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt;&lt;span class="nd"&gt;:empty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;110vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Skeletons take up at least the full viewport */&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="c"&gt;/* Assume this is an image of the skeletons for now */&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;/* This is the faded white bar that scrubs across the skeletons */&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;10%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shimmer&lt;/span&gt; &lt;span class="m"&gt;2.5s&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;shimmer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-100%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:empty" rel="noopener noreferrer"&gt;The &lt;code&gt;:empty&lt;/code&gt; pseudo-class&lt;/a&gt; is the key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;While waiting for the search API, the opening &lt;code&gt;&amp;lt;div class="SearchSkeletons"&amp;gt;&lt;/code&gt; is streamed to browsers, without children or a closing tag.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;:empty&lt;/code&gt; only selects elements without children, such as the aforementioned &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As soon as the HTML resumes streaming and fills &lt;code&gt;.SearchSkeletons&lt;/code&gt; with results, &lt;code&gt;:empty&lt;/code&gt; no longer applies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The skeleton styles disappear at the same time the &lt;code&gt;&amp;lt;ProductCard&amp;gt;&lt;/code&gt; components display, reanimating the product skeletons into real products.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A nice thing about this approach is that if the search endpoint responds quickly, &lt;code&gt;:empty&lt;/code&gt; &lt;em&gt;never&lt;/em&gt; matches and browsers waste no resources styling or displaying the product skeletons.&lt;/p&gt;
&lt;h2&gt;
  
  
  Avoiding style recalculation
&lt;/h2&gt;

&lt;p&gt;Do we need &lt;code&gt;:empty&lt;/code&gt;? Couldn’t this also work?&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;SiteHead&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Search for “${searchQuery}”&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;…&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div.SearchSkeletons&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;await&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;searchResultsFetch&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;for&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="na"&gt;of=&lt;/span&gt;&lt;span class="s"&gt;result.products&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ProductCard&lt;/span&gt; &lt;span class="na"&gt;product=&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/for&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nt"&gt;then&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/await&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Yes, that does work. But it’s slower: appending new CSS to a document triggers &lt;a href="https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations" rel="noopener noreferrer"&gt;“style recalc”&lt;/a&gt;, where browsers update their selector buckets, invalidate and re-match elements, etc.&lt;/p&gt;

&lt;p&gt;We can’t avoid browsers performing &lt;a href="https://developers.google.com/speed/docs/insights/browser-reflow" rel="noopener noreferrer"&gt;&lt;em&gt;reflow&lt;/em&gt;&lt;/a&gt;, as that always happens when new HTML streams in. But by avoiding additional style recalc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browsers show the new HTML sooner&lt;/li&gt;
&lt;li&gt;User interaction doesn’t hitch as much&lt;/li&gt;
&lt;li&gt;There’s more CPU time left over to run JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using &lt;code&gt;:empty&lt;/code&gt; vs. additional &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; elements is a subtle decision, but it impacts user experience just the same.&lt;/p&gt;

&lt;p&gt;Hopefully, this illustrates why a strong understanding of HTML and CSS is important for making a site fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hardware-accelerated animation or bust
&lt;/h2&gt;

&lt;p&gt;And if &lt;em&gt;that&lt;/em&gt; didn’t illustrate why a strong understanding of HTML and CSS is important for making a site fast, &lt;strong&gt;this sure as hell will&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A predefined &lt;code&gt;@keyframes&lt;/code&gt; that only changes the &lt;code&gt;transform&lt;/code&gt; property is one way to &lt;a href="https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/" rel="noopener noreferrer"&gt;ensure that an animation is hardware-accelerated on the GPU&lt;/a&gt;. That means it frees up the CPU for all the other responsibilities of the main thread: parsing, JavaScript, user interaction, reflow…&lt;/p&gt;

&lt;p&gt;Skeleton animations that run on the main thread have a &lt;em&gt;raft&lt;/em&gt; of complications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The shimmer animation will hiccup and stall whenever JavaScript executes, the document reflows, style recalculates, or JSON is parsed.&lt;/li&gt;
&lt;li&gt;The time the CPU spends running the animation makes the above tasks take longer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The loading indicator delays the content it’s a placeholder for!&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At my job, I changed a similar loading animation from using &lt;code&gt;background-position&lt;/code&gt; to &lt;code&gt;transform&lt;/code&gt;. The page FPS went from 49 to 55 on a powerful developer MacBook — imagine how much more on mobile!&lt;/p&gt;
&lt;h2&gt;
  
  
  But wait, there’s more!
&lt;/h2&gt;

&lt;p&gt;Remember this from the earlier code sample?&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;background-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="c"&gt;/* Assume this is an image of the skeletons for now */&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The TL;DR is the background image is made of CSS gradients and so that the skeleton is shown ASAP. It makes no sense to have your loading indicator wait on an HTTP request, does it?&lt;/p&gt;

&lt;p&gt;I implemented the background images with Sass variables to prevent the skeletons from drifting out of sync with the product cards if any changes were made. For example, if I tweaked the padding of the actual product cards, the following code would also update the spacing of the skeletons:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nv"&gt;$skeleton-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#dfe1e1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$card-padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$card-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="mi"&gt;.125rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$img-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$img-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;45%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$img-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;right&lt;/span&gt; &lt;span class="nv"&gt;$card-padding&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$img-skeleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;15%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="m"&gt;15%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$img-height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;15%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transparent&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$img-height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;15%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$name-line-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.844rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-1-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-1-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$card-padding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-1-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$card-padding&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-1-skeleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="nv"&gt;$name-line-1-offset&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="nv"&gt;$name-line-1-offset&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name-line-1-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$name-line-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transparent&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name-line-1-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$name-line-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-2-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-2-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$card-padding&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$name-line-size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-2-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$card-padding&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$name-line-2-skeleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="nv"&gt;$name-line-2-offset&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="nv"&gt;$name-line-2-offset&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name-line-2-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$name-line-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transparent&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name-line-2-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$name-line-size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$price-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$price-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$price-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$name-line-2-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="mi"&gt;.3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$price-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$card-padding&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$price-skeleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="nv"&gt;$price-offset&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="nv"&gt;$price-offset&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;$skeleton-color&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$price-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$price-height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transparent&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$price-offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$price-height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt;&lt;span class="nd"&gt;:empty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat-y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;$img-skeleton&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$name-line-1-skeleton&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$name-line-2-skeleton&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$price-skeleton&lt;/span&gt;
  &lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;$img-width&lt;/span&gt; &lt;span class="nv"&gt;$card-height&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$name-line-1-width&lt;/span&gt; &lt;span class="nv"&gt;$card-height&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$name-line-2-width&lt;/span&gt; &lt;span class="nv"&gt;$card-height&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$price-width&lt;/span&gt; &lt;span class="nv"&gt;$card-height&lt;/span&gt;
  &lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nl"&gt;background-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nv"&gt;$img-position&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$name-line-1-position&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$name-line-2-position&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$price-position&lt;/span&gt;
  &lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30rem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="na"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto-fill&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20rem&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.75fr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="na"&gt;grid-gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;:empty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* TODO show how to use `background-repeat-x: round` to make skeletons responsive */&lt;/span&gt;
      &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here’s what that Sass compiles to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.SearchSkeletons&lt;/span&gt;&lt;span class="nd"&gt;:empty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat-y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;15%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;85%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;1.344rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;1.544rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;2.388rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;2.844rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#dfe1e1&lt;/span&gt; &lt;span class="m"&gt;4.344rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="m"&gt;45%&lt;/span&gt; &lt;span class="m"&gt;8.125rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;13ch&lt;/span&gt; &lt;span class="m"&gt;8.125rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;10ch&lt;/span&gt; &lt;span class="m"&gt;8.125rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;4ch&lt;/span&gt; &lt;span class="m"&gt;8.125rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;right&lt;/span&gt; &lt;span class="m"&gt;.5rem&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;.5rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;.5rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;.5rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I was going to finish this post with how to make these mobile-first styles responsive using &lt;code&gt;background-repeat&lt;/code&gt;, but it was making me put off publishing this altogether, &lt;em&gt;and that’s terrible&lt;/em&gt;. If you’re interested, let me know and I’ll write a followup.&lt;/p&gt;
&lt;h2&gt;
  
  
  Update
&lt;/h2&gt;

&lt;p&gt;I haven’t written that followup yet, but &lt;a href="https://twitter.com/scurker/status/1387546619046989829" rel="noopener noreferrer"&gt;Jason “scurker” shared that the AXE browser extension does this too&lt;/a&gt;, and was able to share some code for how they do repeating skeleton backgrounds:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



</description>
      <category>performance</category>
      <category>html</category>
      <category>css</category>
      <category>marko</category>
    </item>
  </channel>
</rss>
